go 学习笔记

本文初始化于 2019-11-11 02:12:12,深夜开启学习 golang 的道路。

更新历史

2021 年 06 月 26 日 - 更新 go 语言中文网
2021 年 06 月 20 日 - 如今是 go 1.16 了, go module 是主流
2021 年 06 月 19 日 - 更新 库源码文件

reference

go 是静态类型的语言,入门推荐Go 编程语言指南
还有一个极客时间的课程示例代码

社区推荐:go 语言中文网

教学视频:只推荐目前比较新的 go1.14: 8小时转职Golang工程师
入门 Go语言学习之路/Go语言教程

项目

小知识

  1. runtime
  2. Go并发编程-runtime包
  3. Go 语言 50 个技巧
  4. 并发与并行
  5. The Top 10 Most Common Mistakes I’ve Seen in Go Projects

基础知识

工作区和GOPATH

GOPATH 已经被弃用,现在官方推荐 Go Modules , 但因为有的老项目没有启用 go modules , 所以还是有必要学习一下历史的行程

  • 煎鱼 Go Modules指南
  • 因为 GOPATH 模式下没有版本控制的概念, 所以被弃用
  • Go Modules 下,GOPATH 还是存在,新建的项目可以在 $GOPATH 外任意地方。
  • Go Modules 优势: 一是依赖包的版本控制,而是集中管理包,go mod download 下载的包统一存放在module cache, located in GOPATH/pkg/mod.

GOROOT (GOPATH 已过时)

  1. GOROOT:Go 语言安装根目录的路径,也就是 GO 语言的安装路径。
  2. GOPATH:若干工作区目录的路径。是我们自己定义的工作空间。 官方解释
  3. GOBIN:GO 程序生成的可执行文件(executable file)的路径。


注意,环境变量 GOPATH 中包含的路径不能与环境变量 GOROOT 的值重复。

1
2
3
4
5
➜  go_entrytask git:(master) ✗ env | grep GO  
GOPROXY=direct
GOPATH=/Users/yang.fei/go
GOROOT=/usr/local/Cellar/go/1.16/libexec
GO111MODULE=on

面试问题是:你知道设置 GOPATH 有什么意义吗?

你可以把 GOPATH 简单理解成 Go 语言的工作目录,它的值是一个目录的路径,也可以是多个目录路径,每个目录都代表 Go 语言的一个工作区(workspace)。我们需要利于这些工作区,去放置 Go 语言的源码文件(source file),以及安装(install)后的归档文件(archive file,也就是以“.a”为扩展名的文件)和可执行文件(executable file)。

常用命令

go build

  • go build 在运行go build命令的时候,默认不会编译目标代码包所依赖的那些代码包。如果要强制编译它们,可以在执行命令的时候加入标记-a。此时,不但目标代码包总是会被编译,它依赖的代码包也总会被编译,即使依赖的是标准库中的代码包也是如此。另外,如果不但要编译依赖的代码包,还要安装它们的归档文件,那么可以加入标记-i。
    那么我们怎么确定哪些代码包被编译了呢?有两种方法。
  1. 运行go build命令时加入标记-x,这样可以看到go build命令具体都执行了哪些操作。另外也可以加入标记-n,这样可以只查看具体操作而不执行它们。
  2. 运行go build命令时加入标记-v,这样可以看到go build命令编译的代码包的名称。它在与-a标记搭配使用时很有用。

go get

go get 命令go get会自动从一些主流公用代码仓库(比如 GitHub)下载目标代码包,并把它们安装到环境变量GOPATH包含的第 1 工作区的相应目录中。如果存在环境变量GOBIN,那么仅包含命令源码文件的代码包会被安装到GOBIN指向的那个目录。
最常用的几个标记有下面几种。

  • -u:下载并安装代码包,不论工作区中是否已存在它们。
  • -d:只下载代码包,不安装代码包。
  • -fix:在下载代码包后先运行一个用于根据当前 Go 语言版本修正代码的工具,然后再安装代码包。
  • -t:同时下载测试所需的代码包。
  • -insecure:允许通过非安全的网络协议下载和安装代码包。HTTP 就是这样的协议。

Go 语言官方提供的go get命令是比较基础的,其中并没有提供依赖管理的功能。对代码包的远程导入路径进行自定义的方法是:在该代码包中的库源码文件的包声明语句的右边加入导入注释,像这样:

1
package semaphore // import "golang.org/x/sync/semaphore"

