Go 记录

ipsum-0320 于 2024-03-13 发布

1.go 的包引入机制

go 的 import 会将整个路径下的东西都引入进来,并且可以取别名:

import (
	apiserver "k8s.io/apiserver/pkg/server"
)

如此一来,server 目录就被命名为了 apiserver。

go 代码中可以直接引用目录,并引用目录下的变量或者结构体;默认情况下,引用名称是最末级的目录。

2.go 中的变量作用阈

Go 中的变量可以在三个地方声明:

  1. 函数内定义的变量称为局部变量。它们的作用域只在函数体内,参数和返回值变量也是局部变量。
  2. 函数外定义的变量称为全局变量。全局变量可以在整个包甚至外部包(被导出后)使用。
  3. 函数定义中的变量称为形式参数。形式参数会作为函数的局部变量来使用。

在 Go 中,全局变量指的是在包的最顶层声明的字母大写的导出变量,这样这个变量在整个 Go 程序的任何角落都可以被访问和修改。

全局变量的生命周期与程序的整个生命周期等同。

3.interface{} 是什么类型?

interface{} 是一个空的interface 类型,一个类型如果实现了一个interface 的所有方法就说该类型实现了这个interface,空的interface 没有方法,所以可以认为所有的类型都实现了 interface{} 。

4.defer 的作用

defer 关键字用于推迟(延迟)一个函数调用,使得这个函数会在当前函数返回之前执行。

5.Go 中的上下文机制

1.Context 是干什么的

在 Go 语言中,Context 是一个非常重要的概念,它用于在不同的 goroutine 之间传递请求域的相关数据,并且可以用来控制 goroutine 的生命周期和取消操作。

Context 本质上是一个接口,其有如下方法待实现:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}

Context 具体有什么应用场景呢?

Go 的协程写起来是非常轻松的,并且在协程里面还能继续开协程,颇有一种一生二,二生三,三生万物的感觉了。这么多协程,协程与协程之间的通信如何解决呢?这个协程我不想要了,我如何关闭它?

那么context来了,就是解决如何关闭协程这个问题的。除了关闭协程,他还能用于传输数据,做到协程与协程之间的桥梁,所以我们叫他上下文。

参考文章:https://www.fengfengzhidao.com/article/WdlGxI0BEG4v2tWkq3bD。

2.有哪些作用?

context.Background() 会返回一个基础上下文。

  1. 数据传递。
    • 使用 context.WithValue 创建上下文。
    • 使用 ctx.Value 获取数据。
  2. 取消协程。
    • 使用 context.WithCancel 创建上下文。
    • 使用 cancel 终止上下文。

      关于这一点可以参考文章:https://golang.design/go-questions/stdlib/context/cancel/。

      本质上关闭一个 channel,从而让监听这个 channel 的协程做出相应的取消动作。

  3. 截止时间。
    • 使用 context.WithDeadline 创建上下文。
    • 设置截止时间。
  4. 超时时间。
    • 使用 context.WithTimeout 创建上下文。
    • 设置超时时间。

3.官方的使用建议

  1. 不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。
  2. 不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。
  3. 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
  4. 同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。

6.Go 中的 channel 和协程(goroutine)

1.goroutine

对于进程、线程,都是有内核进行调度,有 CPU 时间片的概念,进行抢占式调度(有多种调度算法)。

对于协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程,通常只能进行协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。

本质上,goroutine 就是协程。不同的是,Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的CPU (P) 转让出去,让其他 goroutine 能被调度并执行,也就是 Golang 从语言层面支持了协程。Golang 的一大特色就是从语言层面原生支持协程,在函数或者方法前面加 go关键字就可创建一个协程。

协程在应用层(Go runtime)实现线程切换的逻辑。协程并不是由操作系统调度的,而且应用程序也没有能力和权限执行 cpu 调度。

2.channel

channel 是一个数据管道,可以往里面写数据,同时也能从其中读数据。channel 是 goroutine 之间数据通信桥梁,而且是线程安全的。channel 遵循先进先出原则,写入,读出数据都会加锁。

channel 可以分为三种类型:

  1. 只读 channel,单向 channel。
  2. 只写 channel,单向 channel。
  3. 可读可写 channel。

channel 还可按是否带有缓冲区分为:

操作 channel 时,如果当前不能够读取(譬如没有数据),那么就阻塞;如果当前不能写入(例如缓冲区空间不够),那么就阻塞

当发送者要告诉接收者没有值发送给 channel 了,这时就需要关闭 channel 了,并且只能由发送者关闭,不然接受者关闭的话,发送者继续发送会报错误的。

for range 语法是可以应用到 channel 上的。

ch := make(chan int, 3)

// 向通道发送数据
go func() {
    defer close(ch)
    ch <- 1
    ch <- 2
    ch <- 3
}()

// 通过 for range 迭代通道中的值
for value := range ch {
    fmt.Println("Received:", value)
}

参考文章:https://www.cnblogs.com/jiujuan/p/16014608.html。

channel 可以作为协程之间通信的管道。

没有缓冲区的 channel 可以作为同步数据的管道,起到同步数据的作用(或者说阻塞协程的作用)。

注意:同步的 channel 不要在同一个 goroutine 协程里发送和接收数据,会导致deadlock死锁。

7.Go 的值传递和引用传递

