Go

函数传参是值类型还是引用类型?

  • 无论如何都是值传递

    • 基本类型(int, string等):拷贝值

    • 结构体:拷贝值

    • 指针类型:拷贝值,但由于指针指向变量的地址,所以能实现修改值

    • slice/map/channel:依旧拷贝值,能实现修改值是因为内部有指针指向同一个数组

      比如 slice 的本质是:

      1
      2
      3
      4
      5
      type slice struct {
      ptr *T // 指针变量共享一个数组
      len int
      cap int
      }

数组和切片的区别?

  • 长度角度:

    • 数组长度不可变,在定义时就确定了,[3]int[4]int 不是一个类型
    • 切片长度可变。可使用 append() 来扩容切片
  • 是否为值类型角度:

    • 数组是值类型,值拷贝

      1
      2
      3
      4
      a := [3]int{1,2,3}
      b := a
      b[0] = 0
      fmt.Println(a) // [1 2 3]
    • 切片是引用类型,底层共享数组

      1
      2
      3
      4
      s1 := []int{1,2,3}
      s2 := s1
      s2[0] = 100
      fmt.Println(s1) // [100 2 3]
  • 底层实现角度:

    • slice本质是一个结构体,三个属性:数组指针、长度len、容量cap,指针指向了底层数组。切片可以视作数组的窗口(view)

说说空结构体 struct{}

  1. 空结构体的大小:0,也就是说他不占内存空间

    1
    fmt.Println(unsafe.Sizeof(struct{}{})) // 0
  2. 我们可以struct{} 作为 map 的值来模拟一个 set

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type Set map[string]struct{}  // typedef

    set := make(Set)
    // 添加数据
    for _, v := range []string{"a", "a", "b", "c"} {
    set[v] = struct{}{}
    }
    // 检查
    if _, ok := set["a"] {
    fmt.Println("a exists")
    }
  3. 管道发送空结构体,代表一种信号,可以节省空间,代替 bool

    1
    ch <- struct{}{}

init() 函数是什么时候执行的?

  • init() 函数是 golang 中用来初始化包的函数,在 main() 函数之前自动执行,可以放一些字段的初始化逻辑。
  • 一个 go pkg的初始化流程:依赖包 import -> 常量 -> 变量 -> init() -> main()(如果有)
  • 一个包可以有多个init()执行时不保证先后顺序

两个 interface 可以比较吗?

可以。interface可以用 == 比较。一看interface 的动态类型,二看interface 的实际值

  • 一个 interface 内部包含两部分:(type, value)
  • 比较时,一看 动态type:
    • type一致,往下继续看值
    • type不一致,返回 false
    • type一致,但不是“可比较类型”,返回 false
      • 不可比较类型有:slice, map, func 等,channel 可比较
  • 二看 实际value,一致则返回 true:
    • 两个 nil,一致
    • 一个nil一个非nil,不一致
    • 两个值类型(int, string)或结构体内部字段值 相等,一致
    • 两个指针类型,要看是不是指向同一个内存地址,地址相同则一致

几个典型示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type MyImpl struct {
Name string
}

func main() {
// nil
var nili1 interface{} = nil
var nili2 interface{} = nil
fmt.Println("- nil(non-typed) == nil(non-typed): ", nili1 == nili2) // true

// typed nil
var typedi *int = nil
nili2 = typedi
fmt.Println("- nil(non-typed) == nil(*int): ", nili1 == nili2) // false

// same runtime type, same value
var myImpl1 interface{} = MyImpl{"aaa"}
var myImpl2 interface{} = MyImpl{"aaa"}
fmt.Println("- abc(MyImpl) == abc(MyImpl): ", myImpl1 == myImpl2) // true

// same runtime type, different values
var myImplptr1 interface{} = &MyImpl{"aaa"}
var myImplptr2 interface{} = &MyImpl{"aaa"}
fmt.Println("- abc(*MyImpl) == abc(*MyImpl): ", myImplptr1 == myImplptr2) // false

// uncomparable type
var slice1 interface{} = []int{1, 2, 3}
var slice2 interface{} = []int{1, 2, 3}
fmt.Println("- [1 2 3]([]int) == [1 2 3]([]int): ", slice1 == slice2) // panic
}

Slice

八股

切片源码见 runtime/slice.go

Slice底层结构

底层三个属性

1
2
3
4
5
type Slice struct {
array unsafe.Pointer // 指向底层的数组
len int // 长度
cap int // 容量
}

slice的扩容机制

源码见 growslice()nextslicecap() 方法,后者计算新容量。