这个代码包原本的完整导入路径是github.com/golang/sync/semaphore。这与实际存储它的网络地址对应的。该代码包的源码实际存在 GitHub 网站的 golang 组的 sync 代码仓库的 semaphore 目录下。而加入导入注释之后,用以下命令即可下载并安装该代码包了:
1
go get golang.org/x/sync/semaphore

而 Go 语言官网 golang.org 下的路径 /x/sync/semaphore 并不是存放semaphore包的真实地址。我们称之为代码包的自定义导入路径。不过,这还需要在 golang.org 这个域名背后的服务端程序上,添加一些支持才能使这条命令成功。
关于自定义代码包导入路径的完整说明可以参看这里

go install

go install : 我们在构建或者安装这个代码包的时候,提供给go命令的路径应该是目录的相对路径,就像这样:

1
go install puzzlers/article3/q2/lib

安装后的目录如下所示
1
2
3
4
5
6
7
8
9
➜  Golang_Puzzlers git:(master) ✗ tree pkg 
pkg
`-- darwin_amd64
`-- puzzlers
`-- article3
`-- q2
`-- lib.a

4 directories, 1 file

get vs install

What is the difference between go get and go install?
go get does two main things in this order:

  • downloads and saves in $GOPATH/src/<import-path> the packages (source code) named in the import paths, along with their dependencies, then
  • executes a go install

go get -d 只下载不安装。 那面 go install 存在的意义,是当需要测试本地的 packages 时,就直接安装

Go 1.16 has updated and clarified the usage of go install and go get: https://tip.golang.org/doc/go1.16#modules

  • go install, with or without a version suffix (as described above), is now the recommended way to build and install packages in module mode.
  • go get should be used with the -d flag to adjust the current module’s dependencies without building packages, and use of go get to build and install packages is deprecated. In a future release, the -d flag will always be enabled.

命令源码文件

其实就是你自己写的代码

命令源码文件是程序的运行入口,是每个可独立运行的程序必须拥有的。我们可以通过构建或安装,生成与其对应的可执行文件,后者一般会与该命令源码文件的直接父目录同名。
如果一个源码文件声明属于main包,并且包含一个 无参数声明 且无结果声明 的main函数,那么它就是命令源码文件。
在同一个目录下的源码文件都需要被声明为属于同一个代码包。源码文件声明的包名可以与其所在目录的名称不同,只要这些文件声明的包名一致就可以。

库源码文件

类似 python 的包

库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话)。
第一条规则,同目录下的源码文件的代码包声明语句要一致。也就是说,它们要同属于一个代码包。这对于所有源码文件都是适用的。如果目录中有命令源码文件,那么其他种类的源码文件也应该声明属于main包。这也是我们能够成功构建和运行它们的前提。

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
➜  q1 git:(master) ✗ cat demo4.go
package main

import (
"flag"
)

var name string

func init() {
flag.StringVar(&name, "name", "everyone", "The greeting object.")
}

func main() {
flag.Parse()
hello(name)
}

// go run demo4.go demo4_lib.go


➜ q1 git:(master) ✗ cat demo4_lib.go
package main

import "fmt"

func hello(name string) {
fmt.Printf("Hello, %s!\n", name)
}

➜ q1 git:(master) ✗ go run demo4.go demo4_lib.go
Hello, everyone!

第二条规则,源码文件声明的代码包的名称可以与其所在的目录的名称不同。在针对代码包进行构建时,生成的结果文件的主名称与其父目录的名称一致。对于命令源码文件而言,构建生成的可执行文件的主名称会与其父目录的名称相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  q2 git:(master) ✗ cat demo5.go                                                                                                
package main

import (
"flag"
"puzzlers/article3/q2/lib"
)

var name string

func init() {
flag.StringVar(&name, "name", "everyone", "The greeting object.")
}

func main() {
flag.Parse()
lib.Hello(name)
}
  • 名称的首字母为大写的程序实体才可以被当前包外的代码引用,否则它就只能被当前包内的其他代码引用。
  • 通过名称,Go 语言自然地把程序实体的访问权限划分为了包级私有的和公开的。对于包级私有的程序实体,即使你导入了它所在的代码包也无法引用到它。
1
2
3
4
5
6
7
8
➜  q2 git:(master) ✗ cat lib/demo5_lib.go                                                                                                                                                                            
package lib

import "fmt"

func Hello(name string) {
fmt.Printf("Hello, %s!\n", name)
}

在 Go 1.5 及后续版本中,我们可以通过创建internal代码包让一些程序实体仅仅能被当前模块中的其他代码引用。这被称为 Go 程序实体的第三种访问权限:模块级私有。具体规则是,internal代码包中声明的公开程序实体仅能被该代码包的直接父包及其子包中的代码引用。当然,引用前需要先导入这个internal包。对于其他代码包,导入该internal包都是非法的,无法通过编译。

1
2
3
4
5
6
7
8
9
➜  q4 git:(master) ✗ tree .
.
|-- demo6.go
`-- lib
|-- demo6_lib.go
`-- internal
`-- internal.go

