《Go程序设计语言》阅读笔记
- 《Go程序设计语言》阅读笔记
第1章 入门
给几个例子快速讲解go语言的结构例子,如果是对go比较熟悉的读者或许会比较方便。但我是从零开始,对我而言跳跃得有些太快了,看到1.4就跟不上了。不过也没关系,从第二章开始看更详细更系统化。
第2章 程序结构
2.1 名称
给出Go语言中的关键字和预声明的常量、类型和函数,有需要可以查表。
- 命名规则
一般使用驼峰式,首字母缩写使用相同大小写。
2.2 声明
没啥内容,重要的都放在后续章节详细展开。
2.3 变量
变量声明通用形式:var name = expression
类型和表达式可以省略其一。如果省略类型,将由表达式自动推断;如果省略表达式,初始值为对应类型的零值。
2.3.1 短变量声明
name := expresion
- 短小灵活,常用于局部变量的声明和初始化
- 不需要声明所有变量,对已声明变量相当于赋值
- 最少声明一个新变量
2.3.2 指针
基本用法和C一致,使用&
取地址,*
解引用。
简单介绍了flag
包的用法来辅助说明指针。
2.3.3 new 函数
表达式new(T)
将创建一个未命名的T类型变量,初始化为该类型的零值,并返回其地址。简单来说,就是方便快捷地获取某一类型的地址,而不必先声明再取地址。
new
是一个预声明的函数,不是关键字,可以被重定义。
2.3.4 变量的生命周期
生命周期的概念很多编程语言都有,不用再看一遍。
这部分还提到了垃圾回收和变量逃逸的概念,不过也是浅尝辄止,不看也行。
2.4 赋值
Go和C++类似有着形如+=
这样的运算赋值和++
,--
这样的自增自减运算。
2.4.1 多重赋值
和Python类似,Go也可以同时为复数变量赋值,并可以将不需要的值赋给空标识符_
。
2.4.2 可赋值性
没看懂在讲什么,但是不重要。
2.5 类型声明
type name underlying-type
和C++的typedef
类似,Go也可以通过type
关键字为某一类型设置一个别名,方便阅读理解和使用。但不同的是,Go使用type
定义的命名类型,即使底层类型相同也不能使用算术表达式进行比较和合并,主要是为了避免混用。
这部分还提了下类型声明在接口中的使用方法,这将在第七章详细说明。
2.6 包和文件
-
可见性
-
声明在函数内部,是函数的本地值,类似private
-
声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect
-
声明在函数外部且首字母大写是所有包可见的全局值,类似public
2.7 作用域
大部分内容都不难理解。只是有一个地方需要注意一下:短变量声明依赖一个明确的作用域。错误示例:
var x int = 0
if true {
x, y := 1, 2
fmt.Println(x, y)
}
x += x
fmt.Println(x)
//1,2
//0
在如上代码中,x,y在内部作用域都为声明,短变量声明:=
将会将x,y都声明为内部的局部变量,使外部的x声明不可见,从而未能成功变更外部x的值。这是一个很隐蔽的错误,甚至不会报错。
要避免这个错误,可以放弃使用短变量声明而是先声明再直接赋值:
var x int = 0
if true {
var y int
x, y = 1, 2
fmt.Println(x, y)
}
x += x
fmt.Println(x)
//1,2
//2
第3章 基本数据
Go四类数据类型:基础类型(basic type),聚合类型(aggregate type),引用类型(reference type),接口类型(interface type)。
3.1 整数
数据类型和C中的stdint.h类似,对不同位数的int进行了精确划分,这里记几个比较特殊的。
rune
和int32
同意,指明一个值是Unicode码点(code point)。byte
和uint8
同意,强调一个值是原始数据而非量值。uintptr
大小不定,完整存放指针,用于底层编程。
这部分还介绍了算数运算符,逻辑运算符和位运算符以及他们之间的优先级,有需要可以查表。
位运算符和C稍有不同,记一下。
^
作为二元运算符表示按位“异或”(XOR),如果需要写次方可以用math.Pow()
。作为一元运算符表示取反。&^
表示按位清除(AND NOT),如表达式z=x&^y中,若y的某位是1,则z的对应位等于0;否则,它就等于x的对应位。
格式化输出八进制%o
,十六进制%x
时,前面的副词#
告知是否输出相应前缀0
,0x
。
3.2 浮点数
格式化输出浮点数时,%g
会自动保持足够的精度,%e
(有指数)%f
(无指数)能自定义输出宽度和数值精度。
3.3 复数
GO居然还有复数类型,但感觉我用不上就没怎么看,应该科学计算或者图形学会用的多些。
3.4 布尔值
唯一需要注意的是Go中整数不能像C那样直接当布尔值用需要显性转换。
3.5 字符串
和python类似,没啥新东西。
3.5.1 字符串字面量
介绍了一些转义符,可以查表。
-
字符串字面量使用双引号,可以转义。
-
原生的字符串字面量使用反引号,不能转义,可以展开多行。
3.5.2 Unicode
介绍Unicode来源和rune
。
3.5.3 UTF-8
略,有需要再翻书。
3.5.4 字符串和字节 slice
比较了字符串和slice,因为不熟悉slice没看出什么重点,有空回头补。
3.5.5 字符串和数字的相互转换
略,有需要再翻书。该加速了,少扣点细节。
3.6 常量
同时声明一组常量时,可以省略第一项以外的表达式,这时会复用前一项的表达式及类型。
3.6.1 常量生成器 iota
iota
从0开始取值逐项加1。复数声明常量时由于复用表达式可以达到不同初始值的效果。具体实例见书。
3.6.2 无类型常量
略,有需要再翻书。
第4章 复合数据类型
4.1 数组
大部分都是基础知识,有一些比较新奇的定义方法:
- 使用
...
代替数组长度,数组长度将有初始化时的元素个数决定,如arr := [...]int{1,2,3}
长度为3。 - 初始化时可以使用
pos:value
仅定义部分元素,未定义的元素默认为零值。如arr := [...]int{4:1}
,即为{0,0,0,0,1}。
在如C等编程语言中,数组都是隐式的引用传递,而在Go中,直接传递数组当参将会和其他类型一样使用值传递,创建一个副本。此外,数组的大小也是其类型的一部分,类型 [10]int 和 [20]int 是不同的。
4.2 slice
slice的大部分操作都和数组类似,主要是长度不定,概念和用法更接近c的vector和python的list。slice的零值是nil。slice也可以通过make([]T, len, cap)
来声明,其中容量可省略。
slice和数组的区别首先是定义式slice不需要指定长度。然后是数组可以直接比较而slice不可以,需要手写比较函数。
4.2.1 append 函数
在讲什么没找到重点。可能是想让读者注意一下len和cap吧,但这个基本上都是隐性的用的时候真不用想真么多。
4.2.2 slice 就地修改
介绍一些slice的精细操作,还是感觉没大用。
4.3 map
记一下定义方式:ages := make(map[string]int)
可以使用{key:value,...}
初始化,以及delete(key,value)
删除元素。
map和slice都是引用,不能直接获取地址,可以用range遍历,将返回键值对。
map中元素迭代顺序不固定,一般认为是随机的,可以是程序在不同散列算法实现下变得健壮。如需按顺序遍历,必须显式地给键排序。
map类型的零值是nil
,大多数map操作都可以在零值nil上执行,但设置元素会导致错误,必须先初始化map。
可以通过if value, ok := m[key]; !ok
来判断这个元素是不存在还是零值。
map需求特殊数据类型作为键(如slice),可以定义一个帮助函数中转,将不可比较的数据类型映射到字符串。示例如下:
var m make(map[string]int)
func k(list []string) string { return fmt.Sprintf("%q", list) }
func Add(list []string) { m[k(list)]++ }
func Count(list []string) { return m[k(list)] }
4.4 结构体
定义:
type struct1 struct{
field1 type1
field2 type2
struct2 // 嵌入结构体,继承所有字段 Embedded
struct3 struct3 // 嵌套结构体,通过.访问子结构体字段 Nested
}
结构体定义时不能定义一个相同结构体类型的成员变量(对其他聚合类型也适用),但可以定义一个指向该类型的指针,从而实现递归数据结构,比如链表和树。
结构体零值由成员零值组成。
没有成员变量的结构体为空结构体,写作struct{}
。这里没有过多介绍空结构体,作者似乎也不提倡这么用。
4.4.1 结构体字面量
可以通过结构体字面量,即设置结构体成员变量来设置结构体的值。有两种格式,两种方法不能混用:
- 按顺序为每个成员变量赋值
可读性差,仅用于顺序明显的小结构体 - 指定成员变量名称和值
不需要按顺序,可以仅赋值部分成员
关于结构体指针的使用和c类似,不再赘述。
4.4.2 结构体比较
4.4.3 结构体嵌套和匿名成员
关于这部分内容我最早还是再gorm里看到的。那边把两种情况分开,分别叫嵌套(Nested)和嵌入(Embedded),这边都叫嵌套,然后匿名作为一种特殊用法。
type Point struct {X,Y int}
type Circle struct {Point; Radius int}
type Wheel struct {Circle; Spokes int}
这时就可以直接访问需要变量而省略中间变量,即匿名成员:
var w Whell
w.x = 1 // 两种用法等价
W.Circle.Point.y = 1
但是使用结构体字面量初始化并不能省略,需要遵循定义:
var w1, w2 Wheel
w1 = Wheel{Circle{Point{1,2},3},4}
w2 = Wheel{
Circle : Circle{
Point : Point{
X : 1,
y : 2,
},
Radius : 3,
},
Spoke : 4,
}
4.5 JSON
json格式略。
Go数据结构转换为JSON:
json.marshal(v any)
返回一个不带任何多余空白字符的很长的字符串。
json.marshalIndent(v any,prefix string,indent string)
输出格式化结果,prefix为前缀,indent为缩进。
只有可导出的成员才能转换为json。
可以使用 成员标签定义 更改转换为json后的字段名,如json:fieldname
。
JSON解析为Go数据结构:
json.unmarshal(data []byte, v any)
jsong字段关联到结构体成员时忽略大小写,但存在下划线的话还是要用标签定义。
流式解码器json.Decoder
,使用示例:
terms := []string{"..."}
q := url.QueryEscape(string.Join(terms, " "))
resp, err := http.Get(URL + "?q=" + q)
defer resp.Body.Close()
json.NewDecoder(resp.body).decode(&result)
4.6 文本和HTML模板
模板是一个字符串或者文件,它包含一个或者多个两边用双大括号包围的单元
{{...}}
,这称为操作。
操作提供的功能:
- 输出值
- 选择结构体成员
- 调用函数和方法
- 描述控制逻辑
- 实例化其他模板
.
表示模板里的参数
range var
end
表示循环,此时的.
表示var里的元素
|
将上一个操作的结果作为下一个操作的输入,和管道类似。
通过模板输出:
template.New
创建并返回一个新的模板
Func
添加函数到模板内部可以访问的函数列表
Parse
解析文本模板
temlate.Must
错误处理,详见5.9
temp1, err := template.New("name").Funcs(template.FuncMap{"func1": func1}).Parse(text)
temp1 = temlate.Must(temp1)
html/template
包使用和text/template
包里面一样的API和表达式语
句,并且额外地对出现在HTML、JavaScript、CSS和URL中的字符串进行自动转义。
第5章 函数
5.1 函数声明
func name(parameter-list)(result-list){
body
}
函数的基本内容略。
如果几个形参或返回值类型相同则可以简写:a int, b int
->a, b int
go函数支持复数返回值,并允许在返回值列表中显性声明变量,此时return
的内容可省略,即裸返回。
func func1()(res int){ ... ; return }
函数的类型称为 函数签名 。
有些函数声明没有函数体,说明这个函数使用了Go以外的语言实现。
5.2 递归
略
5.3 多返回值
如果一个函数返回一组值,想要使用这些返回值就必须显式地将其赋给变量。
如果想要忽视其中一部分可以赋给空标识符_
。
一个多值调用可以当作多个参数进行传参。
5.4 错误
与许多其他语言不同,Go语言通过使用普通的值而非异常来报告错误。尽管Go语言有异常机制,这将在5.9节进行介绍,但是Go语言的异常只是针对程序bug导致的预料外的错误,而不能作为常规的错误处理方法出现在程序中。
这样做的原因是异常会陷人带有错误消息的控制流去处理它,通常会导致预期外的结果:错误会以难以理解的栈跟踪信息报告给最终用户,这些信息大都是关于程序结构方面的而不是简单明了的错误消息。
5.4.1 错误处理策略
- 传递错误,使子例程的错误变为主调例程错误。
使用fmt.Errorf()
格式化错误信息并返回新的错误值,为原始错误信息添加额外上下文信息来建立可读的错误描述。 - 对操作进行重试,超出次数或时间后再报错退出。
- 输出错误并停止程序。
fmt.Fprintf(os.Stderr, "...%v...\n", err)
log.Fatalf("...%v\n", err)
- 只记下错误信息然后程序继续运行。
5.4.2 文件结束标识
io.EOF定义:
package io
import "errors"
// 当没有更多输入时返回EOF
var EOF =errors.New("EOF")
检测示例:
in := bufio.NewReader(os.Stdin)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break //结束读取
}
if err != nil{
return fmt.Errorf("read failed: %v", err)
}
}
// ...使用r...
5.5 函数变量
函数变量的零值为nil
(空值),不能调用空的函数变量。
函数变量可以和空值比较的,但它们本身不可比较。
5.6 匿名函数
命名函数只能在包级别作用域声明。但使用 函数字面量 能在任何表达式内指定函数变量。函数字面量就像函数声明,但在func关键字后面没有函数的名称。它是一个表达式,它的值称作匿名函数。
更重要的是,以这种方式定义的函数能够获取到整个词法环境,因此里层的函数可以使用外层函数中的变量。
警告:捕获迭代变量:
func main() {
var printNums []func()
nums := []int{1,2,3,4,5,6,7,8,9,10,11}
for _, num := range nums {
// num := num // 解决方法
printNums = append(printNums, func() {
fmt.Printf("%d ", num) // NOTE: incorrect!
})
}
for _, f := range printNums {
f()
}
fmt.Println()
}
// $ go run main.go
// 11 11 11 11 11 11 11 11 11 11 11
在Go语言中,迭代变量捕获通常指的是在使用循环(如for循环)与匿名函数或闭包结合时,循环的迭代变量被闭包捕获的现象。这种情况下,如果闭包在其外部函数返回后仍然存活,并且在之后的某个时刻被调用,那么它访问的是迭代变量的最终值,而不是特定迭代中的值。
解决方法:用局部变量拷贝循环变量的值。
简单说就是闭包中存的迭代变量使用的是引用地址,而不是本身的值,所以迭代完成后存的是同一个地址。拷贝变量就为每次迭代分离出一个独立的地址。因为Go中为了方便,值和指针基本是混用的区别很少,所以可能不是很好理解。我也不确定我的理解对不对。
5.7 变长函数
在参数列表最后的类型名称前使用省略号...
声明变长函数,可以传递任意数目该类型的参数。
当实参存在slice中时,在和后面放一个省略号来调用变长函数。以下两种调用等价:
func sum(vals ...int) int { }
values := []int{1,2,3,4}
sum(1,2,3,4)
sum(values...)
5.8 延迟函数调用
语法上,一个defer语句就是一个普通的函数或方法调用,在调用之前加上关键字defer。函数和参数表达式会在语句执行时求值,实际的调用推迟到包含defer语句的函数结束后才执行。defer语句没有限制使用次数;执行的时候以调用defer语句顺序的倒序进行。
5.9 宕机
宕机发生时,程序执行终止,goroutine所有延迟函数执行,程序异常退出并留下日志信息,包括宕机的值(错误消息),函数调用的栈跟踪消息,用于诊断问题原因。
5.10 恢复
8910这三部分其实都没看懂,勉强抄点书。
如果内置的
recover
函数在延迟函数的内部调用,而且这个包含defer 语句的函数发生宕机,recover会终止当前的宕机状态并且返回宕机的值。函数不会从之前宕机的地方继续运行而是正常返回。如果recover在其他任何情况下运行则它没有任何效果且返回nil。
func doSomethingThatPanic() { panic("something went wrong") }
func main() {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered in main: %v", r)
}
}()
doSomethingThatPanic()
fmt.Println("This line will not be reached.")
}
第6章 方法
6.1 方法声明
方法声明和函数类似,但在前面多了一个参数,将方法显式地绑定在对应类型上。
Go语言中,接收者不使用如this
或self
的特殊名,而是自己定义。
Go和其他面向对象语言不同,可以把方法绑定到任何类型上,可以方便地为简单类型定义附加行为。
6.2 指针接收者的方法
由于主调函数会复制每一个实参变量,如果函数需要更新一个变量,或者如果一个实参太大而我们希望避免复制整个实参,因此我们必须使用指针来传递变量的地址。这也同样适用于更新接收者:我们将它绑定到指针类型,比如*Point。
习惯上遵循如果Point的任何一个方法使用指针接收者,那么所有的Point方法都应该使用指针接收者,即使有些方法并不一定需要。
关于实参接收者和形参接收者:
- 同一类型,比如都是T或*T,没问题
- 实参T形参*T,编译器会隐式取地址
- 实参*T形参T,编译器会隐式解引用
总之就是在调用方法时不用太在意类型,编译器基本都会隐式转换为合适的类型。这样做简洁明了,避开了指针这一个常规重难点,但有时也会引起混乱,分不清楚当前类型。
nil是一个合法的接收者:
就像一些函数允许nil指针作为实参,方法的接收者也一样,尤其是当nil是类型中有意义的零值(如map和 slice 类型)时,更是如此,例如用list *List = nil
表示空链表。当定义允许nil作为接收者时应该在注释中显式标明。
6.3 通过接口提内嵌组成类型
和前面匿名成员提到的内容差不多,不再赘述。
6.4 方法变量与表达式
可以把方法赋给一个方法变量,它是一个函数,把方法绑定在一个接收者上,此时函数只需要提供实参不需要提供接收者。如func1 := t.func; func1(a,b,c)
类似的还有方法表达式,必须提供接受者并按照选择子语法调用,把原来的接收者换成函数的第一个形参,可以像函数一样调用。如func2 := T.func; func2(t,a,b,c)
6.5 示例:位向量
没啥内容
6.6 封装
Go语言只有一种方式控制命名的可见性:定义的时候,首字母大写的标识符是可以从包中导出的,而首字母没有大写的则不导出。同样的机制也同样作用于结构体内的字段和类型中的方法。结论就是,要封装一个对象,必须使用结构体。
另一个结论就是在Go语言中封装的单元是包而不是类型。无论是在函数内的代码还是方法内的代码,结构体类型内的字段对于同一个包中的所有代码都是可见的。
封装的优点:
- 使用方不能直接修改对象的变量,不需要更多语句检查变量的值。
- 隐藏实现细节防止属性改变,设计者可以更灵活地改变API实现而不破坏兼容性。
- 防止使用者肆意更改对象内变量。
第7章 接口
7.1 接口即约定
接口是一种抽象类型,不暴露所含数据和内部结构,只提供一些方法。简单来说相当于提供一个黑盒,不用关心内部实现只要知道能干什么就行。其实我们学编程大部分适合也是学着使用一个个黑盒而已,只不过我们不止用还负责造罢了。
7.2 接口类型
定义所含方法,嵌入其他接口,或者两者混用。这有必要单独开一节讲吗。。。
7.3 实现接口
如果一个类型实现了接口要求的所有方法,那么这个类型实现了这个接口。这时我们常说某类型是一个(is a)特定的接口类型。
给接口赋值时,只有当表达式实现了接口时才能赋值给该接口。
接口封装了对应的类型和数据,只有通过接口暴露的方法才可以调用,类型的其他方法无法通过接口调用。
interface{}
被称为空接口类型。它对其实现类型没有任何要求,因此可以把任何值赋给空接口类型。
之后介绍了一下定义接口的方法,例如从类型出发,提取共性等等。
7.4 使用 flag.Value 来解析参数
没啥内容,就一示例。
7.5 接口值
接口值分为两部分:一个具体类型和该类型的一个值,分别称为接口的动态类型和动态值。
讲什么编译时动态分发,没看懂。不过不影响用接口。
一个接口值可以指向多个任意大的动态值。 这段也没看懂。
-
接口值的比较
接口值可以用==和!=做比较。
两个接口相等要求都是nil或动态类型和动态之都一致。
因为接口值可以比较,所以可以作为map的键或则和switch的操作数。
此外,接口值动态类型一致但动态值不可比较也很常见。
相比其他类型的可比性是确定的,接口类型的比较必须是否小心确认其可比性。 -
获取接口值动态类型
可以使用fmt包的%T实现。
var w io.Writer
fmt.Printf("%T\n",w)//"<nil>"
w= os.Stdout
fmt.Printf("%T\n",w)//"*os.File"
w= new(bytes.Buffer)
fmt.Printf("%T\n",w)//"*bytes.Buffer"
在内部实现中,fmt用反射获取接口的动态类型,这将在12章详细讨论。
- 注意:含有空指针的非空接口
假设由如下代码
var buf *bytes.Buffer
f(buf)
func f(out io.Writer){
if(out!=nil){
out.Write([]byte("done!\n"))
}
}
当函数运行到out.Write时会报错,因为out的值为空,即方法的接收者为空。前面我们说过,接口值包括动态类型值和动态值。在把形参的空指针buf赋给实参out时,out的动态类型由nil变为了*bytes.Buffer。而只有动态类型和动态值都为nil的接口值才为空,所以out通过了非空检查。
正确做法是一开始就把buf类型定义为io.Writer,避免把功能不完整的值(仅有类型)赋给接口。
7.6 使用 sort.Interface 来排序
一个接口的使用示例
7.7 http.Handler 接口
接口的使用示例,介绍一个特殊的接口http.Handler,但我没看懂特殊在哪,而且对http相关也不熟悉,到底什么课会教啊,怎么感觉不管看什么都默认对它很熟悉的样子。。。
7.8 error 接口
这里还找到个错,书上“包含”写成“不含”了。投稿会不会给我钱(想peech)
后面好像全是示例,先跳一下吧。
7.9 示例:表达式求值器
7.10 类型断言
一文掌握 Golang 中的类型断言
书上原文讲的有点散,换了篇更通俗的文自己看。有时候学东西没必要死磕,看看别人怎么讲的对照一下说不定更好理解。
Golang 中的接口是一种抽象类型,可以存储任何实现了该接口方法的类型实例。然而,由于接口本身不包含类型信息,需要通过类型断言来将接口变量转换为实际类型。基本语法如下:
value, ok := x.(T)
x 是一个接口类型的变量,T 是希望断言的类型。value 将会是 x 转换为类型 T 后的值,ok 是一个布尔值,当类型断言成功时为 true,失败时为 false 。也可以不带ok,但如果断言失败会引发 panic。
x 必须是接口类型,非接口类型的 x 不能做类型断言。
T如果是非接口类型,检查x的动态类型是否为T,即 T 应该实现 x 的接口。
T 如果是接口,则 x 的动态类型也应该实现接口 T。
如果操作数是空接口值,则类型断言失败。
7.11 使用类型断言来识别错误
MARK
没怎么看懂,好像大概意思就是通过断言转换成特定类型,含有更多的信息。
再给给个例子说明,并提示通过fmt.Errorf等合并错误消息可能丢失结构信息,最好在失败操作发生时马上处理错误,而不是返回给调用者后。
7.12 通过接口类型断言来查询特性
咕,看不懂。接口这个东西放到实战怎么能衍生出这么多复杂东西。我还以为有个方法就算接口了,结果用起来千奇百怪,也不知道是go的特性还是其他面向对象语言都这样。
7.13 类型分支
MARK
关于联合,可识别联合,类型多态,特设多态等一大堆专有名词没看懂。
简单来说,x.(type)
可以返回类型,配合switch和重用变量名可以很方便判断类型并操作:
func sqlQuote(x interface{}) string {
switch x := x.(type) {
case nil:
return "NULL"
case int, uint:
return fmt.Sprintf("%d", x) //这里x类型为interface{}
case bool:
if x {
return "TRUE"
}
return "FALSE"
case string:
return sqlQuotestring(x) //(未显示具体代码)
default:
panic(fmt.Sprintf("unexpected type %T:%v", x))
}
}
7.14 示例:基于标记的XML解析
看得脑子一团浆糊,先看看别的换换脑子吧。是我前置知识不够吗?在学校的时候应该多学点的。
7.15 一些建议
MARK
略
第8章 goroutine和通道
8.1 goroutine
go的并发相对其他编程语言很简单,只需要在函数或方法前加上go
关键字即可创建一个goroutine并发执行操作。goroutine类似于线程,但其实是一个更小的单位。
8.2 示例:并发时钟服务器
略,很简单的一个例子,顺便介绍了时间格式化。
8.3 示例:并发回声服务器
略,很简单的一个例子
8.4 通道
通道是可以让一个goroutine发送特定值到另一个goroutine的通信机制。通道的类型为chan
。
ch := make(chan int)
通道是一个使用make创建的数据结构的引用。当复制或参数传递时引用同一份数据结构,零值为nil。
同种类型的通道可以比较,当引用同一通道数据时返回true,也可以和nil进行比较。
通道的两个操作:发送和接收,都使用操作符<-
,数据从右到左传递。在接收表达式中,结果未使用也是合法的。
ch <- x // 发送语句
x = <- ch // 赋值语句,接收表法式
<- ch // 接收语句,丢弃结果
可以使用close(ch)
来关闭通道。它设置一个标志位来指示通道关闭。关闭后发送将宕机(panic),关闭后接收将获取发送的值直到通道为空。此时接收操作完成并获取通道元素类型对应的零值。
make不带参数创建的为 无缓冲通道,也可以带一个可选参数表示通道容量来创建 缓冲通道。
8.4.1 无缓冲通道
无缓冲通道上的发送和执行操作将 堵塞 ,直到另一方执行相反操作。这将导致发送和接收goroutine 同步化 ,因此无缓冲通道也被称为 同步通道 。
当x即不比y早也不比y晚时称x和y并发,但顺序是不确定的,需要主动排序。
一点点错误处理知识,略。
强调通道本身及通讯发送时间等方面时,把消息叫做事件。当事件没有携带消息,只用于进行同步时,通过一个struct{}类型的通道来强调它done <- struct{}{}
8.4.2 管道
通道可以连接goroutine,使一个的输出成为另一个的输入,即管道(pipeline)。
没有一个直接的方式判断通道是否以及关闭,但可以通过接收结果间接判断。
go func(){
for {
x,ok := <- ch
if !ok {
break // 通道关闭且读完
}
// ...
}
}
关闭每一个通道不是必须的,只有通知接收方goroutine所有数据发送完毕才需要关闭通道。垃圾回收器将根据是否可访问来回收,而不是是否关闭。
关闭已关闭的通道和关闭空通道都会导致宕机。关闭通道也可以作为一个广播机制,详见8.9。
8.4.3 单向通道类型
为了文档化限制通道仅发送或仅接收的意图,Go提供了单向通道类型,chan<-
只能发送,<-chan
只能接收,违反这个原则会在编译时被检查出来。
close操作说明的是通道上没有数据再发送,所以只能关闭发送通道,关闭仅接收通道会在编译时报错。
赋值操作或者参数调用可以把双向通道转化为单向通道,但反过来不行。
8.4.4 缓冲通道
可以通过cap(ch)
获取缓冲区容量,len(ch)
获取通道内元素个数。
MARK
一点关于goroutine泄露的内容,不是很懂。
最后介绍一点缓冲和无缓冲的区别,用流水线作例子。有点似懂非懂。
8.5 并行循环
MARK
看是看懂了好像,但找不出重点。以后需要些并行循环时再回来看看吧。
8.6 示例:并发的Web爬虫
MARK
提了一下避免死锁,但没有展开,我也不知道为什么会导致死锁
之后介绍并行度不是越高越好,总有各种限制。
使用容量为n的缓冲通道建立并发原语,称为 计数信号量。每一个孔宪草表示一个令牌,保证没有接收操作时最多由n个发送。
另一种方法是通过n个长期存活的goroutine调用,保证最多只有n个并发。
8.7 使用select多路复用
select多路复用示例:
select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default:
// ...
}
select将保持等待直到出现通信,然后进行这次通信并执行对应语句。
如果没有对应情况将永远等待。可以设置一个<-time.After(...)
来实现超时退出。
如果多个情况同时满足,select将随机选择一个。
此外还介绍了计时器time.tick
怎么避免goroutine泄露。
可以使用select实现非阻塞通信。select的default
用于指定没有其他通信发生时立即执行的动作,这是一个非阻塞的动作,重复这个动作称为对通道轮询。
8.8 示例:并发目录遍历
进一步介绍并发的写法,引入select,time.Tick,break loop,sync.WaitGroup,计数信号量等。
8.9 取消
可以创建一个取消通道,再上面不发送任何值,关闭表示取消该goroutine。同时定义一个工具函数cancelld,被调用时检测或轮询取消状态。
var done = make(chan struct{})
func cancelled() bool {
select {
case <-done:
return true
default:
return false
}
}
创建一个goroutine,堵塞地等待触发条件,如果满足条件就通过关闭goroutine来关闭done通道来广播取消事件。
// 当检测到输入时取消遍历
go func() {
os.Stdin.Read(make([]byte,1)) // 读一个字节
close(done)
}
在主goroutine添加关闭通道后的处理:
for{
select{
case <-done:
// 处理一些待完成任务,相当于退出函数
return
}
}
让子goroutine在开始时轮询取消状态,如果已被取消,则什么都不做立即返回。
go func ... {
if cancelled(){
return
}
// 正常子goroutine内容
}
MARK:
最后介绍一点程序退出后的清理,这部分没看懂。
8.10 示例:聊天服务器
MARK:
暂略
第9章 使用共享变量实现并发
9.1 竞态
考虑一个在串行程序中正确工作的函数,如果它在并发调用时仍能正确工作,那么这个函数是并发安全的。
程序并发安全不需要每一个类型都是并发安全的。对于绝大部分变量,如果要回避并发访问,要么限制变量只存在一个goroutine内,要么维护一个更高层的互斥不变量。
导出的包级别函数通常可以认为是并发安全的,因为包级别变量无法限制在一个gouroutine内,修改这些变量的函数必须采用互斥机制。
数据竞态:两个goroutine并发读写同一个变量且至少一个是写入。
- 不要修改变量。
e.g. 直接初始化全局变量且不再修改。 - 避免从多个goroutine访问同一个变量。
必须使用通道来发送查询请求或更新变量。
“不要通过共享内存来通信,而应该通过通信来共享内存”
即使变量不能受限于单个goroutine,也可以借助通道传递变量地址实现变量共享,即串行受限。 - 同一时间只有一个goroutine可以访问变量,即互斥机制。见下节。
9.2 互斥锁 sync.Mutex
可以使用容量为1的通道保证同时只有一个goroutine访问共享变量,即二进制信号量。
var(
sema = make(chan struct{}, 1)
v int
)
func Use(){
sema <- struct{}{} // 获取令牌
defer <- sema // 释放令牌
// 使用变量v
}
sync
包有一个单独的Mutex
变量支持互斥锁模式。
var(
mu = sync.Mutex
v int
)
func Use(){
mu.Lock() // 获取令牌
defer mu.Unlock() // 释放令牌
// 使用变量v
}
Go语言的互斥量是不可再入的(不能对已上锁互斥量再次上锁),所以嵌套申请互斥锁将导致死锁。
MARK
这里关于go互斥量不可再入的理由和解决方法没完全懂。
9.3 读写互斥锁:sync.RWMutex
多个读操作可以安全并发运行,但写操作需要独享的访问权限,这种锁称为多读单写锁,可以通过go的sync.RWMutex
实现。
var mu sync.RWMutex
var v int
func Read() int {
mu.RLock() // 读锁
defer mu.RUnlock()
return v
}
RLock
只能用于无写操作,但逻辑上只读的操作不一定没有写操作,如递增计数器或更新缓存。如果不确定是否有写操作仍以使用Lock
。
因为需要更复杂的内部工作,只在竞争激烈时RWMutex
才有优势,否则它比普通的互斥锁慢。
9.4 内存同步
MARK
介绍了一种由内存的本地缓存可能引发的问题,结论是尽量限制变量在单个goroutine内或使用互斥锁来避免数据竞态。
9.5 延迟初始化:sync.Once
对于那些一次性初始化以及初始化非必须的变量,如果直接用判v!=nil
是并发不安全的,因为初始化时首先将v使用make构造再进行赋值操作,在这期间判得v!=nil但读不到正确值。
如果直接使用Mutex无法并发读,使用RWMutex实现并发安全较为复杂。
可以使用sync.Once
实现这一并发安全,它包含一个布尔变量和一个互斥量,前者记录初始化是否完成,后者保护数据。
var once sync.Once
var v map[string]int
func InitV(){...}
func GetV(s string)(int){
once.Do(InitV)
return v[s]
}
9.6 竞态检测器
go工具链有一个动态分析工具:竞态检测器(race detector),将-race
参数加到go build
,go run
,go test
命令中即可使用。
竞态检测器只能检测到运行时发生的竞态,所以需要晚完善的测试用例以获得最佳效果。
9.7 示例:并发非阻塞缓存
TODO:
看困了,以后再看。
有个singleflight
包好像也能实现这个需求。
9.8 goroutine 与线程
9.8.1 可增长的栈
OS线程的栈内存是固定大小(通常为2MB)。
goroutine的栈内存是可增长的,初始通常为2KB,最大可达1GB。
9.8.2 goroutine 调度
线程由内核调度,通过硬件时钟触发,线程切换需要一个完整的上下文切换,成本高操作慢。
goroutine由Go自己的调度器调度,使用m:n
调度(复用/调度m个goroutine到n个OS线程),通过特定的go语言结构触发,不需要切换到内核语境,成本低很多。
9.8.3 GOMAXPROCS
Go调度器使用GOMAXPROCS
参数确定使用多少个OS线程同时执行Go代码,默认值是机器CPU数量(m:n 调度中的n)。休眠或通道通信阻塞的goroutine不占用线程。I/O阻塞和其他系统调用或非Go函数的goroutine需要独立OS线程,但不计算在GOMAXPROCS内
。
可以用GOMAXPROCS
环境变量或runtime.GOMAXPROCS
函数显式控制该参数。
9.8.4 goroutine 没有标识
在大部分支持多线程的操作系统和编程语言中,线程都有一个独特的标识(通常是一个整数或指针),本质上是一个全局map,能够轻松构建一个线程的局部储存。
goroutine在设计中没有提供这个标识,这是为了避免线程局部存储被滥用,鼓励一种更简单的编程风格。
第10章 包和 go 工具
10.1 引言
10.2 导入路径
10.3 包的声明
每个go源文件开头都需要进行包声明,作为被其他包引入时的标识符。
10.4 导入声明
导入多个包时可以使用多个import声明导入,也可以使用一个import (...)
一次性导入多个包。一般后者更常见。
import (
"fmt"
"os"
)
导入两个同名包时,需要指定别名避免冲突,也就是 重命名导入。
即使没有包名冲突,也可以使用重命名导入来简化包名或者避免局部变量冲突。
import (
"crypto/rand"
mrand "math/rand"
)
10.5 空导入
可以通过将别名设置为_
的方式实现空白导入,例如import _ "image/png" // 注册png解码器
。多数情况下,它用来实现一个编译时的机制,开启主程序中可选的特性。
比起图片格式解码器,数据库驱动程序中用到的空白导入对我而言更为熟悉。
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // 添加 MySQL 支持
_ "github.com/lib/pq" // 添加 Postgres 支持
)
db, err = sql.Open("postgres", dbname) // 0K
db, err = sql.Open("mysql", dbname) // 0K
db, err = sql.Open("sqlite3", dbname) //返回错误消息:unknown driver“sqlite3"
=
10.6 包及其命名
10.7 go 工具
10.7.1 工作空间的组织
现在都用go module不用gopath了,略。
10.7.2 包的下载
10.7.3 包的构建
10.7.4 包的文档化
go doc
命令可以输出包的注释和内部声明,也可以输出包成员或方法的注释。
godoc
在新版本不再内置,通过 go install golang.org/x/tools/cmd/godoc@latest
安装。
运行命令godoc -http=:6060 -play -index
,之后访问localhost:6060
,就可以以互相链接的网页的形式查看注释文档了。官方的pkg.go.dev
也是用的这种方式。
10.7.5 内部包
go build
工具会特殊对待internal
路径下的包,即 内部包。内部包只能被internal父目录为根的目录树下的包导入。
10.7.6 包的查询
go list
工具将给出可用包的信息。参数也可以包含...
通配符,将匹配任意字串。go list
用法如下:
go list <包名>
判断包是否存在于工作空间,如果存在将输出导入路径。go list ...
枚举工作空间中的所有包go list <路径>/...
一个指定子树中的所有包go list ...<关键字>...
查询路径包含关键字的包
go list
命令获取的是完整元数据,而不仅仅是导入路径。可以使用-json
,-f
等标记定制输出内容和格式。
第11章 测试
11.1 go test 工具
_test.go
结尾的文件不是go build
编译的目标,而是go test
编译的目标。
*_test.go
中有三种特殊函数:
- 功能测试函数:
Test前缀,检测逻辑正确性,结果为PASS或FAIL - 基准测试函数:
Benchmark开头,测试操作性能,结果为平均执行时间 - 示例函数:
Example开头,提供机器检查过的文档
11.2 Test 函数
功能测试函数必须以Test开头,后缀名以大写字母开头,如下是一个测试函数示例:
type TestCase struct{
input interface{}
want interface{}
}
func TestFunc(t *testing.T) {
var tests = []TestCase
// 初始化测试用例
for test := range tests{
if got := Func(test.input); got != test.want{
t.Errorf("Func(%v) = %v , want %v",test.input, got, test.want)
}
}
}
go test
在不指定包参数时以当前目录所在包为参数。可选的命令行参数如下:
-v
输出包中每个测试用例的名称和执行时间-run
= 正则表达式
只运行测试函数名称匹配给定模式的函数
11.2.1 随机测试
要确定随机输入的正确输出有两种方法:
- 使用低效但清晰的算法,比较结果是否一致。
- 构建符合模式的输入,从而知道对应输出。
使用rand.New(rand.NewSource(time.Now().UnixNano()))
生成随机数种子,进而生成随机输入。在保存测试用例的输入时,比起保存整个输入数据结构,记录随机数种子更简便。
11.2.2 测试命令
go test除了测试代码也可用于测试命令。
11.2.3 白盒测试
黑盒测试:假设测试者仅了解公开API和文档,而内部逻辑不透明。
百合测试: 可以访问包的内部函数和数据结构,并且可以做一些常规用户无法做到的观察和改动。
在进行白盒测试时,可以保存全局变量或方法后更新其值,最后使用defer恢复,从而实现更精细的调试。
// develop file
var globalVar := 10
func globalFunc( ) {
//...
}
targetFunc( ) {
// used globalVar
// used globalFunc
}
// test file
func testFunc(t testing.T){
// 保存原变量和方法
savedVar = globalVar
savedFunc = globalFunc
// 退出时恢复原值
defer func ( ) {
globalVar = savedVar
globalFunc = savedFunc
}
// 定义用于测试的变量和方法
globalVar = newVar
globalFunc = newFunc
// 测试targetFunc,将使用赋的新值
}
11.2.4 外部测试包
11.2.5 编写有效测试
11.2.6 避免脆弱的测试
TODO:
不是很懂
11.3 覆盖率
11.4 Benchmark 函数
11.5 性能刨析
11.6 Example 函数
OMIT:
略 需要再查
第12章 反射
12.1 为什么使用反射
有时需要写一个统一处理各种类型的函数(如Printf),但这些类型可能:
- 无法共享同一个接口
- 布局位置
- 在设计函数时不存在
12.2 reflect.Type 和 reflect.Value
reflect.TypeOf
接受interface{}
参数,并将接口中动态类型以reflect.Type
形式返回。
t := reflect.TypeOf(3) // reflect.Type
fmt.Println(t) // int
fmt.Println(t.String()) // int
注意:reflect.TypeOf
总是返回具体类型,而不是接口类型。所以可以使用%T
直接获取接口的具体类型:
fmt.Printf("%T\n", 3) // <int Value>
reflect.ValueOf
接受interface{}
参数,并将接口中动态值以reflect.Value
形式返回。
这里说返回值是具体值,但 也可以包含一个接口值。在接口部分我们了解到接口值包括动态类型和动态值,即reflect.Value
也是包含动态值和动态类型的,调用Value的Type方法将类型以 reflect.Type
返回。
reflect.ValueOf
的逆操作是``reflect.Value.Interface方法,返回一个
interface{}接口值,与
reflect.Value`值相同。
var i interface{} = 3
fmt.Println(i) // 3
fmt.Printf("%v\n", i) // 3
fmt.Printf("%T\n", i) // int
v := reflect.ValueOf(i)
fmt.Println(v) // 3
fmt.Println(v.Int()) // 3
fmt.Println(v.Type()) // int
fmt.Println(v.String()) // <int Value>
x := v.Interface()
fmt.Println(x) // 3
fmt.Printf("%d\n", x.(int)) // 3
fmt.Printf("%T\n", x.(int)) // int
reflect.Value
和interface{}
都可以包含任意值,区别是空接口隐藏了值的信息,需要先进行类型断言否则操作有限,而reflect.Value
有很多方法去分析值而不用知道类型。
可以使用reflect.Value
的kind
方法来区分类型:
- 基础类型:Bool,String,以及各种数字类型
- 聚合类型:Array和Struct
- 引用类型:Chan,Func,Prt,Slice和Map
- 接口类型:Interface
- Invalid类型:表示没有任何值,如
reflect.Value
的零值
12.3 Display: 一个递归的值显示器
12.4 示例: 编码S表达式
OMIT: 示例略
12.5 使用 reflect.Value 来设置值
一个变量是一个可寻址的存储区域,其中包含一个值,并且可以通过这个地址来更新。对reflect.Value
也有类似区分,只有可寻址的Value才能用于更新变量。
调用reflect.ValueOf(&x).Elem()
来获得任意变量x的可寻址的Value值。对slice的e[i]也有reflect.ValueOf(e).Index(i)
,得到的Value也是可寻址的,有一定的指针基础会更好理解。
通过reflect.Value.CanAddr()
可以询问Value变量是否可寻址。
从一个可寻址的reflect.Value()
获取变量有如下三步:
- 调用Addr(),返回一个Value,包含指向变量的指针。
- 调用Interface(),返回包含该指针的interface{}值。
- 使用类型断言将接口转换成普通指针。
然后就可以使用这个指针来更新变量了。
也可以使用reflect.Value.Set()
直接从可寻址的Value来更新变量。
x := 2
d := reflect.ValueOf(&x).Elem() // d refers to the variable x
px := d.Addr().Interface().(*int) // px := &x
*px = 3 // x=3
fmt.Println(x) // 3
d.Set(reflect.ValueOf(4))
fmt.Println(x) // 4
12.6 示例:解码S表达式
OMIT: 示例略。
12.7 访问结构体字段标签
主要介绍通过反射去解析字段标签的底层实现,例如解析http请求并将参数绑定到结构体上。
大致过程是通过Field和Tag等方法解析结构体中的字段和标签,通过kind方法分类http请求中的数据类型选择合适的方法赋值。现在用的web框架都把这些底层逻辑封装好了,有需要时再翻书。
12.8 显示类型的方法
reflect.Type
和reflect.Value
都有Method(i)
方法,前者返回reflect.method
类型的实例,描述方法的名称和类型等。后者返回一个reflect.Value
类型的方法值,可以通过reflect.Value.Call
进行调用。
12.9 注意事项
- 基于反射的代码很脆弱。编译时能报错的错误,使用反射只有在执行时才会崩溃报告。
- 将反射的使用完整地封装在包内。
- 包的API中避免使用reflect.Value,而是特定类型。
- 每个危险操作前做额外的动态检查。
- 反射降低了自动重构和分析工具的安全性与准确度,因为它们无法检测到类型信息。类型也算文档的一直,而反射相关操作无法做静态类型检查。
- 基于反射的函数比特定类型优化的函数慢一两个数量级。
第13章 低级编程
很多实现细节无法通过Go程序访问,通过隐藏底层细节,Go程序有着更好的可读性和可移植性。但有时也可以放弃一些有益的保证去追求高性能,和其他语言交互和实现纯Go无法描述的函数。
这一章主要是读一下扩宽视野,掌握一些与操作系统底层交互的方式,普通程序用不上低级编程。
有C/C++基础看这些内容就跟回家了一样。
13.1 unsafe.Sizeof、Alignof 和 Offsetof
关于内存对齐的知识和方式。个人感觉平时编程其实不用关注内存对齐,关注那点内存提升不如提升可读性。真搞高性能要求的开发之类还是用C。
13.2 unsafe.Pointer
指针相关内容,C++的指针本质上只是一个内存地址,可以往里面写入任何数据。Go原本的指针做了个类型封装,而unsafe.Pointer相当于解除了这个限制。使用unsafe.Pointer还有很多注意事项,有需要再回来翻一下。
13.3 示例:深度相等
reflect.DeepEqual
判断两个变量的值是否"深度"相等。对基本数据类型使用==进行比较,对组合类型逐层深入比较。
注意:对nil和空结构体将判断为不相等。如 reflect.DeepEqual(nil,[]string{})
结果为false。
OMIT: 示例略。
13.4 使用cgo调用C代码
简单介绍cgo的用法,有需要再回来翻书。
13.5 关于安全的注意事项
高级语言隔离了许多底层细节,让我们可以编写安全健壮的代码并在任何操作系统上运行。
包unsafe可以穿透这层隔离去使用一些特性或实现更高性能,代价则是可移植性和安全性,需要自己评估风险。大多数时候我们都不需要用到unsafe包,如果确实需要使用,则尽可能限制在小范围内。
完结撒花🎉
2025.1.18