Go 语言圣经学习笔记

Table of Contents

Go 语言圣经(中文版):https://legacy.gitbook.com/book/yar999/gopl-zh/details

1. 程序结构

1.1. 声明

Go 语言主要有四中声明语句:

  • var: 变量
  • const: 常量
  • type: 类型
  • func: 函数实体对象

1.2. 变量

:== 的区别:

  • := 是声明变量并赋值
  • = 是个单纯的赋值语句,如果声明变量需要加上 var 关键字

指针: 一个变量保存了变量对应类型值的内存空间。并不是每一个值都会有一个内存地址,但是对于每一个变量都必然有对应的内存地址。 & 表示取地址; * 表示解引用。

new 函数: 创建一个匿名变量、初始化之后,返回它的变量地址(注意和 C 语言的区别)。

Go 语言并不是通过使用 var 还是 new 声明变量的方式决定在堆上还是栈上分配变量,而是看实际的使用场景,如果一个变量的地址在 函数外继续使用了,则会分配在堆上(Go 语言术语称之为“逃逸”)。

1.3. 赋值

Go 中的递增和递减是「语句」而非表达式,并且只支持后置的方式。比如: v++ 是正确的, x = i++ 是错误的。

Go 支持向 Python 一样元组赋值。和 Python 一样, _ 用来丢弃不需要的值。

1.4. 包和文件

包中如果一个名字是大写字母开头的,那么该名字是导出的,否则在包外不可见。

使用 package 声明包,使用 import 导入包。

1.5. 作用域

不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是 指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。

任何在函数外部(也就是包级别作用域)声明的名字可以在同一个包的任何源文件中访问。

2. 基础数据类型

Go 语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。

2.1. 整型

int8, int16, int32, int64 和 uint8, uint16, uint32, uint64 。

int 和 uint 是根据 CPU 平台机器字大小的有符号和无符号整数。

Unicode 字符的 rune 类是和 int32 等价的类型,通常用于一个 Unicode 码点。

byte 和 uint8 是等价类型,byte 类型用于强调数值是一个原始的数据而不是一个整数。

最后,还有一种无符号的整数类型 uintptr,没有指定具体的 bit 大小但是足以容纳指针,在底层编程才需要。

2.2. 浮点数

float32, float64

math.MaxFloat32 表示 float32 表示的最大值,对应的 mach.MaxFloat64 表示 float64 最大值。

float32 能提供 6 个十进制数的精度,而 float64 则可以提供大约 15 个十进制的进度;通常应该优先使用 float64 类型。 float32 表达的最大值并不是很大,因为只有 23 有效 bit,其它的 bit 用户指数和符号,当整数大于 23 bit 表达的范围时,float32 的表示将出现误差。

2.3. 复数

complex64, complex128 分别对应 float32 和 float64 两种浮点数精度。

2.4. 布尔值

true, false

bool 不会隐式的转换为 0 或者 1 。

2.5. 字符串

一个字符串是一个不可改变的字节序列。索引操作返回第 i字节 的字节值,也就是说第 i 个字节并不是第 i 个字符, 因为对于非 ASCII 字符的 UTF8 编码需要两个或者多个字节。

子串操作 s[i:j] 是基于原始的字符串 [i,j) 左开右闭区间得到的一个新字符串。

字符串值是不可变的(字节序列),重新赋值操作会重新分配字符串值。也不可以修改字符串的值。

直接按照索引来遍历得到的字节,所以遍历字符串需要借助 range 关键字。

for i, r := range "Hello, 世界" {
    fmt.Printf("%d\t%q\t%d\n", i, r, r)
}

统计字符串长度,也需要这样:

n := 0
for range s {
    n++
}

2.5.1. 字符串和 Byte 切片

标准库中有四个包对字符串处理尤为重要:

  • bytes:类似 strings 的函数,只不过针对 []byte 类型;
  • strings:提供了字符串查询、替换、比较、截断、拆分和合并等功能;
  • strconv:提供了布尔型、整数型、浮点数和对应字符串的转换,还提供了双引号转义相关的转换;
  • unicode:提供了 IsDigit IsLetter IsUpper IsLower 等功能,用于给字符串分类。

2.5.2. 常量

常量表达式在编译器计算,而不是运行器。每种常量的潜在类型都是基础类型:boolean, string 或者数字。