2 directories, 3 files

demo6.go 只能调用 demo6_lib.go , 而 demo6_lib.go 能调用 internal.go 。但是 demo6.go 不能直接调用 internal.go, 因为不是直接父级

Go Modues (推荐)

下图是实际的 go module 结构

1
2
3
4
5
6
7
8
9
10
11
12
go mod init      #初始化当前目录为模块根目录,生成go.mod, go.sum文件
go mod download #下载依赖包
go mod tidy #整理检查依赖,如果缺失包会下载或者引用的不需要的包会删除
go mod vendor #复制依赖到vendor目录下面
go mod #可看完整所有的命令
go mod graph #以文本模式打印模块需求图
go mod verify #验证依赖是否正确
go mod edit #编辑go.mod文件

go list -m all #显示依赖关系。
go list -m -json all #显示详细依赖关系。
go list -m -versions <path> #显示包有哪些已发布版本

go.mod

go.mod 提供了 module, requirereplaceexclude 四个命令

  • module 语句指定包的名字(路径)
  • require 语句指定的依赖项模块
  • replace 语句可以替换依赖项模块
  • exclude 语句可以忽略依赖项模块

go.sum

go.sum 的每一行都是一个条目,大致是这样的格式:

1
2
3
4
5
<module> <version>/go.mod <hash>

// 或者
<module> <version> <hash>
<module> <version>/go.mod <hash>

备注:其中module是依赖的路径,version是依赖的版本号。hash是以h1:开头的字符串,表示生成checksum的算法是第一版的hash算法(sha256)

  • 项目没有打 tag,会生成一个版本号,格式如下:v0.0.0-commit日期-commitID 引用一个项目的特定分支,比如 develop branch,也会生成类似的版本号:v当前版本+1-commit日期-commitID
  • 项目有用到 go module,那么就是正常地用 tag 来作为版本号。如果项目打了 tag,但是没有用到 go module,为了跟用了 go module 的项目相区别,需要加个 + incompatible 的标志。比如: ++incompatible/go.mod+
  • 对于使用了 v2+ go module 的项目,项目路径会有个版本号的后缀。比如: <module/v2>+ +

Migration

Migrating to Go Modules

语法

Go 三个点(…)用法

数组 array

数组是连续存储在内存中的,每一个切片的底层实现都是绑定着一个数组。当切面的值改变时,底层数组也会跟着改变。当切片的 capacity 增大超过当前数组长度时,go 会自动产生一个新的底层数组,长度为以前的 2 倍,然后再绑定到切片上。当切片 capacity 大于 1000 时,底层数组的增长因子就会有 2 变为 1.25。数组的容量永远等于其长度,都是不可变的。
数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。

切片 slice

Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。
注意,Go 语言里不存在像 Java 等编程语言中令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。
如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。
我们在数组和切片之上都可以应用索引表达式,得到的都会是某个元素。我们在它们之上也都可以应用切片表达式,也都会得到一个新的切片。切片的容量却不是这样,并且它的变化是有规律可寻的。用make函数初始化切片时,如果不指明其容量,那么它就会和长度一致。如果在初始化时指明了容量,那么切片的实际容量也就是它了。 切片代表的窗口是无法向左扩展的。
slice := make([]int, len, cap)

字典的操作和约束

Go 语言的字典类型其实是一个哈希表(hash table)的特定实现,在这个实现中,键和元素的最大不同在于,键的类型是受限的,而元素却可以是任意类型的。

  • Go 语言字典的键类型不可以是函数类型、字典类型和切片类型。
  • Go 语言规范规定,在键类型的值之间必须可以施加操作符==和!=。换句话说,键类型的值必须要支持判等操作。由于函数类型、字典类型和切片类型的值并不支持判等操作,所以字典的键类型不能是这些类型。
  • 在那些基本类型中应该优先选择哪一个?答案是,优先选用数值类型和指针类型,通常情况下类型的宽度越小越好。如果非要选择字符串类型的话,最好对键值的长度进行额外的约束。

通道的基本操作

