Golang 基础语法
变量和常量
unsafe.Sizeof 确定类型大小
变量
- go 没有隐式类型转换,必须使用显式(任何可转换类型之间),注意精度损失(可以使用 gconv 包)
- 显示类型转换都会发生内存拷贝
- 字符串和数值转换使用
strconv包- 字符串转数字:
strconv.FormatInt/strconv.Itoa这种 - 数字转字符串:
strconv.Atoi这种
- 字符串转数字:
常量
const 表示,不能寻址操作
基本数据类型
数值类型
- 布尔类型:
bool,零值 false - 整数类型:零值 0
- 有符号:
int8、int16、int32、int64 - 无符号:
uint8、uint16、uint32、uint64 - 特殊整数类型:
int,不同平台位数不同,有 32 位或 64 位,上面的与平台无关
- 有符号:
- 浮点数:
float32、float64,零值 0.0 - 复数类型:
complex64,complex128,零值为0+0i
字符串和字符
- 字符:零值为
''- byte:uint8的别名,一个字节,通常表示ASCII字符
- rune:int32的别名,用于表示Unicode码点(Go 使用 UTF-8 编码),不同字符占用 1~4 个字节
- 字符串: byte 切片,零值为
""- 不要直接用索引获取,直接索引获取的是 byte,会导致非 ASCII 字符乱码
- 用 for range 或者转换成 rune 之后进行索引遍历
// 字符串类型,零值为 ""
var s string = "Hello, 世界" // []rune(s)转换成 rune 数组
// 字符类型,零值为 ''
var r rune = '中' // Unicode码点
var b byte = 'A' // byte是uint8的别名,如果给byte赋值'中'会报错溢出复合数据类型
零值都是 nil
除了结构体的零值是所有字段都为零值
1. 结构体(Struct)
// 自定义数据类型
type Person struct {
Name string `json:"id" db:"user_id"` // 如果不加标签就是直接小写,db 标签
private int // 私有字段,就算加了标签也没啥用,包外用不了
}- 结构体内存布局
- 结构体的整体大小必须是最大字段对齐值的整数倍,如果不够进行填充
- 每个字段的起始地址必须是其类型对齐值的整数倍
- 如果前面的类型小,会填充直到满足这个要求
- 因此最好按照字段对齐值的大小降序排列,避免前面的无效填充
- 两个结构体可以直接使用
==进行等值比较(仅限于相同类型,不相同类型会报错)- 如果结构体内部有切片,该能力会丢失(会直接 报错),需要使用
reflect.DeepEqual进行比较
- 如果结构体内部有切片,该能力会丢失(会直接 报错),需要使用
- 结构体标签:
- json:通常是 json.Marshal 和 json.Unmarshal 使用,如果不指定,直接使用变量名(如上面的 ID)
- db:
- xml:
- validate:
- 序列化和反序列化:
- 序列化:
jsonData, _ := json.MarshalIndent(product, "", " ") - 反序列化:
e := json.Unmarshal([]byte(jsonStr), &newProduct)
- 序列化:
- go 没有继承,只能组合,如果匿名组合,相当于拆解了
// 匿名组合
type Address struct {
City, State string
}
type Person struct {
Name string
Address // 匿名字段
}
// Address的所有字段和方法都直接给到了Person,相当于进行了一次解包2. 接口(Interface)
- 一个结构体实现了接口的所有方法,就是实现了这个接口,就能用接口变量指向结构体
// 定义接口
type Writer interface {
Write([]byte) (int, error)
}
type FileWriter struct {
filename string
}
func (fw FileWriter) Write(data []byte) (int, error) {
fmt.Printf("写入文件 %s: %s\n", fw.filename, string(data))
return len(data), nil
}- 空接口可以存储任何类型,
var empty interface{} - 接口尽量的小,如果需要大接口,用小接口的组合实现
- 接口有动态类型和动态值,如果一个函数的参数是接口类型,传入的值是 nil,但是定义好了类型,那么这个参数就不等于 nil
实现代码
// ✅ 好的接口设计:小而专,通过组合进行更多的设计
type Validator interface {
Validate() error
}
type Serializer interface {
Serialize() ([]byte, error)
}
type DeserializerV interface {
Validator
Serializer
}
// ✅ 接受接口,返回具体类型
func ProcessData(validator Validator, serializer Serializer) ([]byte, error) {
if err := validator.Validate(); err != nil {
return nil, err
}
return serializer.Serialize()
}3. 指针(Pointer)
零值是 nil
- 通过
&取地址、*解引用 - 注意 for 循环陷阱
4、数组(Array)/ 切片(slice)
数组
- 固定大小,不能使用append,但能用arr1[start:end]得到切片,之后可以进行append
- 数组作为参数传递进行值传递,复制整个数组的内容,副本内的数值改变不会影响原数据
- slice 两者共享内存,可以使用
copy(copied1, original)或者copied2 := append([]int{}, original...)
- slice 两者共享内存,可以使用
- 不能将一个slice类型和array类型相互交换
- 数组的类型是[2]int,[3]int这种,根据长度不同有不同的类型,切片只有不定常[]int类型 切片
- 切片的Data字段就是对底层数组的引用(两者是上下级关系)
- 所有空切片指向的数组引用地址是一样的(append 之后重新分配)
// 固定长度的数组
var arr1 [5]int // [0 0 0 0 0],通过var不能指定值,只有默认值
arr3 := [...]int{1, 2, 3, 4} // 长度自动推断为4
// 切片,获得长度为 len,容量为 cap
var slice1 []int // nil slice
slice2 := []string{"a", "b", "c"} // 字面量创建
slice3 := make([]int, 5, 10) // 长度5,容量10
slice2 = append(slice2,"d") // 追加元素
slice4 := slice2
slice4[0] = "f" // 输出 slice2 和 slice4 都是["f" "b" "c" "d"]底层结构
// Go 运行时中 slice 的底层结构定义
// 位于 runtime/slice.go
type sliceHeader struct {
array unsafe.Pointer // 指向底层数组的指针,8 字节的指针
len int // 当前长度,8 字节的整数
cap int // 容量,8个字节的整数
}// (在 64 位系统上,总共 24 字节)Slice 的扩容机制
当len大于cap时,会进行扩容
- 如果新容量 > 2倍旧容量,直接使用新容量(append一个很大的值)
- 如果旧容量 < 256,新容量 = 2倍旧容量
- 如果旧容量 >= 256,新容量 = 旧容量 + (旧容量 + 3* 256) / 4 目的:逐渐降低增长率,避免大slice浪费太多内存 容量会被调整到与元素大小和系统内存分配粒度(通常为 8 字节或 16 字节)对齐的边界,保证可用 为了性能,需要避免频繁扩容,通过预分配容量进行,但是不是
make([]int, 10000)而是make([]int, 0, 10000),前者是放了 10000 个 0,容量都被占用了
// ⚠️ 注意:Slice 截取导致的内存泄漏
func sliceMemoryLeak() {
// 问题:bigSlice 的底层数组不会被释放
bigSlice := make([]byte, 1024*1024*100) // 100MB
smallSlice := bigSlice[:10] // 只用10个字节
// ✅ 解决方案:完整拷贝
smallSliceCopy := make([]byte, 10)
copy(smallSliceCopy, bigSlice[:10])
// 现在 bigSlice 可以被 GC 回收了
}陷阱
数组是值传递
- Data部分因为是指针,所以在函数中更改,在外层会看见
- len和cap部分因为是值,所以在函数中更改,在外层不会看见
- 在函数中操作切片
- 如果不超过容量cap值,函数内部的修改外部是能看见的
- 如果超过cap,会重新获取Data指针,内外的切片不是同一个地址,内部的修改外部看不见
- 但是函数内部的扩容后的len会变大,这个因为len是值,里面的修改外面看不到,所以外面想要获得同样的len只能通过切片方式获取,否则会panic
代码实现
func doSlice(slice[]int) {
slice = append(slice, 1)
fmt.Println(slice) // 打印为[0 1 2 3 4 5 6 7 8 1]
fmt.Println(len(slice)) // 打印为10
fmt.Println(cap(slice)) // 打印为10
fmt.Println(&slice[0]) // 打印为0x1000000000
slice = append(slice, 1)
fmt.Println(slice) // 打印为[0 1 2 3 4 5 6 7 8 1]
fmt.Println(len(slice)) // 打印为11
fmt.Println(cap(slice)) // 打印为20,扩容后返回了新的容量
fmt.Println(&slice[0]) // 打印为0x1000000020,扩容后返回了新的Data指针
slice[9] = 10
fmt.Println(slice) // 打印为[0 1 2 3 4 5 6 7 8 10]
}
func main() {
slice := make([]int, 9,10)
for i := 0; i < 9; i++ {
slice[i] = i
}
doSlice(slice)
fmt.Println(slice) // 打印为[0 1 2 3 4 5 6 7 8]
fmt.Println(len(slice), cap(slice)) // 打印为9 10
fmt.Println(&slice[0]) // 打印为0x1000000000
fmt.Println(slice[0:10]) // 打印为[0 1 2 3 4 5 6 7 8 1],看不到扩容后的修改
fmt.Println(slice[10]) // panic,超出长度
}5. 映射(Map)
基本使用
// 键值对集合
var map1 map[string]int // nil map
map2 := make(map[string]int) // 空map
map3 := map[string]int{ // 字面量创建
"apple": 5,
"banana": 3,
}
// 添加元素
fruits["pear"] = 6
// 读取元素
count, exists := fruits["apple"]
if exists {
fmt.Printf("苹果数量: %d\n", count)
}
// 删除元素
delete(fruits, "banana")
// 遍历map
for fruit, count := range fruits {
fmt.Printf("%s: %d\n", fruit, count)
}底层结构
// Go 运行时中 map 的底层结构定义
// 位于 runtime/map.go
// hmap 是 map 的头部结构
type hmap struct {
count int // map 中的键值对数量
flags uint8 // 标志位(是否在写入等)
B uint8 // bucket 数量的对数:2^B 个 bucket
noverflow uint16 // 溢出 bucket 的大概数量
hash0 uint32 // hash 种子
buckets unsafe.Pointer // 指向 2^B 个 bucket 的数组
oldbuckets unsafe.Pointer // 扩容时指向旧的 bucket 数组
nevacuate uintptr // 扩容进度
extra *mapextra // 可选字段
}
// bmap 是 map 的 bucket 结构
type bmap struct {
// tophash 数组存储每个 key 的 hash 值的高8位
// 用于快速判断 key 是否在 bucket 中
tophash [8]uint8
// 实际上还有以下字段(编译器动态生成):
// keys [8]keytype // 8个key
// values [8]valuetype // 8个value
// overflow *bmap // 溢出bucket指针
}
// Map 的内存布局示意
func demonstrateMapStructure() {
m := make(map[string]int)
// hmap 大小:48 bytes (64位系统)
// 包含:
// - count: 8 bytes
// - flags: 1 byte
// - B: 1 byte
// - noverflow: 2 bytes
// - hash0: 4 bytes
// - buckets: 8 bytes (指针)
// - oldbuckets: 8 bytes (指针)
// - nevacuate: 8 bytes
// - extra: 8 bytes (指针)
fmt.Printf("Map header大小: %d bytes\n", unsafe.Sizeof(m)) // 8 bytes (只是指针)
// 每个 bucket 可以存储 8 个键值对
// 如果超过8个,会创建溢出 bucket
}扩容机制详解
package main
import "fmt"
func demonstrateMapGrowth() {
// Map 的两种扩容情况:
// 1. 负载因子过大(元素过多)
// 触发条件:count / (2^B * 8) > 6.5
// 扩容方式:翻倍扩容(2^B -> 2^(B+1))
// 2. 溢出 bucket 过多(但元素不多)
// 触发条件:noverflow >= 2^B
// 扩容方式:等量扩容(重新排列,减少溢出bucket)
m := make(map[int]int)
fmt.Println("=== Map 扩容演示 ===")
// 插入大量数据观察扩容
for i := 0; i < 100; i++ {
m[i] = i
if i%10 == 0 {
fmt.Printf("插入 %d 个元素\n", i+1)
}
}
}
// 渐进式扩容:不会一次性迁移所有数据
func demonstrateIncrementalRehash() {
// Go 的 map 扩容是渐进式的:
// 1. 分配新的 bucket 数组
// 2. 每次操作时迁移1-2个旧bucket
// 3. 查找时会同时查新旧bucket
// 优点:
// - 避免扩容时的长时间停顿
// - 分摊扩容成本到每次操作
// 缺点:
// - 扩容期间内存占用翻倍
// - 查找需要查两个bucket数组
}为了避免多次扩容,可以考虑预分配容量:m := make(map[int]int, 10000)
map 不是并发安全的,可以使用锁或者 sync.Map
- 如果删除一个 key(不准确,需要再确认)
- 元素是值类型,不会自动释放内存
- 元素是引用类型,会自动释放内存
- 将 map 置为 nil 后,内存自动回收
6. 通道(Channel)
底层结构
// Go 运行时中 channel 的底层结构定义
// 位于 runtime/chan.go
type hchan struct {
qcount uint // 队列中的元素个数
dataqsiz uint // 环形队列的大小
buf unsafe.Pointer // 指向环形队列的指针
elemsize uint16 // 元素大小
closed uint32 // 是否关闭
elemtype *_type // 元素类型
sendx uint // 发送索引
recvx uint // 接收索引
recvq waitq // 等待接收的 goroutine 队列
sendq waitq // 等待发送的 goroutine 队列
lock mutex // 互斥锁
}
// waitq 是等待队列
type waitq struct {
first *sudog
last *sudog
}
// Channel 的内存布局
func demonstrateChannelStructure() {
ch := make(chan int, 3)
// hchan 结构大小:96 bytes (64位系统)
// 包含:
// - 环形缓冲区(buf)
// - 发送/接收索引(sendx/recvx)
// - 等待队列(recvq/sendq)
// - 互斥锁(lock)
fmt.Printf("Channel大小: %d bytes\n", unsafe.Sizeof(ch)) // 8 bytes (只是指针)
}- 写入流程:
- 1、检查 channel 是否关闭,关闭了直接 panic(为了安全写入,可以增加 recover,当 channel 关闭直接丢弃数据)
- 2、无锁快速检查:
- 无缓冲且有接收者,直接 CAS 发送,唤醒接收者
- 有缓冲且未满,尝试 CAS 写入,不成功继续下一步
- 3、加锁完整处理
- 加锁后再次检查关闭状态
- 写入缓冲区并唤醒接收者,缓冲区满了会进入 sendq 并阻塞等待,否则直接返回
- 读取流程
- 1、判定
qcount是否大于 0- 大于零直接 CAS 读取,返回(data,true)
- 已经关闭返回 (zero_value,false)
- 2、加锁
- 有数据,直接读取
- 没有数据,加入 recvq 阻塞,并唤醒发送者
- 读取最好使用
v1, ok1 := <-ch第二个返回值确定是否有数据 - 或者使用
for value := range ch,range 会在 channel 关闭时自动退出 for select如果只有一个分支v1, ok1 := <-ch,需要在ok1==false时退出循环,否则死循环
- 1、判定
- 对于 nil Channel 操作
- 读:永久阻塞
- 写:永久阻塞
- 关闭:panic
- 因为在关闭时,会判定如果是 nil,直接 panic
- 在读取时,会先判定是否为 nil,如果是 nil,会直接返回,导致一直在自旋
- 注意,虽然close 之后,如果channel里面还有数据,会继续读取,但是如果直接将 channel 赋值为 nil,会丢失剩余的数据,并阻塞
最佳实践
使用 select 处理多个 channel,实现多路复用
- select 机制用来处理异步 IO 问题
- 最大的限制是每个 case 语句必须是一个 IO 操作
- 1、使用超时控制,避免一致阻塞:
case <- time.After(1 * time.Second) - 2、使用信号通知关闭:
case <-done - 3、默认分支保证不会一直阻塞:
default:
实现
func basicSelect() {
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch1 <- "来自ch1的消息"
select {
case msg1,ok := <-ch1:
if ok {
fmt.Printf("接收到: %s\n", msg1)
}
case <-time.After(1 * time.Second):
fmt.Println("操作超时")
case <-done:
fmt.Println("处理完成")
return
default:
fmt.Println("没有可用的通道操作")
}
}select 特性
- 非阻塞特性:关闭的 Channel 不会阻塞 select,总是返回 nil
- 但是如果是 nil 的 channel 会阻塞 select 造成死锁,如果只有一个 chan 的 select 风险很高,最好加上 default 保证不会死锁
- 随机选择:多个可用 case 时随机选择
复杂对象初始化
| 特性 | new | make |
|---|---|---|
| 核心目的 | 分配内存,返回地址 | 初始化复杂结构,立即可用 |
| 适用类型 | 所有类型 | 仅slice、map、channel |
| 返回值 | 指针(*T) | 类型本身(T) |
| 初始化 | 零值初始化 | 完整初始化 |
| 是否可用 | 引用类型不可用(nil),需要使用(*v) | 立即可用 |
复杂对象零值可用性
| 操作 | nil slice | nil map | nil chan |
|---|---|---|---|
len() | 返回 0 | 返回 0 | 不可用(编译错误) |
range | 正常(0 次迭代) | 正常(0 次迭代) | 永久阻塞(无元素可取) |
| 读取元素 | panic(索引越界) | 返回零值,false | 永久阻塞 |
| 写入元素 | panic(索引越界) | panic(写入 nil map) | 永久阻塞 |
append | 正常工作 | 不适用 | 不适用 |
close | 不适用 | 不适用 | panic(关闭 nil chan) |
- map,chan 必须使用 make 初始化,否则内部数据都是零值,无法使用
- slice 可以使用 new 初始化,只是因为 append 内部实现会检查,如果为 nil 会分配data 内存,使得可用
make
- slice:分配+初始化,返回可用slice
- map:创建hash表结构
- chan:创建通道+缓冲区+锁
- make 不返回指针是因为 slice、map、channel 本身内部就是通过指针指向实际数据,不需要"指针的指针"
new
- 返回零值的指针,和 var 类似,但是一个指针,一个值
闭包和变量捕获
点击查看完整代码实现
func closureExample() {
// 错误的方式:循环变量捕获
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Printf("错误方式 - i: %d\n", i) // 都输出3
})
}
// 正确的方式:创建局部变量
var funcs2 []func()
for i := 0; i < 3; i++ {
i := i // 创建新的局部变量
funcs2 = append(funcs2, func() {
fmt.Printf("正确方式 - i: %d\n", i) // 输出0,1,2
})
}
fmt.Println("执行错误方式的闭包:")
for _, f := range funcs {
f() // 都输出 3
}
fmt.Println("执行正确方式的闭包:")
for _, f := range funcs2 {
f() // 输出 0, 1, 2
}
}函数和方法
Go不支持函数重载(同名不同参数),可以通过范型实现 可以将函数赋值给变量,或者作为返回值
// 将函数赋值给变量
var op BinaryOperation = add
result := op(3, 4)
// 函数切片
operations := []BinaryOperation{add, multiply}
for i, op := range operations {
fmt.Printf("operations[%d](5, 6) = %d\n", i, op(5, 6))
}
// 函数映射
opMap := map[string]BinaryOperation{
"add": add,
"multiply": multiply,
}
// 小函数会被内联
func add(a, b int) int { return a + b }
func multiply(a, b int) int { return a * b }可变参数
- 可以普通参数+可变参数
// 混合参数(普通参数 + 可变参数),可变参数必须放在最后
func printf(format string, args ...interface{}) {
fmt.Printf(format, args...)
}闭包
// 闭包函数
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
// 闭包捕获外部变量
func multiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
func demonstrateClosures() {
// 每个counter都有自己的状态
c1 := counter()
c2 := counter()
fmt.Printf("c1(): %d\n", c1()) // 1
fmt.Printf("c1(): %d\n", c1()) // 2
fmt.Printf("c2(): %d\n", c2()) // 1
fmt.Printf("c1(): %d\n", c1()) // 3
// 闭包捕获参数
double := multiplier(2)
triple := multiplier(3)
fmt.Printf("double(5) = %d\n", double(5)) // 10
fmt.Printf("triple(5) = %d\n", triple(5)) // 15
}匿名函数和立即执行函数
func demonstrateAnonymousFunctions() {
// 匿名函数赋值给变量
square := func(x int) int {
return x * x
}
fmt.Printf("square(4) = %d\n", square(4)) // 16
// 立即执行函数
result := func(a, b int) int {
return a + b
}(3, 4)
fmt.Printf("立即执行函数结果: %d\n", result) // 7
}方法和接收器
type Rectangle struct {
Width float64
Height float64
}
// 值接收器方法
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// 值接收器方法(不会修改原对象)
// 可以被指和指针调用,对于指针而言会自动解引用 (*rectPtr).Area()
func (r Rectangle) Scale(factor float64) Rectangle {
r.Width *= factor
r.Height *= factor
return r
}
// 指针接收器方法(会修改原对象)
// 可以被值和指针调用,对于值而言会自动取地址 (&rect).ScaleInPlace(2)
func (r *Rectangle) ScaleInPlace(factor float64) {
r.Width *= factor
r.Height *= factor
}嵌套继承
// 父结构体
type Animal struct {
Name string
}
func (a Animal) Speak() {
fmt.Println(a.Name, "is making a sound")
}
// 子结构体
type Dog struct {
Animal // 嵌套 Animal,相当于继承 Breed string
}
func main() {
dog := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
}
dog.Speak() // 调用嵌套结构体的方法
fmt.Println(dog.Name, "is a", dog.Breed)
}
// 子结构也可以进行方法重写
func (d Dog) Speak() { fmt.Println(d.Name, "is barking") }类型断言和转换详解
从接口值中获取具体类型值
- 可以是基本数据类型或者是结构体、接口类型
- 非接口值无法进行断言操作
// 空接口存储不同类型的值
var x interface{}
// 不安全的方式(如果类型不符合会panic)
value := x.(T)
// 安全的方式(推荐)
value, ok := x.(T)x必须是接口类型,非接口类型不能做类型断言- 如果
T是非接口类型,则T必须实现x的接口 - 如果
T是接口类型,则x的动态类型也应该实现接口T
interface实现
type iface struct {
tab *itab
data unsafe.Pointer
}tab 中存放的是类型、方法等信息。data 指针指向的 iface 绑定对象的原始数据的副本。
类型断言是否发生拷贝取决于接口内部持有的数据类型:
值类型:当接口持有的是值类型(例如
int、float、struct等),进行类型断言时会发生拷贝,因为接口存储的是这个值的副本,断言后得到的是该值的拷贝。引用类型:当接口持有的是引用类型(例如指针、切片、映射、通道等),进行类型断言时不会发生拷贝,因为接口存储的是一个引用,断言得到的也是相同的引用。
因此,如果接口中存储的是一个结构体实例,通过断言得到的是结构体的值拷贝,修改断言后的变量不会影响接口中的值;而如果接口中存储的是指针,通过断言得到的依然是指针引用,修改断言后的指针值会影响接口内的原数据。
常见面试追问
Q1: 如何判断接口中存储的是值类型还是引用类型?
func checkInterfaceType(i interface{}) {
//通过反射
t := reflect.TypeOf(i)
fmt.Printf("类型: %v\n", t)
fmt.Printf("类型种类: %v\n", t.Kind())
switch t.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func:
fmt.Println("这是引用类型")
default:
fmt.Println("这是值类型")
}
// 通过断言
switch v := x.(type) {
case nil:
fmt.Println("x是nil")
case bool:
fmt.Printf("x是bool类型,值为%t\n", v)
default:
fmt.Printf("x是未知类型%T,值为%v\n", v, v)
}
}控制流和错误处理
if语句
- 可以在 if 语句中初始化,
if num := rand.Intn(100); num < 50 - else if 和 else 不能换行
- if 后面不需要跟
()
2. switch语句的多种形式
和其他语言相比,自带break,想要穿透,需要fallthrough
- switch表达式
- 是可选的,如果省略,等价于switch true,即每个 case 可以看成一个 if 语句
- 可以在 switch 中进行赋值操作,类似于 if 赋值操作
- 可以通过
switch x := v.(type),case 中跟数据类型判定变量的类型 - default 是都匹配不上的默认值,等价于 else
- case允许匹配多个条件
3. for循环的各种形式
for range循环遍历切片时,切片长度在开始遍历时就已经被固定下来,即使循环中使用append动态修改切片的长度,也不会影响range的遍历次数。
代码实现
// 经典for循环
for i := 0; i < 5; i++ {
fmt.Printf("%d ", i)
}
// while风格的for循环
i := 0
for i < 5 {
fmt.Printf("%d ", i)
i++
}
// 无限循环
for {
fmt.Printf("计数\n")
}
// for-range循环
numbers := []int{1, 2, 3, 4, 5}
for index, value := range numbers {
// 如果只要索引,value 可以省略,只要值,index 改成 _,map 获取的是 kv
fmt.Printf("索引%d: 值%d\n", index, value)
}break 和 continue,还有标签(一般不用,goto)
####陷阱
- go 1.22 之前 for 循环中的变量是一个循环只有一个,因此如果在 for 循环中使用变量容易出错,如果对变量取地址,也都是取到最后的值
- 需要使用临时变量或者闭包的方案解决
- go 1.22 之后对这个常见问题进行了解决,每次循环都是一个(新的变量)[https://go.dev/blog/loopvar-preview](但是为了兼容性,一般还是需要上面的解决方案,除非可以保证一定不会被 go 1.22 之前版本使用)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v) // 输出一般都是 c,看执行速度
}()
}
var pointers []*int
for i := 0; i < 3; i++ {
pointers = append(pointers, &i) // 危险!都指向同一个变量
}
// 正确的做法1:使用临时变量
var correctPointers1 []*int
for i := 0; i < 3; i++ {
temp := i // 创建临时变量
correctPointers1 = append(correctPointers1, &temp)
}
// 正确的做法2:使用闭包
var correctPointers2 []*int
for i := 0; i < 3; i++ {
func(val int) {
correctPointers2 = append(correctPointers2, &val)
}(i)
}错误处理
内置的error接口
type error interface {
Error() string
}- 可以使用
errors.New或者fmt.Errorf创建错误 - 可以使用
errors.Is进行错误类型比较 - 可以用
errors.As将错误转换成特定类型 - 定义一个结构体实现 Error() 函数就能自定义错误
- errors.Unwrap(err)
哨兵错误:预定义的错误值,用于表示特定的错误条件
// 定义哨兵错误
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidCredentials = errors.New("invalid credentials")
)panic和recover机制详解
goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。
defer 语句的执行顺序和定义顺序相反(栈的压入弹出) 用来制定一些必须释放的资源或者必须记录的日志等,
- 如打开文件等情况,或者开启事务,不报错提交,报错回滚。
- 如果在 defer 中直接获取参数值,获取当前值,如果是闭包获取,获取是终态值
func deferVariableCapture() {
x := 1
// defer会在声明时捕获参数的值
defer fmt.Printf("defer 1: x = %d\n", x) // 输出: x = 1
x = 2
defer fmt.Printf("defer 2: x = %d\n", x) // 输出: x = 2
// 使用闭包可以捕获变量的引用
defer func() {
fmt.Printf("defer 3 (closure): x = %d\n", x) // 输出: x = 3
}()
x = 3
fmt.Printf("函数结束时: x = %d\n", x) // 输出: x = 3
}
// 输出顺序:
// 函数结束时: x = 3
// defer 3 (closure): x = 3
// defer 2: x = 2
// defer 1: x = 1- defer 中可以修改字面量返回值,不能修改变量返回值
func deferWithNamedReturn() (result int) {
defer func() {
result++ // 可以修改命名返回值
}()
return 5 // 实际返回 6
}
func deferWithAnonymousReturn() int {
result := 5
defer func() {
result++ // 这不会影响返回值
}()
return result // 返回 5
}
func demonstrateDeferReturn() {
fmt.Printf("命名返回值: %d\n", deferWithNamedReturn()) // 6
fmt.Printf("匿名返回值: %d\n", deferWithAnonymousReturn()) // 5
}常见 panic 触发方式
- 手动 panic:
panic("message"),可以传入任何类型值。 - 数组/切片越界:
a := make([]int,0); a[1] - 空指针解引用:
var p *int; fmt.Println(*p) - 类型断言失败:
var i interface{}="string"; num := i.(int) - 向已经关闭的 channel 发送数据
- 除零
- panic 后面的内容都不会执行,前面的都会执行,但是 defer 都会执行
- 一般只允许程序初始化失败、配置文件加载失败、数据库连接失败出现 panic,其他的尽量不要 panic recover的使用和限制
if r := recover(); r != nil {
fmt.Printf("方式1 - 直接recover: %v\n", r)
}- 限制:只能在 defer 函数中使用,否则无法捕获 panic,只能捕获当前层级的 panic,更高层的 panic 无法捕获,即调用函数中的 recover 只能捕获子函数的 panic,不能捕获当前层级的
- 只能捕获当前 goroutine 中的 panic,其他 goroutine 中的需要另外的捕获
- 没有 panic,recover
包的概念和管理详解 - Golang基础面试题
包结构示例
/*
项目结构示例:
myapp/
├── main.go
├── go.mod
├── go.sum
├── internal/ // 内部包,不能被外部导入
│ ├── config/
│ │ └── config.go
│ └── database/
│ └── db.go
├── pkg/ // 可以被外部导入的库代码
│ ├── auth/
│ │ ├── auth.go
│ │ └── token.go
│ ├── utils/
│ │ ├── string.go
│ │ └── time.go
│ └── models/
│ ├── user.go
│ └── product.go
├── cmd/ // 应用程序入口点
│ ├── server/
│ │ └── main.go
│ └── cli/
│ └── main.go
├── api/ // API定义
│ └── v1/
│ └── handlers.go
├── web/ // Web资源
│ ├── static/
│ └── templates/
└── docs/ // 文档
└── README.md
*/包导入
- 每个Go文件必须以package声明开始
- 包名通常与目录名一致,一个目录一个包
- main包: 特殊的包,用于创建可执行程序
package math // 当前文件所在包,可以通过 path/package_name 导入
// 多个包一起导入
import (
"errors"
"fmt"
)
// 导入单个包
import math
// 给导入包起别名,防止名字太长或者重名,默认名称就是包名
k8s "k8s.io/client-go/kubernetes"
// 空白导入(仅执行init函数)
_ "time/tzdata"
// 全部导入,使用时不需要包名前缀
. "time/tzdata"导出
- 对于一个包内部的变量、函数、结构体等
- 如果首字符大写,表示导出量,通过 import 包之后可以直接使用
- 如果首字符小写,表示包级别变量,import 之后无法使用,只能在包内部使用
初始化要点
- 初始化顺序: 包级变量 → init函数 → main函数
- 依赖顺序: 被依赖的包先初始化
- 多个init: 一个包可以有多个init函数
- 副作用: 避免在init中执行复杂逻辑
Go 模块
Go模块是相关Go包的集合,它们一起进行版本控制。模块由以下组件构成:
- go.mod文件:模块定义文件
- go.sum文件:模块校验和文件
- 模块路径:唯一标识模块的路径
- 版本:遵循语义化版本控制
go.mod文件示例
// go.mod文件示例
module github.com/username/myproject
go 1.19
require (
github.com/gin-gonic/gin v1.9.1
github.com/go-redis/redis/v8 v8.11.5
github.com/golang-jwt/jwt/v4 v4.5.0
gorm.io/gorm v1.25.4
)
require (
// 间接依赖
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
// ... 更多间接依赖
)
// 排除特定版本
exclude github.com/some/package v1.2.3
// 替换依赖
replace github.com/old/package => github.com/new/package v1.0.0
replace github.com/local/package => ./local/package模块使用
# 初始化新模块
go mod init github.com/username/myproject
# 添加依赖
go get github.com/gin-gonic/gin
# 添加特定版本的依赖
go get github.com/gin-gonic/gin@v1.9.1
# 升级依赖到最新版本
go get -u github.com/gin-gonic/gin
# 升级所有依赖到最新版本
go get -u all
# 下载依赖
go mod download
# 验证依赖
go mod verify
# 清理不需要的依赖
go mod tidy
# 查看依赖图
go mod graph
# 查看模块信息
go list -m all
# 查看特定模块的可用版本
go list -m -versions github.com/gin-gonic/gin模块发布
# 1. 确保代码质量
go fmt ./...
go vet ./...
go test ./...
# 2. 更新依赖
go mod tidy
# 3. 验证模块
go mod verify
# 4. 创建版本标签
git add .
git commit -m "Release v1.0.0"
git tag v1.0.0
git push origin v1.0.0
# 5. 发布到模块代理
go list -m github.com/username/mymodule@v1.0.0
# 6. 验证发布
go get github.com/username/mymodule@v1.0.0私有模块管理
# 1. 设置私有模块路径
export GOPRIVATE="github.com/mycompany/*,gitlab.mycompany.com/*"
# 2. 配置Git认证
git config --global url."git@github.com:mycompany/".insteadOf "https://github.com/mycompany/"
# 3. 配置模块代理(跳过私有模块)
export GOPROXY="https://proxy.golang.org,direct"
export GONOPROXY="github.com/mycompany/*"
export GONOSUMDB="github.com/mycompany/*"
# 4. 企业内部代理设置
export GOPROXY="https://goproxy.mycompany.com,https://proxy.golang.org,direct"