常量的值不可修改。

3. 复合数据类型

数据、slice、map、结构体。

3.1. 数组

固定长度的特定元素组成的序列,因为数组的长度是固定的,因此 Golang 中很少直接使用数组。

在数组初始化过程中,如果数组的长度位置出现的是 ... 省略号,则表示数组的长度是根据初始化值的个数来计算的。

q := [...]int{1, 2, 3}

数组还可以指定索引初始化,有点像 Python 的 dict,只不过 key 是数组索引,例如:

r := [...]int{99: -1}

将直接初始化 100 个元素的数组,最后一个元素是 -1 。

数组的比较是当数组中的所有元素数值都相等时,则认为数组相等。

3.2. Slice

变长的特定类型的序列,声明一般用 []T 和数组的区别仅在于没有固定长度。

与数组不同的是,slice 之间不能比较,因此不能用 == 操作符来判断两个 slice 是否全部相等元素。

判断 slice 是否为空,使用 len(s)==0 而不应该用 s==nil 来判断。除了和 nil 相等比较外,一个 nil 值的 slice 的行为 和其它任意 0 长度的 slice 一样。

slice 的底层实现类似 C++ 中的 vector 或者 string 内置了一个容量为 cap 的数组,一个标志当前长度的变量,一个头指针。 每次添加元素时,判断 cap 是否足够,不够则已当前容量二倍的长度扩展,然后将旧数据 copy 到新内存中,变更指针和容量标识等等。

3.3. append

append 对 slice 序列进行扩展,每次添加元素并不能知道 append 是否导致了内存的重新分配,因为也不能确定新的 slice 和 原始的 slice 是否引用的是相同的底层数组空间。因此通常是将 append 返回的结果直接赋值给输入的 slice 变量。

slice 中存放三个字段:序列指针、当前长度、总容量。除了底层元素之外,其它都是直接访问的。

3.4. Map

哈希表,声明格式为: map[K]V ,K, V 分别对应 key 和 value 的类型。K 对应的 key 必须是支持 == 比较运算符的数据类型, 用来判断是否重复。

不建议使用 float 作为 map 的 Key 类型,因为 NaN 所表示的浮点数可能和任何浮点数都不相等。

可以像 Python 创建 dict 一样创建 Golang 中的 Map:

ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}

使用内置的 delete 函数删除元素。但是和 Python 不同的是 Go 可以直接使用不存在的元素。当 key 不存在时返回一个该类型的默认值。 也正因为 key 不存在时 map 也可以正确的返回值,所以使用时一般需要对返回值进行判断:

m := map[string]int
m["cc"]  // 如果 cc 不存在将返回 0
_, ok := m["cc"] // 如果 cc 不存在,ok = false

map 的元素并不是一个变量,不能对 map 的元素进行取址操作。因为 map 可能随着元素数量的增长而分配更大的内存空间,从而使导致之前的地址无效。

遍历 map 使用 range ,将得到对应的键/值对。

for name, age := range ages {
    fmt.Printf("%s\t%d\n", name, age)
}

3.5. 结构体

结构体是一种聚合的数据类型,是由零个或者多个任意类型的值聚合成的实体。每个值称为结构体的成员。结构体的成员通过点操作符来访问。

Go 中的指针也通过点操作符来访问(WTF?)…

如果结构体的成员名字是以大写字母开头的,那么该成员就是导出的;这是 Go 语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。

一个结构体类型不能包含自身,但是可以包含自身类型的指针(就像 C++ 中的类成员不能包含类自身一样,但是可以包含自身类型的指针, 很好理解,因为包含自身的情况下如何初始化?)。

结构体初始化有两种形式:一种按照变量声明的顺序初始化,另外一种像 Python 字典一样初始化。

结构体初始化时,不能在外部包初始化未导出的成员。结构体传参都是值传递,所以结构体一般都是指针传参。

3.5.1. 结构体比较

如果结构体中的全部成员都是可以比较的,那么结构体也是可以比较的,这种情况下两个结构体将可以使用 == 或者 != 运算符进行比较。

可以比较的结构体类型,才可以作为 map 的 key 类型。

3.5.2. 匿名成员

Go 语言有个特性让我们只声明成员对应的数据类型,而不是指明成员的名字;这类成员就叫匿名成员。

type Circle struct {
    Point
    Radius int
}