计算新容量的代码很直接:

  1. 如果期望长度 > 2 x 旧容量,取期望长度

  2. 以阈值256 分两种情况

    1. 如果旧容量 < 256,容量翻倍,即2 x 旧容量

    2. 如果旧容量 > 256,新容量的计算公式为 (旧容量 + 3 * 256) >> 2,右移2代表除以4。

      意思是对于大切片的扩容,每次*近似 1.25,避免内存浪费

slice扩容后还共享底层数组吗?

看如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]

s2 = append(s2, 100)
s2 = append(s2, 200)
s1[2] = 20

fmt.Println(s1)
fmt.Println(s2)
fmt.Println(slice)
}

回答:

  1. s1, s2 初始化多少?
  2. 最后程序输出什么?

slice作为参数传递,可能改变原来的slice吗?

分情况,这要看你有没有更改slice指向的数组值。因为slice本身会拷贝一份副本当成参数传入,底层三个属性:指向数组的指针、len、cap都是副本,但是如果在方法中修改底层数组的话,原有的slice是会变的。

Channel

小徐先生:https://mp.weixin.qq.com/s/QgNndPgN1kqxWh-ijSofkw

引入

Golang有个著名的思想:

不要通过共享内存来通信,而是通过通信来共享内存

Java/C++ 中多线程的写法常常是:多个线程中共享某些变量,再通过信号量/锁机制来解决同步问题。本质上是先共享内存,以此来解决通信问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var count int
var mu sync.Mutex

go func() {
mu.Lock()
count++
mu.Unlock()
}()

go func() {
mu.Lock()
count++
mu.Unlock()
}()

而 Golang 引入了 Channel 管道来解决 Goroutine之间的通信问题,并以此实现数据共享。我们可以明显看到好处:

  1. Goroutine之间不需要共享内存变量,它们除了将数据传给ch以外 对内存是无感知的。避免了因为共享内存而不得不引入同步,加锁/信号这些繁杂的操作,减少错误。
  2. 内存变量如果是敏感数据,共享给不同的线程/Goroutine增加风险;而基于Channel通信别的Goroutine根本访问不到这些敏感变量,提高安全性。

这里有一个银行账户的例子:

  • 传统的共享内存的实现:
1
2
3
4
5
6
7
8
9
10
11
12
type Account struct {
int balance
mu sync.Mutex // 加锁
}

// Withdraw: 取钱
func (a *Acount) Withdraw(amount int) {
mu.Lock()
defer mu.Unlock()

a.balance -= amount
}
  • 基于Channel实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
      type Withdraw struct {
    amount int
    }

    reqCh := make(chan Withdraw)

    // 取钱实现:不断读管道数据,每次都更新账户
    go func() {
    account := &Account{1000}

    for req := range reqCh {
    account.balance -= req.amount
    }
    }()

    // 调用取钱的时候:往管道里塞数据就行
    reqCh <- Withdraw{100}



    ## 核心数据结构

    Channel 底层是一个 `hchan` 的结构体,它的缓冲区是一个环形数组的概念。

    ```go
    // runtime/chan.go
    type hchan struct {
    qcount int // 对应len(ch)
    dataqsiz int // 对应cap(ch)
    buf unsafe.Point // 指向缓冲区
    elemsize int // 数据类型
    isClosed bool // 是否关闭
    // ...
    lock sync.Mutex // 互斥锁确保同一时刻只有一个goroutine正在访问channel
    }

构造器

写流程

读流程

阻塞与非阻塞模式

两种读协议

sync.Map

小徐先生:https://mp.weixin.qq.com/s/nMuCMA8ONnhs1lsTVMcNgA

前言

核心数据结构

读流程

写流程

删流程

遍历

GMP

这部分是跟着 刘丹冰aceId 学的,b站有视频,也有他自己出版的书。

原版文档可以看 github

下面是一个思路概括

四个维度:

  • Golang调度器和 GMP思想 的由来
    1. 早期单进程os
    2. 多进程/多线程os
    3. 进入协程co-routine
    4. Go语言的协程goroutine
    5. 旧的Golang调度器
  • Golang调度器 和 GMP 的设计思想
    • GMP模型组成部分
    • P、M的数量?
    • P、M何时创建?
    • 调度器的设计策略(四个特点)
    • 执行 go func() 背后发生了什么?
    • 调度器的生命周期(M0、G0)
  • 常见 Golang调度场景过程
    1. G1创建G2(嵌套G)
    2. G1执行完毕
    3. G2创建过多的G

Go
https://becks723.github.io/2026/04/23/Go/
作者
Becks723
发布于
2026年4月23日
许可协议