Go 语言知识点笔记

1@ 正文前的 bb

第二次面试周期终于结束了,经历了被某公司放鸽子的社会毒打后,第二波投递广撒网,终于顺利结束了秋招。

有了工作就有了新的学习目标。准备接下来这段时间补充一下 go 语言的基础,买了一本被誉为 Go 语言圣经 的 《Go 程序设计语言》,结合现网上存在的一些优秀开源文章,在此记录下学习笔记 (这里自己只记了一些 Go 比较特殊的点或者比较难懂的点),供自己和有需要的同学参考。

2@ 1 ~ 5 章 语言基础

1、Go 原生的支持 Unicode,所以理论上它可以处理任何国家的语言。

2、go run [源文件] 指令是将某个 go 文件编译、链接、然后运行生成可执行文件。

go build [源文件] 构建出一个可执行的二进制文件。

3、Go 不需要在语句或者声明后面使用分号结尾【跟在特定符号后面的换行符被转换为分号】,除非有多个语句或声明出现在同一行。

4、{ 符必须和 func 关键字放在同一行。

5、gofmt 是一个将代码以标准格式重写的命令行工具(Go 自带),默认重写当前目录所有文件。

6、++i 或者 --i 在 Go 里面是不合法的,其只支持后缀。

7、for 循环是 Go 语言唯一的循环语句。

8、Go 不允许存在无用的临时变量,一般使用空标识符 _ 来处理需要丢弃的临时变量,如下面的 for range 循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"os"
)
func main() {
s, sep := "", ""
// range 产生一对值:索引和这个索引处元素的值
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}

9、:= 这种短变量声明通常只在函数内使用,不适合当做全局变量等包级别的变量声明

10、map 里的键的迭代顺序不是固定的,通常是随机的,每次运行都不一致。这是有意设计的,以防止程序依赖某种特定的序列。[具有一定的随机安全性]

11、函数和其他包级别的实体可以以任意次序声明。

12、常用的 http 请求代码:

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

import(
"fmt"
"io/ioutil"
"net/http"
"os"
)
func main() {
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "reading %s: %v\n", url, err)
os.Exit(1)
}
fmt.Printf("%s", b)
}
}

13、包名本身总是以小写字母组成。实体的第一个字母的大小写决定其可见性是否跨包(首字母大写大写跨包可见,类比 Public 属性)。

14、命名习惯用驼峰(大小都可),特殊单词保持大小写同步,譬如 HTML, ASCII 等。

15、包级别的实体名字对同一个包里的所有源文件都可见。

16、数组和结构体这种复合变量的默认成员初始值都是 0。

17、包级别的初始化在 main 函数开始之前进行。

18、短变量声明最少声明一个新变量,否则编译无法通过。

1
2
3
f, err := os.Open(file)

f, err := os.Create(file) // 编译错误:没有新的变量

19、不是所有值都有地址,但是所有变量都有。

20、new 函数是内置的创建变量的函数,例子如下:

1
2
3
4
p := new(int) // *int 类型的 p,指向未命名的 int 变量
fmt.Println(*p) // 输出 0
*p = 2
fmt.Println(*p) // 输出 2

但其是一个预定义函数,不是关键字,可以在代码 的其他地方重新定义并利用。

21、nil 可以赋值给任何接口变量或者引用类型。

22、如果两个类型具有相同的底层类型或者二者都是指向相同底层类型变量的未命名指针类型,则二者是可以相互转换的。

23、包级别声明可以被同一个包的任何文件引用。导入的包是文件级别的,只能在被导入的文件内才能使用。

24、 Go 的数据类型分为四大类:基础类型,聚合类型【数组,结构体】,引用类型【指针,slice,map,函数,通道】,接口类型。

25、rune 类型是 int32 类型的同义词,常常用于指明一个值是 Unicode 码点。同样,byte 类型和 uint8 类型是同义词,强调一个值是原始数据,而非量值。

26、Go 中浮点型数字转换为整型数字会舍弃小数部分。

27、float32 能精确表示的正整数范围有限。

28、内置的 complex 函数根据给定的实部和虚部创建复数,而内置的 real 函数提取复数的实部,函数 imag 提取复数的虚部。

29、布尔值无法隐式地转换为数值。

30、内置的 len 函数返回字符串的字节数,并非文本数。譬如 中国 这个字符串的函数返回结果是 6。

31、Go 中字符串下标访问 a[0] 返回的是对应索引的 unicode 编码。

32、字符串不可变,所以字符串内部的值不能修改:

1
s[0] = "L" // 编译错误:s[0] 无法赋值

33、字符串不可变意味着两个字符串能安全地共用同一段底层内存。类似,字符串及其子串可以安全地共享数据,这两种情况都没有分配新内存,开销低廉。