type Wheel struct {
    Circle
    Spokes int
}

访问匿名成员可以直接跳过完整路径访问子成员:

var w Wheel
w.X = 8            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20

但是结构体字面值,并不能这么简单的表示匿名成员,所以下面是错误的:

w = Wheel{8, 8, 5, 20}                       // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

必须采用下面这两种语法,它们彼此是等价的:

w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

这种匿名类型主要的作用是让结构体访问匿名类型的方法集。

3.6. JSON

将 Go 语言中结构体转换成 JSON 的过程叫做编组(marshaling)。将 JSON 数据解码为 Go 语言的数据结构称之为 unmarshaling。

3.7. 文本和 HTML 模板

类似 Python,模板化生成字符串。

4. 函数

4.1. 函数声明

func name(parameter-list) (result-list) {
    body
}

与 C++ 等语言不同的是,Go 的函数返回值可以设置名称,比如:

func sub(x, y int) (z int)   { z = x - y; return}

而且函数的标识符由“参数列表”和“返回值”列表的变量类型决定,如果两个函数的标识符相同,则认为类型相同。

Go 语言没有函数默认参数值。

实参类型通过值的方式传递,但是,如果实参包含引用类型,比如指针、slice、map、function、channel 等类型,实参可能由于函数的间接引用被修改。

4.2. 错误

Go 内置了 error 接口,用来表示函数运行中的错误, nil 表示运行成功, non-nil 表示失败。对于 non-nil 的 error 类型, 可以调用 error 的 Error 函数或者输出函数获得字符串类型的错误信息。

4.3. 函数值

类似函数指针,函数可以赋值给其它变量,传递给其它函数,从函数函数中返回。

4.4. 匿名函数

Go 使用闭包(closures)技术实现函数值。

4.5. Deferred 函数

  • 如果函数中有多个 defer,执行顺序为先进后出,和栈一样;
  • defer 表达式中变量的值在 defer 表达式被定义时就已经明确,所以要注意在 defer 表达式声明时变量的值;

4.6. Panic 异常和 Recover 捕获异常

Panic 和 Recover 是互相协作的一对机制,panic 会引发程序崩溃,而 recover 可以使程序从 panic 中恢复。

比较适合在 Web 服务器中使用,当一个请求崩溃时,不应该让整个服务器崩溃,而是收集崩溃信息并且上报。

5. 方法

5.1. 方法声明

在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。

package geometry

import "math"

type Point struct{ X, Y float64 }

