Skip to content

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 之后进行索引遍历
go
// 字符串类型,零值为 ""
var s string = "Hello, 世界" // []rune(s)转换成 rune 数组

// 字符类型,零值为 ''
var r rune = ''         // Unicode码点
var b byte = 'A'         // byte是uint8的别名,如果给byte赋值'中'会报错溢出

复合数据类型

零值都是 nil

除了结构体的零值是所有字段都为零值

1. 结构体(Struct)

go
// 自定义数据类型
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 没有继承,只能组合,如果匿名组合,相当于拆解了
go
// 匿名组合
type Address struct { 
	City, State string 
} 
type Person struct { 
	Name string 
	Address // 匿名字段 
}
// Address的所有字段和方法都直接给到了Person,相当于进行了一次解包

2. 接口(Interface)

  • 一个结构体实现了接口的所有方法,就是实现了这个接口,就能用接口变量指向结构体
go
// 定义接口
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
实现代码
go
// ✅ 好的接口设计:小而专,通过组合进行更多的设计
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类型和array类型相互交换
  • 数组的类型是[2]int,[3]int这种,根据长度不同有不同的类型,切片只有不定常[]int类型 切片
  • 切片的Data字段就是对底层数组的引用(两者是上下级关系)
  • 所有空切片指向的数组引用地址是一样的(append 之后重新分配)
go
// 固定长度的数组
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
// Go 运行时中 slice 的底层结构定义
// 位于 runtime/slice.go
type sliceHeader struct {
    array unsafe.Pointer  // 指向底层数组的指针,8 字节的指针
    len   int             // 当前长度,8 字节的整数
    cap   int             // 容量,8个字节的整数
}// (在 64 位系统上,总共 24 字节)
Slice 的扩容机制

当len大于cap时,会进行扩容

  1. 如果新容量 > 2倍旧容量,直接使用新容量(append一个很大的值)
  2. 如果旧容量 < 256,新容量 = 2倍旧容量
  3. 如果旧容量 >= 256,新容量 = 旧容量 + (旧容量 + 3* 256) / 4 目的:逐渐降低增长率,避免大slice浪费太多内存 容量会被调整到与元素大小和系统内存分配粒度(通常为 8 字节或 16 字节)对齐的边界,保证可用 为了性能,需要避免频繁扩容,通过预分配容量进行,但是不是make([]int, 10000) 而是make([]int, 0, 10000),前者是放了 10000 个 0,容量都被占用了
go
// ⚠️ 注意: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
代码实现
go
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)

基本使用
go
// 键值对集合
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
// 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
}
扩容机制详解
go
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
// 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时退出循环,否则死循环
  • 对于 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:
实现
go
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 特性

  1. 非阻塞特性:关闭的 Channel 不会阻塞 select,总是返回 nil
    1. 但是如果是 nil 的 channel 会阻塞 select 造成死锁,如果只有一个 chan 的 select 风险很高,最好加上 default 保证不会死锁
  2. 随机选择:多个可用 case 时随机选择

复杂对象初始化

特性newmake
核心目的分配内存,返回地址初始化复杂结构,立即可用
适用类型所有类型仅slice、map、channel
返回值指针(*T)类型本身(T)
初始化零值初始化完整初始化
是否可用引用类型不可用(nil),需要使用(*v)立即可用

复杂对象零值可用性

操作nil slicenil mapnil 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 类似,但是一个指针,一个值

闭包和变量捕获

点击查看完整代码实现
go
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不支持函数重载(同名不同参数),可以通过范型实现 可以将函数赋值给变量,或者作为返回值

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 }

可变参数

  • 可以普通参数+可变参数
go
// 混合参数(普通参数 + 可变参数),可变参数必须放在最后
func printf(format string, args ...interface{}) {
    fmt.Printf(format, args...)
}

闭包

go
// 闭包函数
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
}

匿名函数和立即执行函数

go
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
}

方法和接收器

go
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
}

嵌套继承

go
// 父结构体 
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") }

类型断言和转换详解

从接口值中获取具体类型值

  • 可以是基本数据类型或者是结构体、接口类型
  • 非接口值无法进行断言操作
go
// 空接口存储不同类型的值
var x interface{}

// 不安全的方式(如果类型不符合会panic)
value := x.(T)