34、Go 中的原生字符串使用反引号包围。

35、bytes.Buffer 是高效处理字节 slice 的类型。

36、strconv,Itoa 可以将整数转换为字符串,或者使用 fmt.Sprintf() 函数。

37、Go 语言中,只有大小不明确的 int 类型,却不存在大小不确定的 float 类型和 complex 类型,原因是,如果浮点型数据的大小不明,就很难写出正确的数值算法。

38、若字符串包含一个字节数组,创建后它就无法改变。相反,字节 slice 的元素允许随意修改。

39、常见不易记住的 Printf 的转义值:

1
2
3
4
%t	==> 布尔型
%q ==> 带引号的字符串
%v ==> 内置格式的任何值
%T ==> 任何值的类型

40、由于数组长度固定,所以在 Go 里很少直接使用数组。

41、数组长度中出现省略号,此时数组长度由初始化数组的元素个数决定。

42、数组的长度也是数组类型的一部分,所以 [3]int[4]int 是两种不同的数组类型,也不能拿来比较了。

43、slice 表示一个拥有相同类型元素的可变长度的序列。通常写为 []T。Go 内置函数 len(), cap 用来返回 slice 的长度和容量。

44、和数组不同的是,slice 无法比较。标准库里面提供了针对字节 slice 的比较 bytes.Equal,但是对于其他类型的 slice,我们需要自己设计方法进行比较。

45、slice 类型的 0 值是 nil,是 slice 唯一允许比较的对象。

46、由于无法保证 append() 操作后 slice 对应的底层数组是否改变,我们一般在 append() 操作后显示的再次赋给被操作的 slice。

1
runes = append(runes, r)

47、map 元素不是一个变量,不能获取它的地址。其中一个原因就是 map 的动态增长可能会使原有元素的地址改变。

48、向零值 map 设置值会导致错误。

49、如果 map 中的键不存在,会返回 map 值类型的零值。一般用两个值来接收 map 的元素访问:

1
age, ok := ages["bob"] // 第二个值返回的是布尔值,用来报告该元素是否存在

50、访问成员变量时,点号同样可以用在结构体指针上。

51、一个结构体可以同时包含可导出和不可导出的成员变量。

52、聚合类型不能包含自己,可以包含自己的类型指针。

53、不可以在一个结构体里面定义两个相同类型的匿名成员。

54、为了避免 html/template 对常见特殊字符的转义,可以使用 template.HTML 类型的字符串避免模板的自动转义。

55、函数多个形参的类型如果一致的话,那么函数声明的时候只需要写一次即可。

56、在 Go 语言中,如果函数声明没有函数体,说明这个函数使用除了 Go 以外的语言实现。

57、Go 语言实现了可变长度的栈,栈的大小会随着使用而增长,可达到 1GB 左右的上下。所以 Go 可以放心安全的使用深度递归不用担心一般的栈溢出问题。

58、函数如果有命名的返回值,可以使用 裸返回(即只需要 return 关键字即可,返回参数顺序与函数声明的返回值序列一致)。

59、Go 程序使用通常的控制流机制(if 和 return 语句)应对错误。

60、函数参数为 interface{} 类型时意味着该函数此处参数可以接受任何值。

61、defer 关键字用来处理一些必须出现的成对操作,譬如关闭网络资源,关闭文件资源等。不论是正常情况的 return, 函数执行完毕,或者是不正常的宕机,它们的操作都必须推迟到包含 defer 语句的函数之后才能正常进行。

62、延迟执行的匿名函数可以观察到函数的返回结果(使用 defer 关键字)。

63、许多文件系统中,尤其是 NFS 系统,写错误往往推迟到文件关闭阶段。

3@ 第 6 章 方法

1、Go 中类型的方法不是通过 this 或者 self 实现,而是直接选择接收者实例的名字。

1
2
3
4
5
type Point struct {X, Y, float64}