参考文章:https://juejin.cn/post/7221730647043244090

Go 和 Java 一样,只有值传递,没有引用传递。

首先弄明白什么是值传递和引用传递:

  1. 值传递指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
  2. 引用传递指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

Go 语言中的值类型和引用类型:

在 Go 语言中:

  1. 引用类型作为参数时,称为浅拷贝,形参改变,实参数跟随变化。因为传递的是地址,形参和实参都指向同一块地址。
  2. 值类型作为参数时,称为深拷贝,形参改变,实参不变,因为传递的是值的副本,形参会新开辟一块空间,与实参指向不同。
  3. 如果希望值类型数据在修改形参时实参跟随变化,可以把参数设置为指针类型

8.Go 中的 make 和 new

参考文章:https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-make-and-new/

Go 中对于线性表这样的数据结构,存在 slice 和数组两种实现,其中数组是定长的,不能出现扩容的,而 slice 是变长的,如果元素过多则会扩容。

在 Go 语言中,一般会使用 make 创建 slice、hash 和 ch,如下所示:

slice := make([]int, 0, 100) // 创建 slice,长度是 0, 容量是 100。
hash := make(map[int]bool, 10) // 创建 map,容量是 10。
ch := make(chan int, 5) // 创建一个 channel,缓冲区长度是 5。

相比于复杂的 make 关键字,new 的功能就比较简单一些了,它只能接收类型作为参数然后返回一个指向该类型的指针(这个指针指向堆上的一片内存空间,大小为接收类型的大小)。

典型的 new 的使用实例如下:

// 创建一个整数指针
pInt := new(int)
// 创建一个浮点数指针
pFloat64 := new(float64)
// 创建一个字符串指针
pString := new(string)
// 创建一个整数类型的指针的指针
ppInt := new(*int)

// 自定义结构体
type MyStruct struct {
    Name string
    Age  int
}
// 创建 MyStruct 类型的指针
pMyStruct := new(MyStruct)
// 创建一个 MyStruct 类型的指针的指针
ppMyStruct := new(*MyStruct)

9.Go 中的 select

select 语句使一个协程可以等待多个通信操作。select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

select 中的其它分支都没有准备好时default 分支就会执行。为了在尝试发送或者接收时不发生阻塞,可使用 default 分支。

select 一般会被包裹到一个 for 循环语句中。

10.Go 中的闭包

参考文章:https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.6.html。

在 Go 中,闭包是由函数及其相关引用环境组合而成的实体。即:

闭包 = 函数 + 引用环境

下面有一段例子:

package main

import "fmt"

func intSeq() func() int {

    i := 0
    return func() int {
        i++
        return i
    }
}

func main() {

    nextInt := intSeq()

    fmt.Println(nextInt())
    fmt.Println(nextInt())

    nextInt2 := intSeq()
    fmt.Println(nextInt2())
}

我们有一个整数序列intSeq函数,它生成一个整数序列。它就返回一个包含递增i变量的闭包。函数中定义的变量i具有局部函数作用域,但是,在这种情况下,即使 intSeq函数执行完成后,闭包仍会绑定到变量i上。

nextInt := intSeq()

我们调用intSeq函数。它返回一个函数,该函数将增加一个计数器。返回的函数关闭了变量 i以形成闭包,并绑定到 nextInt变量上。

关闭是什么意思呢?可以这么理解:我们知道函数的局部变量是在栈上分配的,而闭包上的变量会逃逸到堆上(是由编译器的一种叫escape analyze的技术实现的)。

fmt.Println(nextInt())
fmt.Println(nextInt())

连续调用两次nexInt,因为都是在同一环境下,所以变量i会累加。我们再次调用闭包(生成了一个新的闭包)。

nextInt2 := intSeq()
fmt.Println(nextInt2())

函数的下一次调用intSeq返回一个新的闭包。这个新的闭包有自己独特的状态,所以变量i重置了。

函数是同一个函数,但是环境确实引用不同的环境。

返回如下所示:

$ go run closure.go
1
2
1

1.escape analyze

在继续研究闭包的实现之前,先看一看Go的一个语言特性:

func f() *Cursor {
    var c Cursor
    c.X = 500
    noinline()
    return &c
}

Cursor是一个结构体,这种写法在C语言中是不允许的,因为变量c是在栈上分配的,当函数f返回后c的空间就失效了。但是,在Go语言规范中有说明,这种写法在Go语言中合法的。语言会自动地识别出这种情况并在堆上分配c的内存,而不是函数f的栈上

识别出变量需要在堆上分配,是由编译器的一种叫escape analyze的技术实现的。escape analyze可以分析出变量的作用范围,这是对垃圾回收很重要的一项技术。

2.闭包结构体

回到闭包的实现来,前面说过,闭包是函数和它所引用的环境。那么是不是可以表示为一个结构体呢:

type Closure struct {
    F func()
    i *int
}

事实上,Go 在底层确实就是这样表示一个闭包的,这也就解释了为什么新的闭包中的值会和原有闭包中的值不一致了。

11.Go 中结构体的方法声明

对于以下代码:

func (d *descheduler) runDeschedulerLoop(ctx context.Context, nodes []*v1.Node) error{
    // ...
}

这是一个结构体方法的声明,其和普通函数的主要区别就是其在方法名前面有一个方法所属的结构体。