// 传统函数
func Distance(p, q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// 方法
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

有点像 this 或者 self 只不过名字是任意的。

5.2. 基于指针对象的方法

当调用函数时,需要更新一个变量或者函数的其中一个参数太大希望能够避免进行默认的拷贝,这种情况下需要使用指针。

Go 会自动判断方法的接收器是指针类型,还是非指针类型,都可以通过指针/非指针类型进行调用,编译器会自动做类型转换。

至于选择指针还是非指针类型,取决于应用场景,非指针变量会引发一次拷贝。

5.2.1. Nil 也是一个合法的接收器类型

当 nil 对于对象是合法的零值时,比如 map 或者 slice。也就说 nil 对于对象必须是一个有意义的值。

5.3. 封装

Go 语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。

这种限制包内成员的方式同样适用于 struct 或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个 struct。

6. 接口

6.1. 接口约定

接口类型和具体的类型不同在于接口类型只表示他能干什么,而具体的类型表示他是什么。

Go 中的接口不像 Java 中的接口需要显式实现,也不像 C++ 中的虚基类必须要一个派生类来实现,反而有点像 Python 的鸭子类型。

它仅仅是一个约定。如下:

type Writer interface {
    Write(p []byte) (n int, err error)
}

定义了接口 Writer ,它包含一个方法 Write ,凡是实现了 Write(p []byte) (n int, err error) 的方法,便认为满足了约定:

type ByteCounter int

func (c *ByteCounter) Write(p []byte) (int, error) {
    *c += ByteCounter(len(p)) // convert int to ByteCounter
    return len(p), nil
}

接口和实现并没有显式的做关联。

6.1.1. 接口类型

接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。

接口可以组合 -> 接口内嵌:

package io

type Reader interface {
    Read(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}

type ReadWriter interface {
    Reader
    Writer
}
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

6.1.2. 实现接口的条件

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。

6.1.3. flag.Value 接口

6.1.4. 接口值

概念上讲一个接口的值,接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。

6.1.5. 类型断言(Type assertions)

对于接口类型和类型的表达式,语法如下:

x.(T)

将会检查 x 不为 nil 并且 x 储存是值是不是 T 类型。x.(T) 叫做 类型检查

更确切的说,如果 T 不是一个接口类型,x.(T) 检查 x 的动态类型与 T 是否相同。这种情况下,T 必须要实现了 x 的(接口)类型, 除此之外,断言是无效的,因为 x 不可能存储 T 类型的值;如果 T 是一个接口类型,x.(T) 断言 x 的动态类型是否实现了接口 T。

如果断言成立,表达式的值是类型为 T 值为 x。如果断言不成立,会引发一个运行时 panic。换句话说,只有在运行时才能判断 x 的类型。

var x interface{} = 7          // x 的动态类型是 int 并且值为 7
i := x.(int)                   // i 类型为 int 并且值为 7

type I interface { m() }

func f(y I) {
    s := y.(string)        // 不合法:string 没有实现 I 的 m 方法
    r := y.(io.Reader)     // r 类型为 io.Reader 并且 y 的动态类型必须实现了 I 和 io.Reader 的接口
    ...
}

类型表达式用来赋值或者用个数的形式初始化:

v, ok = x.(T)
v, ok := x.(T)
var v, ok = x.(T)
var v, ok T1 = x.(T)

会产生一个无类型的布尔值,断言成立时 ok 的值是 true ,否则为 false 并且 v 是一个 0 值。这种情况下不会有运行时 panic 发生。

7. GoRoutines 和 Channels

Go 语言支持两种并发手段:

  1. goroutine 和 channels,顺序通信进程(Communicating sequential processes)
  2. 多线程共享内存

7.1. Goroutines

在 Go 语言中,每一个并发的执行单元叫作一个 goroutine。

当一个程序启动时,其主函数即在一个单独的 goroutine 中运行,我们叫他 main goroutine。新的 goroutine 用 go 语句来创建。

7.2. channels

channels 是 goroutine 之间的通信机制:一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型。

ch := make(chan int) // ch has type 'chan int'

一个 channel 有发送和接受两个主要操作。发送和接受两个操作都是用 <- 运算符,WTF? 还不如两个方法来的实在:

ch <- x  // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch     // a receive statement; result is discarded

channel 还支持 close 操作,用来关闭 channel,随后基于该 channel 的任何发送操作都将导致 panic 异常。

channel 可以带缓存。

7.2.1. 不带缓存的 Channels

不带缓存的 Channels 的发送操作将导致发送者 goroutine 阻塞,直到另一个 goroutine 在相同的 Channels 上之行接受操作。 当发送的值通过 Channels 成功传输之后,两个 goroutine 可以继续之行后面的语句。反之,如果接受先发生,那么接受者 goroutine 也将阻塞,直到有另一个 goroutine 在相同的 Channels 上执行发送操作。

7.2.2. 串联的 Channels(Pipeline)

Channels 也可以将多个 goroutine 链接在一起,一个 Channels 的输入作为下一个 Channels 的输入。

7.2.3. 单方向的 Channel

Go 语言类型系统提供了单方向的 channel 类型,分别用于只发送或只接受 channel。

  • 类型 chan<- int 表示一个只发送 int 的 channel,只能发送不能接受;
  • 相反,类型 <-chan int 表示一个只接受 int channel,不能发送。
chan T          // 能发送和接受 T 类型
chan<- float64  // 只能用来发送 float64
<-chan int      // 只能用来接受 int

7.2.4. 带缓存的 Channels

带缓存的 Channel 内部持有一个元素队列。队列的最大容量是在调用 make 函数创建 channel 时通过第二个参数指定的。

ch = make(chan string, 3)

与不带缓存的 Channel 相比,带缓存的 Channel 只有缓存满了之后才会阻塞。我们可以通过 cap(ch) 获取缓存的容量,也可以通过 len(ch) 获取有效元素个数。

关于无缓存或带缓存 channels 之间的选择,或者是带缓存 channels 的容量大小的选择,都可能影响程序的正确性。 无缓存 channel 更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存 channel,这些操作是解耦的。 同样,即使我们知道将要发送到一个 channel 的信息的数量上限,创建一个对应容量大小带缓存 channel 也是不现实的,因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓冲将导致程序死锁。

7.2.5. 并发的退出

Go 语言并没有提供一个 goroutine 中终止另外一个 goroutine 的方法。

8. 基于共享变量的并发

8.1. 竞争条件

竞争条件是指程序在多个 goroutine 交叉执行操作时,没有给出正确的结果。

数据竞争会在两个以上的 goroutine 并发访问同一个变量且至少其中一个为写操作时发生。

8.2. sync.Mutex 互斥锁

Go 提供了互斥量 Mutex,并且为之提供了 Lock()UnLock() 用来加锁和解锁。

8.3. sync.RWMutex读写锁

读写互斥锁(Mutex)在读的时候也会「加锁」,但是对于读频次远大于写频次的情况下,这样性能会比较差,因为大家都是在读, 不修改值,没有必要强制限制。所以需要共享锁。

很多情况下,我们需要「多读单写」锁,Go 提供了 sync.RWMutex ,调用 RLockRUnlock 方法来获取和释放一个读取或者共享锁。

8.4. 内存同步

8.5. sync.Once 初始化

如果初始化成本比较大的话,那么将初始化延迟到需要的时候再做就是一个比较好的选择。 因为在程序启动时做初始化会增加程序启动时间并且因为执行的时候可能也不需要这些变量,所以实际上有一些浪费。

但是延迟初始化不是并发安全的。 sync 包提供了专门的方案来解决一次性初始化的问题: sync.Once

var loadIconsOnce sync.Once
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

8.6. 竞争条件检测

只要在 go build,go run 或者 go test 命令后面加上 -race 的 flag,就会使编译器创建一个你的应用的"修改"版或者一个附带了 能够记录所有运行期对共享变量访问工具的 test,并且会记录下每一个读或者写共享变量的 goroutine 的身份信息。

8.7. Goroutines 和 线程

8.7.1. 动态栈

每一个 OS 线程都有一个固定大小的内存块(一般会是 2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时) 的函数的内部变量。

一个 goroutine 会以一个很小的栈开始其生命周期,一般只需要 2KB。一个 goroutine 的栈,和操作系统线程一样,会保存其活跃或挂 起的函数调用的本地变量,但是和 OS 线程不太一样的是一个 goroutine 的栈大小并不是固定的;栈的大小会根据需要动态地伸缩。 而 goroutine 的栈的最大值有 1GB,比传统的固定大小的线程栈要大得多,尽管一般情况下,大多 goroutine 都不需要这么大的栈。

8.7.2. 调度

OS 线程会被操作系统内核调度。每几毫秒,一个硬件计时器会中断处理器,这会调用一个叫作 scheduler 的内核函数。 这个函数会挂起当前执行的线程并保存内存中它的寄存器内容,检查线程列表并决定下一次哪个线程可以被运行,并从内存中恢复该线程 的寄存器信息,然后恢复执行该线程的现场并开始执行线程。因为操作系统线程是被内核所调度,所以从一个线程向另一个"移动"需要完整的 上下文切换 ,也就是说,保存一个用户线程的状态到内存,恢复另一个线程的到寄存器,然后更新调度器的数据结构。 这几步操作很慢,因为其局部性很差需要几次内存访问,并且会增加运行的 CPU 周期。

Go 的运行时包含了其自己的调度器,这个调度器使用了一些技术手段,比如 m:n 调度,因为其会在 n 个操作系统线程上多工(调度) m 个 goroutine。Go 调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的 Go 程序中的 goroutine(译注:按程序独立)。

和操作系统的线程调度不同的是,Go 调度器并不是用一个硬件定时器而是被 Go 语言"建筑"本身进行调度的。例如当一个 goroutine 调用了 time.Sleep 或者被 channel 调用或者 mutex 操作阻塞时,调度器会使其进入休眠并开始执行另一个 goroutine 直到时机到了 再去唤醒第一个 goroutine。因为因为这种调度方式不需要进入内核的上下文,所以重新调度一个 goroutine 比调度一个线程代价要低得多。

8.7.3. GOMAXPROCS

Go 的调度器使用了一个叫做 GOMAXPROCS 的变量来决定会有多少个操作系统的线程同时执行 Go 的代码。其默认的值是运行机器上的 CPU 的核心数。

8.7.4. Goroutine 没有 ID 号

在支持多线程的程序设计语言中,一般当前线程都会有一个独特的 ID,并且很容易被获取到。

goroutine 没有被程序员获取 ID 的概念。

9. 包和工具

Go 语言有超过 100 个标准包,可以通过 https://godoc.org/ 进行检索。

9.1. 导入声明

当导入的两个包有着相同的名字,比如 math/randcrypto/rand 包,那么导入声明必须要至少为一个同包指定一个新的包名以避 免冲突,这称之为导入包的重命名:

import (
    "crypto/rand"
    mrand "math/rand" // alternative name mrand avoids conflict
)

重命名除了可以解决名字冲突之外,还可以为一个笨重的包名重命名。

9.2. 包的匿名导入

如果只是导入包而不使用将会导致的一个编译错误,但是有时候我们想利用导入包而产生的副作用:它会计算包级别变量的初始化表达式 和之行导入包的 init 初始化函数。Go 提供了匿名导入方法。

import _ "image/png" // register PNG decoder

9.3. 包和命名

包名一般采用单数的形式。

9.4. 工具

Go 语言工具箱集合了一系列的功能命令集。可以看作是一个包管理器,用来做包的查询、计算的包依赖关系、从远程版本控制系统和下 载它们等任务。它也是一个构建系统,计算文件的依赖关系,然后调用编译器、汇编器和连接器构建程序。

Go is a tool for managing Go source code.

Usage:

    go command [arguments]

The commands are:

    build       compile packages and dependencies
    clean       remove object files
    doc         show documentation for package or symbol
    env         print Go environment information
    bug         start a bug report
    fix         run go tool fix on packages
    fmt         run gofmt on package sources
    generate    generate Go files by processing source
    get         download and install packages and dependencies
    install     compile and install packages and dependencies
    list        list packages
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         run go tool vet on packages

Use "go help [command]" for more information about a command.

Additional help topics:

    c           calling between Go and C
    buildmode   description of build modes
    filetype    file types
    gopath      GOPATH environment variable
    environment environment variables
    importpath  import path syntax
    packages    description of package lists
    testflag    description of testing flags
    testfunc    description of testing functions

Use "go help [topic]" for more information about that topic.

9.4.1. 工作区结构

对于 Go 语言用户,Go 的工作区通过 GOPATH 来指定,切换工作区只需要更新 GOPATH 即可。

GOPATH 对应的工作目录有三个子目录: src bin pkg

  • src 用来存储源代码;
  • pkg 存储编译后的包的目标文件;
  • bin 存储编译后的可执行程序;

GOROOT 用来指定 Go 的安装目录,还有它自带的标准库包的位置。GOROOT 的目录结构和 GOPATH 类似,因此存放 fmt 包的源代码对 应目录应该为 $GOROOT/src/fmt

go env 命令用于查看 Go 语言工具设计的所有环境变量的值。

9.4.2. 下载包

使用命令 go get 可以下载一个单一的包或者用 ... 下载整个子目录里面的每个包,并且会自动下载依赖的每个包。

go get 命令支持当前流程的托管网站 GitHub、Bitbucket 和 Lanuchpad。

如果指定了 -u 命令标识参数, go get 命令将确保所有的包和依赖的包的版本都是最新的,然后重新编译和安装他们。如果不包含 该标识参数的话,在本地包已经存在的情况下,代码不会再被更新。

本次程序可能需要对依赖包做精确的版本依赖管理,通常的解决方案是使用 vendor 的目录用于存储依赖包的固定版本的源代码,对本地 依赖的包的版本更新也是谨慎和持续可控的。

9.4.3. 构建包

go build 命令编译命令行参数指定的每个包。如果包的名字是 main=,=go build 将调用链接器在当前目录创建一个可执行程序; 以导入路径的最后一段作为可执行程序的名字。

go run 结合了构建和运行两个步骤。

go install 命令和 go build 命令很相似,但是它会保存每个包的编译成果,而不是将它们都丢弃。被编译的包会被保存到 $GOPATH/pkg 目录下,目录路径和 src 目录路径对应,可执行程序被保存到 $GOPATH/bin 目录。

First created: 2017-12-07 14:14:44
Last updated: 2022-12-11 Sun 12:49
Power by Emacs 27.1 (Org mode 9.4.4)