Go

Go #

更多教程详见:

基础 #

接口 #

  • 如果一个类型使用 value receiver 实现了某个 interface 的所有方法,那么这个类型的 value 和 pointer 都实现了该 interface
  • 如果一个类型使用 pointer receiver 实现了某个 interface 的所有方法,那么这个类型只有 pointer 都实现了该 interface

参考:

标准库 #

为什么需要 response.Body.Close()#

为了 TCP 连接复用

// The default HTTP client's Transport does not
// attempt to reuse HTTP/1.0 or HTTP/1.1 TCP connections
// (keep-alive) unless the Body is read to completion and is
// closed.

读取 body 并关闭,才会复用 TCP 连接

for _, route := range routes {
    res, err := Request(route.method, ts.URL+route.path)
    if err != nil {
        panic(err)
    }
    io.Copy(ioutil.Discard, res.Body)
    res.Body.Close()
}

参考:


你现在使用的是什么版本?最新版本是什么,相对有哪些变化? #

go 版本管理工具,gvm 如何使用? #


简单说说 Go 包管理工具的发展历史? #

  • Go 1.4 及之前
    • 所有的依赖包都是存放在 GOPATH 下,没有版本控制
  • Go 1.5Go 1.10
    • 每个项目的根目录下可以有一个 vendor 目录,里面存放了该项目的依赖的包
  • Go 1.11Go 1.12
    • 默认使用的还是 GOPATH 的管理方式
    • 运行 export GO111MODULE=on,使用 Go Modules
  • Go 1.13 及之后
    • 默认使用 Go Modules

你用过哪些 Go 包管理工具,说说它们的优缺点? #

Go Mod 相对之前的版本管理有哪些优点? #

  • 可以指定版本

Go Mod 如何找到引用的包? #

一般情况:

查看 $GOPATH/pkg/mod/

设置 go mod vendor,使用 go build -mod=vendor 来构建项目时:



进阶 #

make, new 有什么区别? #

  • make
    • 初始化
    • 设置数组的长度、容量等
    • 返回变量本身
  • new
    • 只初始化
    • 返回变量的指针
list := new([]int)
// 不能对未设置长度的指针执行 append 操作
list = append(list, 1)

s1 := []int{1, 2, 3}
s2 := []int{4, 5}
// 编译错误,s2需要展开
// s1 = append(s1, s2)
s1 = append(s1, s2...)
fmt.Println(s1)

// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
//	Slice: The size specifies the length. The capacity of the slice is
//	equal to its length. A second integer argument may be provided to
//	specify a different capacity; it must be no smaller than the
//	length. For example, make([]int, 0, 10) allocates an underlying array
//	of size 10 and returns a slice of length 0 and capacity 10 that is
//	backed by this underlying array.
//	Map: An empty map is allocated with enough space to hold the
//	specified number of elements. The size may be omitted, in which case
//	a small starting size is allocated.
//	Channel: The channel's buffer is initialized with the specified
//	buffer capacity. If zero, or the size is omitted, the channel is
//	unbuffered.
func make(t Type, size ...IntegerType) Type

// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type

panic, recover 是怎么实现的? #

参考:


Makefile 语法 #


Go 插件系统 #

Go 1.8 版本开始提供了一个创建共享库的新工具,称为 Plugins.

go build -buildmode=plugin

Go 插件系统的应用场景? #

  • 通过 plugin 我们可以很方便的对于不同功能加载相应的模块;
  • 针对不同语言 (英文、汉语、德语……) 加载不同的语言 so 文件,进行不同的输出;
  • 编译出的文件给不同的编程语言用 (如:c/java/python/lua 等).
  • 需要加密的核心算法,核心业务逻辑可以可以编译成 plugin 插件
  • 黑客预留的后门 backdoor 可以使用 plugin
  • 函数集动态加载

Go 插件系统是如何实现的? #

参考:


Goroutine 为什么高效? #


Goroutine 如何调度? #

Golang 调度器引入了三个结构来对调度的过程建模:

  • G 代表一个 Goroutine;
  • M 代表一个操作系统的线程Machine
  • P 代表一个 CPU 处理器,通常 P 的数量等于 CPU 核数(GOMAXPROCS)。

三者都在 runtime2.go 中定义,他们之间的关系如下:

  • G 需要绑定在 M 上才能运行;
  • M 需要绑定 P 才能运行;
  • 程序中的多个 M 并不会同时都处于执行状态,最多只有 GOMAXPROCS 个 M 在执行。

早期版本的 Golang 是没有 P 的,调度是由 G 与 M 完成。 这样的问题在于每当创建、终止 Goroutine 或者需要调度时,需要一个全局锁来保护调度的相关对象 (sched)。 全局锁严重影响 Goroutine 的并发性能。 (Scalable Go Scheduler)

通过引入 P,实现了一种叫做 work-stealing 的调度算法:

  • 每个 P 维护一个 G 队列;
  • 当一个 G 被创建出来,或者变为可执行状态时,就把他放到 P 的可执行队列中;
  • 当一个 G 执行结束时,P 会从队列中把该 G 取出;如果此时 P 的队列为空,即没有其他 G 可以执行, 就随机选择另外一个 P,从其可执行的 G 队列中偷取一半。

该算法避免了在 Goroutine 调度时使用全局锁。

  • 抢占
    • 在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死
    • 在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程
  • 全局 G 队列
    • 在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。

为什么 Go 需要自己实现调度器? #

  • Goroutine 的引入是为了方便高并发程序的编写。 一个 Goroutine 在进行阻塞操作(比如系统调用)时,会把当前线程中的其他 Goroutine 移交到其他线程中继续执行, 从而避免了整个程序的阻塞。
  • 由于 Golang 引入了垃圾回收(gc),在执行 gc 时就要求 Goroutine 是停止的。通过自己实现调度器,就可以方便的实现该功能。 通过多个 Goroutine 来实现并发程序,既有异步 IO 的优势,又具有多线程、多进程编写程序的便利性。
  • 引入 Goroutine,也意味着引入了极大的复杂性。一个 Goroutine 既要包含要执行的代码, 又要包含用于执行该代码的栈和 PC、SP 指针。

调度器解决了什么问题? #

  1. 栈管理

    既然每个 Goroutine 都有自己的栈,那么在创建 Goroutine 时,就要同时创建对应的栈。 Goroutine 在执行时,栈空间会不停增长。 栈通常是连续增长的,由于每个进程中的各个线程共享虚拟内存空间,当有多个线程时,就需要为每个线程分配不同起始地址的栈。 这就需要在分配栈之前先预估每个线程栈的大小。如果线程数量非常多,就很容易栈溢出。

    为了解决这个问题,就有了 Split Stacks 技术: 创建栈时,只分配一块比较小的内存,如果进行某次函数调用导致栈空间不足时,就会在其他地方分配一块新的栈空间。 新的空间不需要和老的栈空间连续。函数调用的参数会拷贝到新的栈空间中,接下来的函数执行都在新栈空间中进行。

    Golang 的栈管理方式与此类似,但是为了更高的效率,使用了连续栈 (Golang 连续栈) 实现方式也是先分配一块固定大小的栈,在栈空间不足时,分配一块更大的栈,并把旧的栈全部拷贝到新栈中。 这样避免了 Split Stacks 方法可能导致的频繁内存分配和释放。

  2. 抢占式调度

    Goroutine 的执行是可以被抢占的。如果一个 Goroutine 一直占用 CPU,长时间没有被调度过, 就会被 runtime 抢占掉,把 CPU 时间交给其他 Goroutine。



设计与实现 #


本文访问量

本站总访问量

本站总访客数