Go语法知识

语言教程看官方文档更合适,本文仅做学习记录。

包、变量和函数

每个 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

匿名函数:没有名字,立即执行

1
func(){ ... }()

变量:var声明一系列变量,类型在最后。变量声明可以包含初始值,每个变量对应一个,如果提供了初始值,则类型可以省略,变量会从初始值中推断出类型(%T)。

1
2
var c, python, java bool
var c, python, java = true, false, "no!"

短变量:短赋值语句 :=。函数外的每个语句都 必须 以关键字开始(varfunc 等),因此 := 结构不能在函数外使用。

变量赋值: =是为已声明的变量赋值,必须先使用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),oktrue;否则为 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。

MutexRWMutexsync.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 命令能高效地管理项目依赖。

  1. go mod init [module path]:在当前目录初始化一个新的 Go 模块,并创建 go.mod 文件。
  2. go mod tidy:清理和同步模块依赖。这是日常开发中最常用的命令之一。
  3. go get [package@version]:下载并安装指定的 Go 包,并将其作为当前模块的依赖添加到 go.mod 文件中。
  4. go mod download:下载 go.mod 文件中列出的所有依赖到本地模块缓存(通常是 $GOPATH/pkg/mod 目录)。
  5. go mod graph:打印模块的依赖图,展示所有直接和间接的依赖关系。
  6. go mod verify:验证模块的依赖是否已被篡改或损坏。它会检查 go.sum 文件中的校验和与本地缓存的模块内容是否一致。

参考:

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计
本博客已稳定运行