Go 语言36讲

GOPATH 有什么意义

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

源码安装后的结果

在安装后如果产生了归档文件(以“.a”为扩展名的文件),就会放进该工作区的 pkg 子目录;如果产生了可执行文件,就可能会放进该工作区的 bin 子目录。

某个工作区的 src 子目录下的源码文件在安装后一般会被放置到当前工作区的 pkg 子目录下对应的目录中,或者被直接放置到该工作区的 bin 子目录中。

构建和安装的过程

构建使用命令go build,安装使用命令go install。构建和安装代码包的时候都会执行编译、打包等操作,并且,这些操作生成的任何文件都会先被保存到某个临时的目录中。

构建

如果构建的是库源码文件,那么操作后产生的结果文件只会存在于临时目录中。这里的构建的主要意义在于检查和验证。
如果构建的是命令源码文件,那么操作的结果文件会被搬运到源码文件所在的目录中。

安装

安装操作会先执行构建,然后还会进行链接操作,并且把结果文件搬运到指定目录。
如果安装的是库源码文件,那么结果文件会被搬运到它所在工作区的 pkg 目录下的某个子目录中。
如果安装的是命令源码文件,那么结果文件会被搬运到它所在工作区的 bin 目录中,或者环境变量GOBIN指向的目录中。

源码中包名和文件夹名不同

当源码中包名和文件夹名不一致时,源码文件所在的目录相对于 src 目录的相对路径就是它的代码包导入路径,而实际使用其程序实体时给定的限定符要与它声明所属的代码包名称对应。也就是说导入包的时候,还是以文件夹路径的名称来导入,但是在使用包中的方法的时候,需要使用里面的 “package 名称.方法名” 来使用。
所以为了不让这种情况的代码包的使用者产生困惑,我们总是应该让声明的包名与其父目录的名称一致。

访问权限

名称的首字母为大写的程序实体才可以被当前包外的代码引用,否则它就只能被当前包内的其他代码引用。
通过名称,Go 语言自然地把程序实体的访问权限划分为了包级私有的和公开的。对于包级私有的程序实体,即使你导入了它所在的代码包也无法引用到它。

除此之外 GO 1.4 版本中还新增了 internal (内部包)。内部包的规范约定:导出路径包含internal关键字的包,只允许internal的父级目录及父级目录的子包导入,其它包无法导入。

1
2
3
4
5
6
7
8
9
10
11
.
|-- checker
| |-- internal
| | |-- cpu
| | | `-- cpu.go
| | `-- ram
| | `-- ram.go
| `-- server.go
|-- go.mod
|-- go.sum
`-- main.go

如上包结构的程序,checker/internal/cpuchecker/internal/ram只能被checker包及其子包中的代码导入,不能被main.go导入。当在main.go中导入并调用其函数,编译期会报如下错误:

1
2
$ go build
main.go:10:2: use of internal package app/testing/checker/internal/cpu not allowed

变量重声明和可重名变量

变量重声明中的变量一定是在某一个代码块内的。

变量重声明是对同一个变量的多次声明,这里的变量只有一个。而可重名变量中涉及的变量肯定是有多个的。

不论对变量重声明多少次,其类型必须始终一致,具体遵从它第一次被声明时给定的类型。而可重名变量之间不存在类似的限制,它们的类型可以是任意的。

如果可重名变量所在的代码块之间,存在直接或间接的嵌套关系,那么它们之间一定会存在“屏蔽”的现象。但是这种现象绝对不会在变量重声明的场景下出现。

类型断言

类型断言表达式的语法形式是x.(T)。其中的x代表要被判断类型的值,这个值当下的类型必须是接口类型的。

interface{}(container)container 变量的值转换为空接口值 .([]string) 用于判断前者的类型是否为切片类型 []string

interface{}代表空接口,任何类型都是它的实现类型。

别名类型和潜在类型

别名类型,MyStringstring类型的别名类型。顾名思义,别名类型与其源类型的区别恐怕只是在名称上,它们是完全相同的。Go 语言内建的基本类型中就存在两个别名类型。byteuint8的别名类型,而runeint32的别名类型。

1
type MyString = string

类型再定义,把string类型再定义成了另外一个类型MyString2MyString2string就是两个不同的类型了。这里的MyString2是一个新的类型,不同于其他任何类型。

1
type MyString2 string // 注意,这里没有等号。

切片和数组

数组类型的值的长度是固定的,而切片类型的值是可变长的。

数组的长度在声明它的时候就必须给定,并且之后不会再改变。可以说,数组的长度是其类型的一部分。比如,[1]string[2]string就是两个不同的数组类型。

而切片的类型字面量中只有元素的类型,而没有长度。切片的长度可以自动地随着其中元素数量的增长而增长,但不会随着元素数量的减少而减小。

Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。

估算切片的长度和容量

如下所示,在使用 make 函数创建一个切片的时候,可以指定长度和容量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
// 示例 1。
/**
The length of s1: 5
The capacity of s1: 5
The value of s1: [0 0 0 0 0]
The length of s2: 5
The capacity of s2: 8
The value of s2: [0 0 0 0 0]
*/
s1 := make([]int, 5) // 只指定长度
fmt.Printf("The length of s1: %d\n", len(s1))
fmt.Printf("The capacity of s1: %d\n", cap(s1))
fmt.Printf("The value of s1: %d\n", s1)
s2 := make([]int, 5, 8) // 指定长度和容量
fmt.Printf("The length of s2: %d\n", len(s2))
fmt.Printf("The capacity of s2: %d\n", cap(s2))
fmt.Printf("The value of s2: %d\n", s2)
}

基于某个切片生成新切片的时候,那么新切片的长度和容量又怎么计算呢?例如下面的 s4 的长度和容量是多少?

1
2
3
4
5
s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
s4 := s3[3:6]
fmt.Printf("The length of s4: %d\n", len(s4))
fmt.Printf("The capacity of s4: %d\n", cap(s4))
fmt.Printf("The value of s4: %d\n", s4)

这里的3可被称为起始索引,6可被称为结束索引。那么s4的长度就是6减去3,即3。由于s4是通过在s3上施加切片操作得来的,所以s3的底层数组就是s4的底层数组。在底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾。所以,s4的容量就是其底层数组的长度8, 减去上述切片表达式中的那个起始索引3,即5

切片的扩容

当切片无法容纳更多元素的时候,新切片的容量将会是原切片容量的 2 倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
// 示例1。当切片无法容纳更多元素的时候,新切片的容量将会是原切片容量的 2 倍。
/**
初始容量: 0
s6(1): 长度: 1, 容量: 1
s6(2): 长度: 2, 容量: 2
s6(3): 长度: 3, 容量: 4
s6(4): 长度: 4, 容量: 4
s6(5): 长度: 5, 容量: 8
*/
s6 := make([]int, 0)
fmt.Printf("初始容量: %d\n", cap(s6))
for i := 1; i <= 5; i++ {
s6 = append(s6, i)
fmt.Printf("s6(%d): 长度: %d, 容量: %d\n", i, len(s6), cap(s6))
}

fmt.Println()
}

当原切片的长度大于或等于1024时,Go 语言将会以原容量的1.25倍作为新容量的大小。另外,如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准。