func (p Point) Distance(q Point) float64 { ... // 函数逻辑}

// 可以使用 p.Distance 来调用方法了

2、p.Distance 叫选择子(selector)。

3、在类型中不能声明相同的成员变量和成员方法名。

4、除了指针类型和接口类型,其他任何简单类型我们都可以方便的绑定方法到对应的类型上。

5、接收者声明出智能放类似 Point 或者 *Point 的类型名。为了防止混淆,不允许本身是指针的类型进行方法声明。

1
2
3
type P *int

func (P) f(){} // 编译错误,非法接收者

6、如果方法要求是指针接收者,我们可以简写:

1
p.fuc() // p 是某类型变量,之所以可以这么简写,是因为编译器对变量进行了隐式转换 &p

7、和普通函数一样,方法在引用本身做的任何改变,都不会在调用者身上产生作用。

8、类型和类型的方法声明需在同一个作用域下(全局),才能形成绑定关系。

4@ 第7章 接口

1、Go 语言的接口独特之处在于其隐式实现。对于一个具体的类型无须声明它实现了哪些接口,只要提供接口所必须的方法即可。

2、一个接口值包括两个部分: 一个具体类型和该类型的一个值。

3、接口的零值就是把它的动态类型和值都设置为 nil

4、接口值是可以比较的。当接口值都为 nil 或者动态类型和动态值完全一致的时候,两个接口相等。

5、一般使用 fmt 包中的 %T 格式化参数来实现对接口动态类型的获取。(内部使用反射实现)。

5@ 第 10 章 包和 go 工具

Go 的包管理一直是大众诟病的一点,随着 Go 版本的迭代,出现了不同的包管理模式。这本书上只是简单的讲了原始 GOPATH 的包管理模式,剩余拓展内容见引用链接。

1、在 http://godoc.org 中可以找到已经公开的包搜索的索引。

2、Go 语言中包的依赖性形成有向无环图,因为没有环,所以包可以独立甚至并行编译(也是 Go 编译速度快的一个原因)。

3、重命名导入:为了避免两个包名相同:

1
2
3
4
import (
"crypto/rand"
mrand "math/rand" // 将此包重命名
)

4、导入包未使用会在编译阶段报错,为了避免这一点,我们使用空白标识符来重命名包:

1
import _ “image/png”

5、当把一个 go 文件的包名声明为 main 的时候,就等于告诉 go 编译程序,这是一个可执行程序,编译器就会尝试编译它为一个可执行文件。main 包里面必须有 main() 函数。

6、对于包的查找1,是有优先级的,编译器会现在 GOROOT 里搜索,其次是 GOPATH,一旦找到,会立马停止搜索。若最终未找到,就报编译异常了。

7、远程包导入首先会在 GOPATH 下寻找,如果没找到,会使用 go-get 远程获取,将其存储在 GOPATH 对应目录下。

8、使用 go mod 类型的包管理时,需要先初始化 go modules,在项目根目录执行命令 go mod init [module name],生成 go.mod 文件。需要注意的是,执行 init 暂时还不会将需要的依赖包进行管理,go,mod 文件只会生成 module 名go version (此时这个 go.mod 甚至是隐藏的,存在于内存中?), 只有真正执行 run 或者 build 的命令时才会真正触发依赖的解析和下载。

9、go mod 这种包管理模式在 1.13 的版本以后默认开启。

6@ 其他 Go 语言特性

1、flag 是 Go 标准库提供的包,用于方便的进行命令行解析。

2、绑定 flag 有两种方法:

1
2
3
4
5
6
7
1、flag.Xxx,其中 Xxx 可以是 Int, String 等,返回一个相应类型的指针。

var ip = flag.Int("flagname", 1234. "help message for flagname")

2、flag.XxxVar,将 flag 绑定到一个变量上面。

flag.IntVar(&flagvar, "flagname", 1234. "help message for flagname")

3、常见的 flag 类型:

1、ErrorHandling,定义了参数出错时的处理方式:

type ErrorHandling int

1
2
3
4
5
const (
ContinueOnError ErrorHandling = iota
ExitOnError
PanicOnError
)

2、FlagSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// A FlagSet represents a set of defined flags.
type FlagSet struct {
// Usage is the function called when an error occurs while parsing flags.
// The field is a function (not a method) that may be changed to point to
// a custom error handler.
Usage func()

name string // FlagSet 的名字。CommandLine 给的是 os.Args[0]
parsed bool // 是否执行过 Parse()
actual map[string]*Flag // 存放实际传递了的参数(即命令行参数)
formal map[string]*Flag // 存放所有已定义命令行参数
args []string // arguments after flags // 开始存放所有参数,最后保留 非 flag(non-flag)参数
exitOnError bool // does the program exit if there's an error?
errorHandling ErrorHandling // 当解析出错时,处理错误的方式
output io.Writer // nil means stderr; use out() accessor
}

3、Value 接口

1
2
3
4
5
6
// Value is the interface to the dynamic value stored in a flag.
// (The default value is represented as a string.)
type Value interface {
String() string
Set(string) error
}

所有参数类型都需要实现 Value 接口才能正常解析,内置 flag 包为 int,float,bool 等类型实现了该接口。利用该接口,我们可以自定义 flag。

Reference:

《Go 程序语言设计》

https://www.flysnow.org/2017/03/04/go-in-action-go-package.html

https://zhuanlan.zhihu.com/p/92992277?utm_source=wechat_session

http://books.studygolang.com/The-Golang-Standard-Library-by-Example/ 通过例子学 Go 标准库

查看评论