Go 语言第一课¶
约 6059 个字 328 行代码 预计阅读时间 24 分钟
- 源文件命名全小写,无分隔符
- 只有首字母为大写的标识符才是导出的(Exported),才能对包(package)外的代码可见
- Go Module 的核心是一个名为 go.mod 的文件,在这个文件中存储了这个 module 对第三方依赖的全部信息。
- go.sum 文件记录了 Module 的直接依赖和间接依赖包的相关版本的 hash 值,用来校验本地包的真实性
- internal 目录的出现相当于在一个项目中设定了「包的访问边界」,进一步强调内部实现与外部接口的区别。对于不想暴露给外部用户使用的公共函数或类型,可以将其放置在 internal 包中,从而确保其只服务于项目自身的内部逻辑。
Moudle¶
将基于当前项目创建一个 Go Module,通常有如下几个步骤:
第一步,通过 go mod init 创建 go.mod 文件,将当前项目变为一个 Go Module;
第二步,通过 go mod tidy 命令自动更新当前 module 的依赖信息;
第三步,执行 go build,执行新 module 的构建。
这就是 Go 的“语义导入版本”机制,也就是说通过在包导入路径中引入主版本号的方式,来区别同一个包的不兼容版本,这样一来我们甚至可以同时依赖一个包的两个不兼容版本:
添加依赖¶
现在 import 中导入包路径
- 手动获取:go get
$go get github.com/google/uuid
go: downloading github.com/google/uuid v1.3.0
go get: added github.com/google/uuid v1.3.0
- 使用 go mod tidy 命令,在执行构建前自动分析源码中的依赖变化,识别新增依赖项并下载它们:
$go mod tidy
go: finding module for package github.com/google/uuid
go: found github.com/google/uuid in github.com/google/uuid v1.3.0
升级或降级依赖的版本¶
查询有多少个发布版本
$go list -m -versions github.com/sirupsen/logrus
github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 v1.4.0 v1.4.1 v1.4.2 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1
使用 go mod tidy 来帮助我们降级,但前提是首先要用 go mod edit 命令,明确告知我们要依赖 v1.7.0 版本,而不是 v1.8.1,这个执行步骤是这样的:
$go mod edit -require=github.com/sirupsen/logrus@v1.7.0
$go mod tidy
go: downloading github.com/sirupsen/logrus v1.7.0
添加一个主版本号大于 1 的有依赖¶
需要使用“语义导入版本”机制,在声明它的导入路径的基础上,加上版本号信息。我们以“向 module-mode 项目添加 github.com/go-redis/redis 依赖包的 v7 版本”为例,看看添加步骤。
首先,我们在源码中,以空导入的方式导入 v7 版本的 github.com/go-redis/redis 包:
package main
import (
_ "github.com/go-redis/redis/v7" // “_”为空导入
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
然后跑一下
特殊情况:使用 vendor¶
Go 提供了可以快速建立和更新 vendor 的命令,我们还是以前面的 module-mode 项目为例,通过下面命令为该项目建立 vendor:
$go mod vendor
$tree -LF 2 vendor
vendor
├── github.com/
│ ├── google/
│ ├── magefile/
│ └── sirupsen/
├── golang.org/
│ └── x/
└── modules.txt
我们看到,go mod vendor 命令在 vendor 目录下,创建了一份这个项目的依赖包的副本,并且通过 vendor/modules.txt 记录了 vendor 下的 module 以及版本。
如果我们要基于 vendor 构建,而不是基于本地缓存的 Go Module 构建,我们需要在 go build 后面加上-mod=vendor 参数。
在 Go 1.14 及以后版本中,如果 Go 项目的顶层目录下存在 vendor 目录,那么 go build 默认也会优先基于 vendor 构建,除非你给 go build 传入-mod=mod 的参数。
Go 程序的执行次序¶
- Go 包的初始化函数:
init()
- 先传递给 Go 编译器的源文件中的 init 函数,会先被执行;而同一个源文件中的多个 init 函数,会按声明顺序依次执行。
- Go 程序中我们不能手工显式地调用 init,否则就会收到编译错误 记住 Go 包的初始化次序并不难,你只需要记住这三点就可以了:
- 依赖包按“深度优先”的次序进行初始化;
- 每个包内按以“常量 -> 变量 -> init 函数”的顺序进行初始化;
-
包内的多个 init 函数按出现次序进行自动调用。 2. Go 应用的入口函数:
main.main()
-
main 包中的 main 函数,也就是 main.main,它是所有 Go 可执行程序的用户层执行逻辑的入口函数。
实战项目:图书管理 API 服务¶
Go 标准库中的 http 包:import "net/http"
- ListenAndServe 函数:建立端口与服务之间的联系
- HandleFunc 函数:分发路由
把这个服务大体拆分为两大部分,一部分是 HTTP 服务器,用来对外提供 API 服务;另一部分是图书数据的存储模块,所有的图书数据均存储在这里。
同时,这是一个以构建可执行程序为目的的 Go 项目,我们参考 Go 项目布局标准一讲中的项目布局,把这个项目的结构布局设计成这样:
├── cmd/
│ └── bookstore/ // 放置bookstore main包源码
│ └── main.go
├── go.mod // module bookstore的go.mod
├── go.sum
├── internal/ // 存放项目内部包的目录
│ └── store/
│ └── memstore.go
├── server/ // HTTP服务器模块
│ ├── middleware/
│ │ └── middleware.go
│ └── server.go
└── store/ // 图书数据存储模块
├── factory/
│ └── factory.go
└── store.go
Main 包¶
创建图书数据存储模块实例:用工厂模式创建,工厂只负责注册和创建,具体实现接口是要在 internal 中实现的。
创建 http 服务实例:在 server/server.go
图书数据存储模块(store)¶
在 store/store.go 中实现:
- 抽象数据结构类型 Book
- 针对 Book 的接口类型 Store
工厂负责实例化
- Register 函数:让各个实现 Store 接口的类型可以把自己“注册”到工厂中来。
- 实例化:只需要调用 factory 包的 New 函数,再传入期望使用的图书存储实现的名称,就可以得到对应的类型实例了。
Http 服务模块(server)¶
HTTP 服务模块的职责是对外提供 HTTP API 服务,处理来自客户端的各种请求,并通过 Store 接口实例执行针对图书数据的相关操作。
定义了一个 BookStoreServer 类型如下:
我们看到,这个类型实质上就是一个标准库的 http.Server,并且组合了来自 store.Store 接口的能力。
类型实例:
// server/server.go
func NewBookStoreServeraddr string, s store.Store) *BookStoreServer {
srv := &BookStoreServer{
s: s,
srv: &http.Server{
Addr: addr,
},
}
router := mux.NewRouter()
router.HandleFunc("/book", srv.createBookHandler).Methods("POST")
router.HandleFunc("/book/{id}", srv.updateBookHandler).Methods("POST")
router.HandleFunc("/book/{id}", srv.getBookHandler).Methods("GET")
router.HandleFunc("/book", srv.getAllBooksHandler).Methods("GET")
router.HandleFunc("/book/{id}", srv.delBookHandler).Methods("DELETE")
srv.srv.Handler = middleware.Logging(middleware.Validating(router))return srv
}
NewBookStoreServer 接受两个参数:
- 服务地址参数 addr:通过过这个地址和服务器建立连接,并发起 Http 请求
- 接口类型参数:Go 语言是面向接口编程,只要某个类型实现了这些方法,他就满足了这个接口。
路由管理:采用第三方包 github.com/gorilla/mux
json.Marshal
JSON 序列化,转换为 JSON 格式的字节切片 ([]byte
)。mux.Vars(req)
:获取当前请求中所有路由变量的函数,它接收http.Request
类型的参数,然后返回一个map[string]string
类型的字典,其中键是路由变量的名称,值就是对应的变量具体的值。
通用的 Http 处理函数(middleware)¶
这里的两个 middleware,也就是 Logging 与 Validating 函数的实现:
// server/middleware/middleware.go
func Logging(next http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
log.Printf("recv a %s request from %s", req.Method, req.RemoteAddr)
next.ServeHTTP(w, req)
})
}
func Validating(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
contentType := req.Header.Get("Content-Type")
mediatype, _, err := mime.ParseMediaType(contentType)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if mediatype != "application/json" {
http.Error(w, "invalid Content-Type", http.StatusUnsupportedMediaType)
return
}
next.ServeHTTP(w, req)
})
}
Logging 函数主要用来输出每个到达的 HTTP 请求的一些概要信息,而 Validating 则会对每个 http 请求的头部进行检查,检查 Content-Type 头字段所表示的媒体类型是否为 application/json。
变量¶
变量声明的语法是 “名字在前,类型在后”: var a int = 1
- “var x int” 可以理解为“x 是一个 int 类型的变量”
Go 编译器会根据右侧变量初值自动推导出变量的类型,并给这个变量赋予初值所对应的默认类型
我们更青睐下面这样的形式:
声明但延迟初始化。
对于声明时并不立即显式初始化的包级变量,我们可以使用下面这种通用变量声明形式:
我们知道,虽然没有显式初始化,Go 语言也会让这些变量拥有初始的“零值”。
字面值¶
Go 1.13 版本还支持在字面值中增加数字分隔符“_”,分隔符可以用来将数字分组以提高可读性。比如每 3 个数字一组,也可以用来分隔前缀与字面值中的第一个数字:
a := 5_3_7 // 十进制: 537
b := 0b_1000_0111 // 二进制位表示为10000111
c1 := 0_700 // 八进制: 0700
c2 := 0o_700 // 八进制: 0700
d1 := 0x_5c_6d // 十六进制:0x5c6d
Go 语言中的整型的二进制表示采用 2 的补码形式
字符串¶
我们不能为一个字符串类型变量进行二次赋值。
什么意思呢?我们看看下面的代码就好理解了:
在这段代码中,我们声明了一个字符串类型变量 s。当我们试图通过下标方式把这个字符串的第一个字符由 h 改为 k 的时候,我们会收到编译器错误的提示:字符串是不可变的。但我们仍可以像最后一行代码那样,为变量 s 重新赋值为另外一个字符串。
通过一对 反引号 原生支持构造“所见即所得”的原始字符串(Raw String)。而且,Go 语言原始字符串中的任意转义字符都不会起到转义的作用。
比如下面这段代码:
var s string = ` ,_---~~~~~----._
_,,_,*^____ _____*g*\"*,--,
/ __/ /' ^. / \ ^@q f
[ @f | @)) | | @)) l 0 _/
\/ \~____ / __ \_____/ \
| _l__l_ I
} [______] I
] | | | |
] ~ ~ |
| |
| |`
fmt.Println(s)
我们可以使用两个视角来看待 Go 字符串的组成,一种是字节视角。Go 字符串是由一个可空的字节序列组成,字节的个数称为字符串的长度;另外一种是字符视角。Go 字符串是由一个可空的字符序列构成。Go 字符串中的每个字符都是一个 Unicode 字符。
- 通过常规 for 迭代与 for range 迭代所得到的结果不同,常规 for 迭代采用的是字节视角;而 for range 迭代采用的是字符视角;
常量¶
Go 语言在常量方面的创新包括下面这几点:
- 支持无类型常量;
- 支持隐式自动转型;
- 可用于实现枚举。
无类型常量¶
有类型常量与变量混合在一起进行运算求值的时候,也必须遵守类型相同这一要求,否则我们只能通过显式转型才能让上面代码正常工作,比如下面代码中,我们就必须通过将常量 n 显式转型为 int 后才能参与后续运算:
type myInt int
const n myInt = 13
const m int = int(n) + 5 // OK
func main() {
var a int = 5
fmt.Println(a + int(n)) // 输出:18
}
那么在 Go 语言中,只有这一种方法能让上面代码编译通过、正常运行吗 ?当然不是,我们也可以使用 Go 中的无类型常量来实现,你可以看看这段代码:
你可以看到,在这个代码中,常量 n 在声明时并没有显式地被赋予类型,在 Go 中,这样的常量就被称为无类型常量(Untyped Constant)。
Go 编译器会将 n 隐式转化 为 myInt 类型,从而可以及进行计算
枚举类型¶
如果我们要略过 iota = 0,从 iota = 1 开始正式定义枚举常量,我们可以效仿下面标准库中的代码:
在这个代码里,我们使用了空白标识符作为第一个枚举常量,它的值就是 iota。
数组¶
如果要显式地对数组初始化,我们需要在右值中显式放置数组类型,并通过大括号的方式给各个元素赋值(如下面代码中的 arr2)。当然,我们也可以忽略掉右值初始化表达式中数组类型的长度,用“…”替代,Go 编译器会根据数组元素的个数,自动计算出数组长度(如下面代码中的 arr3):
var arr2 = [6]int {
11, 12, 13, 14, 15, 16,
} // [11 12 13 14 15 16]
var arr3 = [...]int {
21, 22, 23,
} // [21 22 23]
fmt.Printf("%T\n", arr3) // [3]int
切片¶
在实际工程中
- 不要直接修改 arr:
arr[0] = xxx
- 先复制一份,修改完之后,在复制回去。。
相比较数组来说,切片不需要在声明时指定长度。
- 切片的长度不是固定的,类似于 vector
- 内置函数:
len(sums)
获取长度nums = append(nums, 7)
添加元素
指针切片¶
data1 存的是指向 field 数据的 地址(指针)
切片的底层实现¶
- array: 是指向底层数组的指针;
- len: 是切片的长度,实际存储了多少元素
- cap: 是底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值。
切片的初始化¶
-
通过 make 创建切片
如果没有在make中指定cap参数,那么底层数组长度cap就等于len,比如:go sl := make([]byte, 6) // cap = len = 6
-
数组的切片化
通过数组 arr 拿到的切片,切片的底层数组还是 arr,所以对切片的修改会直接改变 arr 中的值。
切片的动态扩容¶
扩容 == 换一个新数组 + 复制之前的数据: 新数组建立后,append 会把旧数组中的数据拷贝到新数组中,之后新数组便成为了切片的底层数组,旧数组会被垃圾回收掉。
比如基于一个已有数组建立的切片,一旦追加的数据操作触碰到切片的容量上限(实质上也是数组容量的上界),切片就会和原数组解除“绑定”,后续对切片的任何修改都不会反映到原数组中了。
复合数据类型¶
Map¶
Map 类型要保证 key 的唯一性。
- Key 的类型必须 必须支持“==”和“!=”两种比较操作符。
- 函数类型、map 类型自身,以及切片类型是不能作为 map 的 key 类型的。
Map 的声明和初始化¶
如果只用 var 来声明一个 map 变量 var m map[string]int
- m 此时为零值(nil)
- Map 中无法 “零值可用”,必须要进一步的对他进行分配空间和初始化操作才可以
-
方法一:使用复合字面值初始化 map 类型变量。
-
方法二:使用make为map类型变量进行显式初始化。 我们可以为map类型变量指定键值对的初始容量,但无法进行具体的键值对赋值,就像下面代码这样:
go m1 := make(map[int]string) // 未指定初始容量 m2 := make(map[int]string, 8) // 指定初始容量为8
Map 的基本操作¶
- 插入
m[1] = "aa"
- 获取长度:
len(m)
- 查找
m := make(map[string]int)
v, ok := m["key1"]
if !ok {
// "key1"不在 map 中
}
// "key1"在 map 中,v 将被赋予"key1"键对应的 value
-
删除元素 delete函数是从map中删除键的唯一方法。即便传给delete的键在map中并不存在,delete函数的执行也不会失败,更不会抛出运行时的异常。
-
遍历 map 对同一map做多次遍历的时候,每次遍历元素的次序都不相同 当然更地道的方式是这样的:
Map 在传参的时候,是 引用传递,并不是 值传递。
代码块与作用域¶
位于控制语句隐式代码块中的标识符的作用域划分。我们以下面这个 if 条件分支语句为例来分析一下:
func bar() {
if a := 1; false {
} else if b := 2; false {
} else if c := 3; false {
} else {
println(a, b, c)
}
}
这是一个复杂的“if - else if - else”条件分支语句结构,根据我们前面讲过的隐式代码块规则,我们将上面示例中隐式代码块转换为显式代码块后,会得到下面这段等价的代码:
func bar() {
{ // 等价于第一个if的隐式代码块
a := 1 // 变量a作用域始于此
if false {
} else {
{ // 等价于第一个else if的隐式代码块
b := 2 // 变量b的作用域始于此
if false {
} else {
{ // 等价于第二个else if的隐式代码块
c := 3 // 变量c作用域始于此
if false {
} else {
println(a, b, c)
}
// 变量c的作用域终止于此
}
}
// 变量b的作用域终止于此
}
}
// 变量a作用域终止于此
}
}
自定义新类型¶
类型定义¶
T1 和 T2 点的顶层类型都是 int,可以通过 显式的类型转换 进行赋值。
类型别名¶
结构体¶
package book
type Book struct {
Title string // 书名
Pages int // 书的页数
Indexes map[string]int // 书的索引
}
- 字段首字母大写:可以导出到包外
- 空标识符 _ 作为结构体字段名称主要是为了占位,或者利用他的 init 函数
- 不可以循环嵌套,在 T 结构体中还存在 T 类型的属性(编译器尝试为 T 分配内存,而 T 又包含自身类型 t,从而形成无限递归,最终无法确定结构体的大小。)
- 但我们却可以拥有自身类型的指针类型、以自身类型为元素类型的切片类型,以及以自身类型作为 value 类型的 map 类型的字段,比如这样:
结构体初始化¶
用“field:value”形式复合字面值,对上面的类型T的变量进行初始化看看:
特定的构造函数:NewXXX
- 实用于参数中存在非导出字段,在 NewXXX 内部进行初始化
- 一般是返回指针类型。
对其结构体字段¶
整个计算过程分为两个阶段。第一个阶段是对齐结构体的各个字段。
首先,我们看第一个字段 b 是长度 1 个字节的 byte 类型变量,这样字段 b 放在任意地址上都可以被 1 整除,所以我们说它是天生对齐的。我们用一个 sum 来表示当前已经对齐的内存空间的大小,这个时候 sum=1;
接下来,我们看第二个字段 i,它是一个长度为 8 个字节的 int64 类型变量。按照内存对齐要求,它应该被放在可以被 8 整除的地址上。但是,如果把 i 紧邻 b 进行分配,当 i 的地址可以被 8 整除时,b 的地址就无法被 8 整除。这个时候,我们需要在 b 与 i 之间做一些填充,使得 i 的地址可以被 8 整除时,b 的地址也始终可以被 8 整除,于是我们在 i 与 b 之间填充了 7 个字节,此时此刻 sum=1+7+8;
再下来,我们看第三个字段 u,它是一个长度为 2 个字节的 uint16 类型变量,按照内存对其要求,它应该被放在可以被 2 整除的地址上。有了对其的 i 作为基础,我们现在知道将 u 与 i 相邻而放,是可以满足其地址的对齐要求的。i 之后的那个字节的地址肯定可以被 8 整除,也一定可以被 2 整除。于是我们把 u 直接放在 i 的后面,中间不需要填充,此时此刻,sum=1+7+8+2。
现在结构体 T 的所有字段都已经对齐了,我们开始第二个阶段,也就是对齐整个结构体。
我们前面提到过,结构体的内存地址为 min(结构体最长字段的长度,系统内存对齐系数)的整数倍,那么这里结构体 T 最长字段为 i,它的长度为 8,而 64bit 系统上的系统内存对齐系数一般为 8,两者相同,我们取 8 就可以了。那么整个结构体的对齐系数就是 8。
这个时候问题就来了!为什么上面的示意图还要在结构体的尾部填充了 6 个字节呢?
我们说过结构体 T 的对齐系数是 8,那么我们就要保证每个结构体 T 的变量的内存地址,都能被 8 整除。
If 条件判断¶
func doSomething() error {
if errorCondition1 {
// some error logic
... ...
return err1
}
// some success logic
... ...
if errorCondition2 {
// some error logic
... ...
return err2
}
// some success logic
... ...
return nil
}
“快乐路径” 原则:结构比较简单,失败就立即返回。
for range¶
在用 for range 修改数组的值时,如果要对数组进行修改,一定要通过下标来修改,如:sl[i] = 123,而不是直接修改 v
- For range 中的 v 是个副本,修改它不会改变原数组的值
- 只关心下标,不关心值 ```go for i := range sl { // ... }
```
-
只关心值,不关心下标
go for _, v := range sl { // ... }
-
既不关心值,也不关心下标
String 类型¶
运行这个例子,输出结果是这样的:
我们看到:for range 对于 string 类型来说,每次循环得到的 v 值是一个 Unicode 字符码点,也就是 rune 类型值,而不是一个字节,返回的第一个值 i 为该 Unicode 字符码点的内存编码(UTF-8)的第一个字节在字符串内存序列中的位置。
Switch¶
只会执行一个 分支中的语句,都没有匹配的 case 才会去 default 分支执行。
func checkWorkday(a int) {
switch a {
case 1, 2, 3, 4, 5:
println("it is a work day")
case 6, 7:
println("it is a weekend day")
default:
println("are you live on earth")
}
}
满足任何一个 case 中的任何一个条件就能进入该分支。
通过显式调用 fallthrough
可以直接进入下一个分支(不用对下一个分支做判断)
Defer¶
defer 关键字后面只能接函数(或方法),这些函数被称为 deferred 函数。
- defer 关键字后面的表达式,执行的时候就被 预计算 了,然后丢到存放 deferred 函数的栈中
- 等到 defer 所在的 函数体运行到 return 或 panic 结束的时候 按照 先进后出 的顺序进行执行
方法¶
方法区别于函数来说,多了个 接收者
- 接受者参数的底层类型:不能为指针或接口
- T 表示传引用,对 T 本身会有修改;T 表示传值,是一个副本。
- 无论是 T 类型实例,还是 T 类型实例,都既可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 T 类型的方法( 会自动转换)
- T 的方法集合是包含 T 的方法集合的,
闭包¶
func TestA() {
for i := 0; i < 10; i++ {
j := i
defer func() {
fmt.Println("j: ", &j, " val: ", j)
}()
}
}
- 闭包不是预计算值,而是捕获变量的引用(地址)
- 闭包在执行时会通过这个地址去读取当时变量的值
- 如果变量在闭包创建后被修改,闭包会看到修改后的值
嵌入字段¶
type T1 int
type t2 struct{
n int
m int
}
type I interface {
M1()
}
type S1 struct {
T1
*t2
I
a int
b string
}
我们看到,结构体 S1 定义中有三个“非常规形式”的标识符,分别是 T1、t2 和 I,这三个标识符究竟代表的是什么呢?是字段名还是字段的类型呢?这里我直接告诉你答案:它们既代表字段的名字,也代表字段的类型。我们分别以这三个标识符为例,说明一下它们的具体含义:
- 标识符 T1 表示字段名为 T1,它的类型为自定义类型 T1;
- 标识符 t2 表示字段名为 t2,它的类型为自定义结构体类型 t2 的指针类型;
- 标识符 I 表示字段名为 I,它的类型为接口类型 I。
接口¶
断言存储在接口类型变量 i 中的值的类型为 T。
Sync 中的并发锁¶
sync.WaitGroup 和 sync.Cond¶
每个 sync.WaitGroup
值在内部维护着一个计数,此计数的初始默认值为零。*sync.WaitGroup
类型有三个方法:Add(delta int)
、Done()
和 Wait()
。
就是一个阻塞队列,wg 表示阻塞队列中的数量。
- Wait 函数:等所有协程执行完毕,即阻塞队列为空
- Add 函数:向阻塞队列中添加 delta 个
- Done 函数:表示一个协程完毕,释放队列中的一个
*sync.Cond
类型有三个方法:Wait()
、Signal()
和 Broadcast()
。sync.Cond 用来处理复杂的并发协作,需要在特定条件下协调多个 goroutine 的执行顺序,适合条件同步的场景。
Waitgroup 一般用来 等待一组 goroutine 执行完
Cond 使用场景更多用来同步,“等待某个条件成立”:一个 goroutine,他需要一个前置的资源,才能够执行,不然就 wait。然后负责 cond.signal 的 goroutine,做完了某个前置的操作,然后给个信号。
sync.Once¶
一个 sync.Once
值被用来确保一段代码在一个并发程序中被执行且仅被执行一次。
读写锁¶
一个 Mutex
值常称为一个互斥锁。 一个 Mutex
零值为一个尚未加锁的互斥锁。
一个 RWMutex
值常称为一个读写互斥锁,它的内部包含两个锁:一个写锁和一个读锁。
¶
并发 Channel¶
创建 channnel¶
- Ch: int 类型的 Channel 变量
无缓冲 Channel:
- 用作信号量传递
type signal struct{}
func worker() {
println("worker is working...")
time.Sleep(1 * time.Second)
}
func spawn(f func()) <-chan signal {
c := make(chan signal)
go func() {
println("worker start to work...")
f()
c <- signal{}
}()
/*
var s chan signal
return s
// 返回一个未初始化的通道,会导致死锁
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]:
main.main()
*/
return c
}
func main() {
println("start a worker...")
c := spawn(worker)
// 等待并接受来自通道c的信号
comma, ok := <-c
fmt.Println(comma, ok)
fmt.Println("worker work done!")
}
发送与接收¶
Go 提供了 <-
操作符用于对 channel 类型变量进行发送与接收操作:
使用操作符 <-
,我们还可以声明只发送 channel 类型(send-only)和只接收 channel 类型(recv-only),我们接着看下面这个例子:
ch1 := make(chan<- int, 1) // 只发送channel类型
ch2 := make(<-chan int, 1) // 只接收channel类型
<-ch1 // invalid operation: <-ch1 (receive from send-only type chan<- int)
ch2 <- 13 // invalid operation: ch2 <- 13 (send to receive-only type <-chan int)
- 箭头指向哪,表示谁接收
实战项目:Goroutine 池 ---- workerpool¶
workerpool 的实现主要分为三个部分:
- pool 的创建与销毁;
- pool 中 worker(Goroutine)的管理;
- task 的提交与调度。
泛型¶
type List[T any] interface {
Add(idx int,t T)
Append(t T)
}
func UseList() {
var l List[int]
// l.Append("string") 只能用 int 类型的
l.Append(1)
结构体,方法中都可以用泛型。
泛型约束:
type Number interface {
int | int64 | float64 | int32
}
func Sum(T NUmber](vals...T) T {
var res T
for _,val := range vals {
res = res + val
}
return res
}
func main() {
println(Sum[int](1,2,3))
}
Created: February 15, 2025