// 安全的方式(推荐)
value, ok := x.(T)
  • x 必须是接口类型,非接口类型不能做类型断言
  • 如果 T 是非接口类型,则 T 必须实现 x 的接口
  • 如果 T 是接口类型,则 x 的动态类型也应该实现接口 T

interface实现

go
type iface struct {
	tab  *itab
	data unsafe.Pointer
}

tab 中存放的是类型、方法等信息。data 指针指向的 iface 绑定对象的原始数据的副本。

类型断言是否发生拷贝取决于接口内部持有的数据类型:

  • 值类型:当接口持有的是值类型(例如 intfloatstruct 等),进行类型断言时会发生拷贝,因为接口存储的是这个值的副本,断言后得到的是该值的拷贝。

  • 引用类型:当接口持有的是引用类型(例如指针、切片、映射、通道等),进行类型断言时不会发生拷贝,因为接口存储的是一个引用,断言得到的也是相同的引用。

因此,如果接口中存储的是一个结构体实例,通过断言得到的是结构体的值拷贝,修改断言后的变量不会影响接口中的值;而如果接口中存储的是指针,通过断言得到的依然是指针引用,修改断言后的指针值会影响接口内的原数据。

常见面试追问

Q1: 如何判断接口中存储的是值类型还是引用类型?

go
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 的遍历次数。

代码实现
go
// 经典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 之前版本使用)
go
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接口

go
type error interface {
    Error() string
}
  • 可以使用errors.New或者fmt.Errorf创建错误
  • 可以使用errors.Is进行错误类型比较
  • 可以用errors.As将错误转换成特定类型
  • 定义一个结构体实现 Error() 函数就能自定义错误
  • errors.Unwrap(err)

哨兵错误:预定义的错误值,用于表示特定的错误条件

go
// 定义哨兵错误
var (
    ErrUserNotFound     = errors.New("user not found")
    ErrInvalidCredentials = errors.New("invalid credentials")
)

panic和recover机制详解

goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。

defer 语句的执行顺序和定义顺序相反(栈的压入弹出) 用来制定一些必须释放的资源或者必须记录的日志等,

  • 如打开文件等情况,或者开启事务,不报错提交,报错回滚。
  • 如果在 defer 中直接获取参数值,获取当前值,如果是闭包获取,获取是终态值
go
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 中可以修改字面量返回值,不能修改变量返回值
go
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的使用和限制
go
if r := recover(); r != nil {
                fmt.Printf("方式1 - 直接recover: %v\n", r)
            }
  • 限制:只能在 defer 函数中使用,否则无法捕获 panic,只能捕获当前层级的 panic,更高层的 panic 无法捕获,即调用函数中的 recover 只能捕获子函数的 panic,不能捕获当前层级的
  • 只能捕获当前 goroutine 中的 panic,其他 goroutine 中的需要另外的捕获
  • 没有 panic,recover

包的概念和管理详解 - Golang基础面试题

包结构示例

go
/*
项目结构示例:

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包: 特殊的包,用于创建可执行程序
go
package math  // 当前文件所在包,可以通过 path/package_name 导入

// 多个包一起导入
import (
    "errors"
    "fmt"
)

// 导入单个包
import math

// 给导入包起别名,防止名字太长或者重名,默认名称就是包名
k8s "k8s.io/client-go/kubernetes"

// 空白导入(仅执行init函数)
_ "time/tzdata"

// 全部导入,使用时不需要包名前缀
. "time/tzdata"

导出

  • 对于一个包内部的变量、函数、结构体等
    • 如果首字符大写,表示导出量,通过 import 包之后可以直接使用
    • 如果首字符小写,表示包级别变量,import 之后无法使用,只能在包内部使用

初始化要点

  1. 初始化顺序: 包级变量 → init函数 → main函数
  2. 依赖顺序: 被依赖的包先初始化
  3. 多个init: 一个包可以有多个init函数
  4. 副作用: 避免在init中执行复杂逻辑

Go 模块

Go模块是相关Go包的集合,它们一起进行版本控制。模块由以下组件构成:

  1. go.mod文件:模块定义文件
  2. go.sum文件:模块校验和文件
  3. 模块路径:唯一标识模块的路径
  4. 版本:遵循语义化版本控制

go.mod文件示例

go
// 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

模块使用

bash
# 初始化新模块
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

模块发布

bash
# 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

私有模块管理

bash
# 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"

正在精进