语言教程看官方文档更合适,本文仅做学习记录。
包、变量和函数
每个 Go 程序都由包构成。开头是package
。
import
导入包,包名和导入路径的最后一个元素一致。圆括号可以把导入的包分成一组,当然,分组导入语句更好。
在Go中,已导出的名字会以大写字母开头,未以大写字母开头的就是未导出,任何“未导出”的名字在该包外均无法访问。
函数:
1
2
3
4
5
6
7
|
func add(x int, y int) int {
return x + y
}
func add(x , y int) int {
return x + y
}
|
类型在变量名的后面,变量类型一致的时候,可以省略,只写最后一个。Go中的函数可以返回任意数量的返回值。函数可以作为函数的参数或返回值。
函数闭包closure:闭包是一函数,捕获其外部作用域中的变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package main
import "fmt"
func makeMultiplier(factor int) func(int) int {
// makeMultiplier 是外部函数
// factor 是其外部作用域的变量
return func(num int) int {
// 这个匿名函数就是闭包
// 它“捕获”了外部函数 makeMultiplier 中的 factor 变量
return num * factor
}
}
func main() {
// 创建一个闭包,它捕获了 factor = 2
double := makeMultiplier(2)
fmt.Println(double(5)) // 输出:10
// 创建另一个闭包,它捕获了 factor = 3
triple := makeMultiplier(3)
fmt.Println(triple(5)) // 输出:15
// 验证它们是独立的
fmt.Println(double(10)) // 输出:20
}
|
Go的返回值可以被命名,它们会被视作定义在函数顶部的变量。返回值的命名应当能反应其含义,它可以作为文档使用。没有参数的 return
语句会直接返回已命名的返回值,也就是「裸」返回值。裸返回语句应当仅用在下面这样的短函数中。在长的函数中它们会影响代码的可读性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package main
import "fmt"
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
func main() {
fmt.Println(split(17))
}
// 7 10
|
匿名函数:没有名字,立即执行
变量:var
声明一系列变量,类型在最后。变量声明可以包含初始值,每个变量对应一个,如果提供了初始值,则类型可以省略,变量会从初始值中推断出类型(%T
)。
1
2
|
var c, python, java bool
var c, python, java = true, false, "no!"
|
短变量:短赋值语句 :=
。函数外的每个语句都 必须 以关键字开始(var
、func
等),因此 :=
结构不能在函数外使用。
变量赋值:
=
是为已声明的变量赋值,必须先使用var
声明变量,才能用=
赋值;:=
是声明并赋值新变量,左侧至少有一个新变量。
基本类型:
1
2
3
4
5
6
7
8
|
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名
rune // int32 的别名, 表示一个 Unicode 码位
float32 float64
complex64 complex128
|
打印的时候用到的格式符:
- 通用格式符
%v
:value,默认格式,对于结构体,会打印字段名和字段值。对于数组和切片,会打印元素。
%T
:type,打印值的类型
- 整型格式符:
%d
十进制整数, %b
二进制,%o
八进制,%x
十六进制表示 (小写字母 a-f),%X
十六进制表示 (大写字母 A-F),%c
对应的 Unicode 字符。
- 浮点型和复数格式符:
%f
标准浮点数,%e
科学计数法(小写e) ,%E
科学计数法 (大写 E。
- 字符串和布尔型格式符:
%s
字符串,%t
布尔型。
- 指针格式符:
%p
零值:没有明确初始化的变量会被赋予零值【0,false,空字符串】
常量:const
for循环: []
内为可选,for可以当while使用,这个时候只需要写条件
1
2
3
4
5
|
for [初始化语句];条件;[每次迭代执行的操作]{
}
//初始化语句通常为一句短变量声明,该变量声明仅在for语句的作用域中可见。
|
if语句
switch分支:case值无需为常量,且取值不限于整数。case语句从上到下顺序执行,直到匹配成功时停止。停止就退出。
defer:defer 语句会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。推迟调用的函数会被压入栈。
指针:
1
2
|
p := &i // 指向 i
*p = 21 // 通过指针设置 i 的值
|
结构体:.
访问
数组:类型 [n]T
表示一个数组,它拥有 n
个类型为 T
的值。数组大小是固定的n。
切片:为数组元素提供了动态大小的、灵活的视角。类型 []T
表示一个元素类型为 T
的切片。切片通过两个下标来界定,一个下界和一个上界,二者以冒号分隔:a[low : high]
,包括第一个元素,但排除最后一个元素。上下界可以忽略,默认下界是0,上界是切片长度。
切片中有长度和容量的概念,长度就是包含元素的个数len(s)
,容量是从切片的第一个元素开始数,到其底层数组元素末尾的个数cap(s)
。
切片的零值是nil
。长度和容量为0且没有底层数组。
切片并不存储数据,只是描述了底层数组的一段,更改切片同时也会修改数组对应的元素。
切片字面量:类似没有长度的数组字面量。
1
|
[]bool{true, true, false} // 创建了数组[3]bool{true, true, false}以及引用了它的切片
|
创建切片:make
函数,元素为0的数组以及引用它的切片。
1
2
|
a := make([]int, 5) // len(a)=5 指定容量
b := make([]int, 0, 5) // len(b)=0, cap(b)=5 指定长度和容量
|
切片可以包含任何类型,包括其他切片
1
2
3
4
5
6
|
// 创建一个井字棋(经典游戏)
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
|
对切片追加新元素:append
函数
如果添加元素后,切片的长度会超出当前容量,Go 运行时会自动为切片分配一个新的、更大的底层数组。旧数组中的元素会被复制到新数组中,然后在新数组中添加新元素。
1
2
|
var s []int
s = append(s, 1,2,3) //s为[1,2,3]
|
range遍历:for
循环的 range
形式可遍历切片或映射。每次迭代会返回两个值,当前元素下标,以及对应元素的副本。
1
2
3
4
|
var pow = []int{1, 2, 4, 8}
for i, v := range pow {
fmt.Printf("%d, %d\n", i, v)
} // 0,1 1,2 2,4 3,8
|
可以使用_
来忽略下表/值。
1
2
3
|
for i, _ := range pow
for _, value := range pow
for i := range pow // 只需要索引
|
map映射:映射的零值为 nil
。nil
映射既没有键,也不能添加键。make
函数会返回给定类型的映射,并将其初始化备用。映射的字面量必须有键名。
1
2
3
4
|
var m map[string]int
m = make(map[string]int)
m["Bell Labs"] = 2003
fmt.Println(m) //map[Bell Labs:2003]
|
增/改:m[key]=elem
删:delete(m,key)
查:xx=m(key)
双赋值检测某个键是否存在:elem, ok = m[key]
- 若
key
在 m
中,ok
为 true
;否则,ok
为 false
。
- 若
key
不在映射中,则 elem
是该映射元素类型的零值。
方法和接口
方法:是一类带特殊的接收者参数的函数。方法接收者在它自己的参数列表内,位于 func
关键字和方法名之间。接收者的类型定义和方法声明必须在同一包内。接收者可以是结构体类型或非结构体类型,还可以声明指针类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
type Int int
func (a Int) jiafa(b int) int{
sum :=int(a)+b
return sum
}
type Vertex struct {
X, Y int
}
func (v Vertex) xiangjia() int{
sum :=v.X+v.Y
return sum
}
func (v *Vertex) Scale(f int) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
x:=Int(3)
fmt.Println(x.jiafa(4)) // 7
v:=Vertex{5,9}
fmt.Println(v.xiangjia()) // 14
v.Scale(10)
fmt.Println(v) //50 90
}
|
继承:Go中没有类和继承的概念,它通过组合(composition)和接口(interface)来实现类似的功能。
组合:是 Go 中实现代码复用的主要方式。通过将一个结构体嵌入到另一个结构体中,子结构体可以"继承"父结构体的字段和方法。
接口:是 Go 中实现多态的主要方式。通过定义接口,不同的结构体可以实现相同的方法,从而实现类似继承的多态行为。
接口本身不实现任何方法,它只声明了方法签名(方法名、参数列表和返回值列表)。Go 语言的接口实现是隐式的 (implicit),不需要显式地声明一个类型实现了某个接口,只要它的方法集合匹配,Go 编译器就会自动识别这种关系。
函数的参数可以为接口类型,任何实现了该接口的类型都可以作为参数传递给这个函数。接口也可以作为函数的返回值。
指定了零个方法的接口值被称为空接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
package main
import "fmt"
// 定义一个名为 Speaker 的接口
type Speaker interface {
Speak() string // 声明一个 Speak 方法,返回一个字符串
}
type Mover interface {
Move() string
}
type Dog struct {
Name string
}
// Dog 类型实现了 Speaker 接口的 Speak 方法
func (d Dog) Speak() string {
return "Woof! My name is " + d.Name
}
// Dog 类型实现了 Mover 接口的 Move 方法
func (d Dog) Move() string {
return "Running around!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return "Meow! My name is " + c.Name
}
// Greeter 函数接受一个 Speaker 接口类型的参数
func Greeter(s Speaker) {
fmt.Println(s.Speak())
}
// Transporter 函数接受一个 Mover 接口类型的参数
func Transporter(m Mover) {
fmt.Println(m.Move())
}
func main() {
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}
// Dog 和 Cat 都实现了 Speaker 接口
Greeter(dog) // 输出:Woof! My name is Buddy
Greeter(cat) // 输出:Meow! My name is Whiskers
// Dog 实现了 Mover 接口
Transporter(dog) // 输出:Running around!
Transporter(cat) // 报错信息:cannot use cat (variable of type Cat) as type Mover in argument to Transporter:Cat does not implement Mover (missing Move method)
}
|
一个接口类型的变量实际上存储了两个信息:
- 动态类型 (Dynamic Type):存储在接口值中实际存储的底层值的类型。
- 动态值 (Dynamic Value):存储在接口值中实际存储的底层值。
当接口值为
nil
时,它的动态类型和动态值都为 nil
。一个接口值只有当它的动态类型和动态值都为 nil
时,才等于 nil
。
类型断言:检查接口类型变量所持有的底层值是否为某个具体类型,并(如果检查通过)将其提取出来。当你有一个 interface{}
(空接口,可以存储任何类型的值)或者一个包含特定方法的接口变量时,你可能需要知道它里面到底存的是什么类型的实际数据,然后才能对这个数据进行操作。类型断言就是用来做这个“检查”和“提取”的。
1
|
value, ok := interfaceVar.(Type)
|
如果断言成功,value
将是 interfaceValue
底层值转换为 Type
类型后的结果。ok
是一个布尔值。如果断言成功(即底层值的类型是 Type
),ok
为 true
;否则为 false
。【也有不带ok的类型断言,不过不推荐,因为如果类型不匹配,会触发panic】
类型选择:是一种按顺序从几个类型断言中选择分支的结构。和switch语句相似,但是case中为类型type而非值,它们针对给定接口值所存储的值的类型进行比较。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("二倍的 %v 是 %v\n", v, v*2)
case string:
fmt.Printf("%q 长度为 %v 字节\n", v, len(v))
default:
fmt.Printf("我不知道类型 %T!\n", v)
}
}
func main() {
do(21)、//二倍的 21 是 42
do("hello") //"hello" 长度为 5 字节
do(true) //我不知道类型 bool!
}
|
Stringer
:是一个可以用字符串描述自己的类型。fmt
包和其他很多包都通过此接口来打印值。只要实现了String()
方法并返回一个字符串,就被认为实现了 fmt.Stringer
接口。直接打印一个 Go 结构体时,比如使用 fmt.Println()
或 %v
格式化动词,通常会得到一个类似 {字段名:字段值 字段名:字段值}
这样的默认输出,这在调试时可能很有用,但在用户界面或日志中可能不够直观。
```go
type Stringer interface {
String() string
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
错误:`error`值来表示错误状态,和`fmt.Stringer`类似,是一个内建接口,`fmt`包会根据对error的实现来打印值。
## 泛型
泛型的出现是为了在处理多种不同类型数据的函数、结构体和接口的时候,避免为每种类型都编写重复的代码。在泛型出现之前,如果你想编写一个能够对整数切片和字符串切片都进行操作(比如查找最小值)的函数,你可能需要编写两个几乎相同的函数:一个用于 `[]int`,另一个用于 `[]string`。泛型解决了这种**代码重复**的问题。
类型参数:是泛型的核心,被放置在函数名或类型名后面的方括号 `[]` 中。这些类型参数就像普通函数的参数一样,只不过它们接受的是类型,而不是值。
```go
package main
import "fmt"
// Min 是一个泛型函数,它接受一个类型参数 T。
// T 必须是实现了 comparable 约束的类型。
func Min[T comparable](a, b T) T {
if a < b { // 注意:这里使用了 < 运算符,所以 T 必须是可排序的,不仅仅是可比较的
return a
}
return b
}
func main() {
fmt.Println(Min(10, 20)) // 输出: 10
fmt.Println(Min(3.14, 2.71)) // 输出: 2.71
fmt.Println(Min("apple", "banana")) // 输出: apple
}
|
类型约束:上述comparable
就是一个类型约束,它限制了哪些类型可以作为类型参数的实际类型。约束可以是 Go 内置的预定义接口(如 comparable
),也可以是自定义的接口。
comparable
约束: comparable
是 Go 语言内置的一个预定义接口,它表示实现该接口的类型可以使用 ==
和 !=
运算符进行比较。当你需要在泛型代码中对值进行相等性判断时,就会使用 comparable
约束。
- 自定义接口作为约束: 定义自己的接口来作为类型约束。这意味着只有实现了该接口中所有方法的类型才能作为类型参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package main
import "fmt"
// Number 接口作为类型约束
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}
// Add 是一个泛型函数,它接受一个类型参数 T,T 必须是 Number 类型
func Add[T Number](a, b T) T {
return a + b
}
func main() {
fmt.Println(Add(1, 2)) // 输出: 3 (int)
fmt.Println(Add(1.5, 2.3)) // 输出: 3.8 (float64)
// fmt.Println(Add("a", "b")) // 编译错误:string does not implement Number
}
|
泛型类型:泛型结构体
1
2
3
4
5
6
|
// ListNode 是一个泛型结构体,它的 Value 字段可以是任何类型 T
// [T any]表示ListNode的类型参数T可以是任何类型
type ListNode[T any] struct {
Value T
Next *ListNode[T]
}
|
并发
Go协程 Goroutine:由 Go 运行时(runtime)管理的、用户态的、轻量级的执行单元。可以理解为轻量级线程。
- 轻量级:内存占用小;创建和销毁开销低;调度靠Go而非操作系统,开销小。
- 用户态:Go 协程的调度完全由 Go 运行时负责,而不是操作系统内核。Go 运行时可以更高效地在 Go 协程之间切换,因为它不需要进行昂贵的内核上下文切换。
- 并发和并行:
- 并发:并发是指多个任务在同一时间段内“交织”执行,它们不一定同时运行,而是通过快速切换来给人一种同时运行的错觉(例如,单核 CPU 上的多任务)。
- 并行:如果你的机器有多个 CPU 核心,Go 运行时会自动将多个 Go 协程调度到不同的 CPU 核心上,实现真正的并行执行,从而充分利用多核处理器的优势。
创建 Go 协程非常简单,只需要在函数调用前加上 go
关键字即可。
信道channel
:用于 goroutines 之间的通信。信道是带有类型的管道,可以通过它用信道操作符 <-
来发送或者接收值。“箭头”就是数据流的方向。和映射与切片一样,信道在使用前必须创建,信道分两种,无缓冲信道和有缓冲信道:
- 无缓冲信道:
ch := make(chan Type)
,发送操作会一直阻塞,直到有对应的接收操作。接收操作会一直阻塞,直到有对应的发送操作。它们保证发送和接收的同步发生,就像一次握手。发送者和接收者在同一时间点上交换数据。【强同步】
- 有缓冲信道:
ch := make(chan Type, capacity)
,capacity
是一个整数,表示信道可以存储多少个元素。发送操作只有在信道满时才会阻塞。接收操作只有在信道空时才会阻塞。它们可以在发送和接收之间提供一个缓冲区,允许发送者在接收者准备好之前发送多个数据,反之亦然。有缓冲信道可以实现异步通信,提高程序的吞吐量,但需要注意缓冲区溢出(导致发送阻塞)或空读(导致接收阻塞)的情况。
关闭信道使用close()
函数。
sync.WaitGroup
:用于等待多个Goroutine完成
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"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Goroutine 完成时调用 Done()
fmt.Printf("Worker %d started\n", id)
fmt.Printf("Worker %d finished\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 增加计数器
go worker(i, &wg)
}
wg.Wait() // 等待所有 Goroutine 完成
fmt.Println("All workers done")
}
|
context
:用于控制 Goroutine 的生命周期。context
包提供了一种处理请求范围的截止日期、取消信号以及请求范围值的方法。它在处理多个 Goroutine 之间的协作,特别是在需要控制 Goroutine 生命周期和避免资源泄露时,显得尤为重要。
context.WithCancel
手动控制Goroutine的停止,context.WithTimeout
定时停止Goroutine。
Mutex
和 RWMutex
:sync.Mutex
提供互斥锁,用于保护共享资源。
1
2
3
4
|
var mu sync.Mutex
mu.Lock()
// critical section
mu.Unlock()
|
select语句:select
语句使得一个 goroutine 可以等待多个通信操作。select
会阻塞,直到其中的某个 case 可以继续执行。
1
2
3
4
5
6
7
8
9
10
11
12
|
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
|
Go Modules 常用命令
Go Modules 是 Go 语言官方推荐的包管理工具,它的核心在于 go.mod
文件,用于定义模块及其依赖。掌握 go mod
命令能高效地管理项目依赖。
go mod init [module path]
:在当前目录初始化一个新的 Go 模块,并创建 go.mod
文件。
go mod tidy
:清理和同步模块依赖。这是日常开发中最常用的命令之一。
go get [package@version]
:下载并安装指定的 Go 包,并将其作为当前模块的依赖添加到 go.mod
文件中。
go mod download
:下载 go.mod
文件中列出的所有依赖到本地模块缓存(通常是 $GOPATH/pkg/mod
目录)。
go mod graph
:打印模块的依赖图,展示所有直接和间接的依赖关系。
go mod verify
:验证模块的依赖是否已被篡改或损坏。它会检查 go.sum
文件中的校验和与本地缓存的模块内容是否一致。
参考: