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.5
至Go 1.10
- 每个项目的根目录下可以有一个
vendor
目录,里面存放了该项目的依赖的包
- 每个项目的根目录下可以有一个
Go 1.11
至Go 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 才执行下一个协程
- 在 Go 中,一个
- 全局 G 队列
- 在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。
为什么 Go 需要自己实现调度器? #
- Goroutine 的引入是为了方便高并发程序的编写。 一个 Goroutine 在进行阻塞操作(比如系统调用)时,会把当前线程中的其他 Goroutine 移交到其他线程中继续执行, 从而避免了整个程序的阻塞。
- 由于 Golang 引入了垃圾回收(gc),在执行 gc 时就要求 Goroutine 是停止的。通过自己实现调度器,就可以方便的实现该功能。 通过多个 Goroutine 来实现并发程序,既有异步 IO 的优势,又具有多线程、多进程编写程序的便利性。
- 引入 Goroutine,也意味着引入了极大的复杂性。一个 Goroutine 既要包含要执行的代码, 又要包含用于执行该代码的栈和 PC、SP 指针。
调度器解决了什么问题? #
-
栈管理
既然每个 Goroutine 都有自己的栈,那么在创建 Goroutine 时,就要同时创建对应的栈。 Goroutine 在执行时,栈空间会不停增长。 栈通常是连续增长的,由于每个进程中的各个线程共享虚拟内存空间,当有多个线程时,就需要为每个线程分配不同起始地址的栈。 这就需要在分配栈之前先预估每个线程栈的大小。如果线程数量非常多,就很容易栈溢出。
为了解决这个问题,就有了 Split Stacks 技术: 创建栈时,只分配一块比较小的内存,如果进行某次函数调用导致栈空间不足时,就会在其他地方分配一块新的栈空间。 新的空间不需要和老的栈空间连续。函数调用的参数会拷贝到新的栈空间中,接下来的函数执行都在新栈空间中进行。
Golang 的栈管理方式与此类似,但是为了更高的效率,使用了连续栈 (Golang 连续栈) 实现方式也是先分配一块固定大小的栈,在栈空间不足时,分配一块更大的栈,并把旧的栈全部拷贝到新栈中。 这样避免了 Split Stacks 方法可能导致的频繁内存分配和释放。
-
抢占式调度
Goroutine 的执行是可以被抢占的。如果一个 Goroutine 一直占用 CPU,长时间没有被调度过, 就会被 runtime 抢占掉,把 CPU 时间交给其他 Goroutine。
叶王 © 2013-2024 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。