一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符<-。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。

  • 问题:单向通道有什么应用价值?概括地说,单向通道最主要的用途就是约束其他代码的行为。我们可以使用带range子句的for语句从通道中获取数据,也可以通过select语句操纵通道。
  • select语句与通道怎样联用,应该注意些什么? select语句是专门为通道而设计的,它可以包含若干个候选分支,每个分支中的case表达式都会包含针对某个通道的发送或接收操作。当select语句被执行时,它会根据一套分支选择规则选中某一个分支并执行其中的代码。如果所有的候选分支都没有被选中,那么默认分支(如果有的话)就会被执行。注意,发送和接收操作的阻塞是分支选择规则的一个很重要的依据。

使用函数的正确姿势

高阶函数可以满足下面的两个条件:1. 接受其他的函数作为参数传入;
关于函数传参的一个注意事项: 既不要把你程序的细节暴露给外界,也尽量不要让外界的变动影响到你的程序。你可以想想这个原则在这里可以起到怎样的指导作用。

结构体 struct

Go语言结构体内存布局
一个结构体类型可以包含若干个字段,每个字段通常都需要有确切的名字和类型。

接口类型的合理运用

接口类型与其他数据类型不同,它是没法被实例化的。接口类型声明中的这些方法所代表的就是该接口的方法集合。一个接口的方法集合就是它的全部特征。
对于任何数据类型,只要它的方法集合中完全包含了一个接口的全部特征(即全部的方法),那么它就一定是这个接口的实现类型。

关于指针的有限操作

  • 常量的值。
  • 基本类型值的字面量。
  • 算术操作的结果值。
  • 对各种字面量的索引表达式和切片表达式的结果值。不过有一个例外,对切片字面量的索引结果值却是可寻址的。
  • 对字符串变量的索引表达式和切片表达式的结果值。
  • 对字典变量的索引表达式的结果值。
  • 函数字面量和方法字面量,以及对它们的调用表达式的结果值。
  • 结构体字面量的字段值,也就是对结构体字面量的选择表达式的结果值。
  • 类型转换表达式的结果值。
  • 类型断言表达式的结果值。
  • 接收表达式的结果值。

go语句及其执行规则

一个进程至少会包含一个线程。如果一个进程只包含了一个线程,那么它里面的所有代码都只会被串行地执行。每个进程的第一个线程都会随着该进程的启动而被创建,它们可以被称为其所属进程的主线程。
相对应的,如果一个进程中包含了多个线程,那么其中的代码就可以被并发地执行。除了进程的第一个线程之外,其他的线程都是由进程中已存在的线程创建出来的。
Go 语言不但有着独特的并发编程模型,以及用户级线程 goroutine,还拥有强大的用于调度 goroutine、对接系统级线程的调度器。
G(goroutine 的缩写)、P(processor 的缩写)和 M(machine 的缩写)。

  • 什么是主 goroutine,它与我们启用的其他 goroutine 有什么不同? 面试中经常提问的编程题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package main

    import "fmt"

    func main() {
    for i := 0; i < 10; i++ {
    go func() {
    fmt.Println(i)
    }()
    }
    }
    与一个进程总会有一个主线程类似,每一个独立的 Go 程序在运行时也总会有一个主 goroutine。已存在的 goroutine 总是会被优先复用。
    严谨地讲,Go 语言并不会去保证这些 goroutine 会以怎样的顺序运行。由于主 goroutine 会与我们手动启用的其他 goroutine 一起接受调度,又因为调度器很可能会在 goroutine 中的代码只执行了一部分的时候暂停,以期所有的 goroutine 有更公平的运行机会。
    一旦主 goroutine 中的代码(也就是main函数中的那些代码)执行完毕,当前的 Go 程序就会结束运行。如此一来,如果在 Go 程序结束的那一刻,还有 goroutine 未得到运行机会,那么它们就真的没有运行机会了,它们中的代码也就不会被执行了。
  • 怎样才能让主 goroutine 等待其他 goroutine?
    最简单粗暴的办法就是让主 goroutine“小睡”一会儿。
    1
    2
    3
    4
    5
    6
    for i := 0; i < 10; i++ {
    go func() {
    fmt.Println(i)
    }()
    }
    time.Sleep(time.Millisecond * 500)
    你可能会想到,既然不容易预估时间,那我们就让其他的 goroutine 在运行完毕的时候告诉我们好了。这个思路很好,但怎么做呢?你是否想到了通道呢?我们先创建一个通道,它的长度应该与我们手动启用的 goroutine 的数量一致。在每个手动启用的 goroutine 即将运行完毕的时候,我们都要向该通道发送一个值。
    如果你知道标准库中的代码包sync的话,那么可能会想到sync.WaitGroup类型