Go语言入门
程序结构
声明与变量
在 Go 语言中,字面量(Literal) 的概念和 C++ 中的字面量非常相似,都是指在代码中直接表示固定值的符号或表达式。它们用于表示基本数据类型(如整数、浮点数、字符、字符串等)的常量值,而无需通过变量或计算来获取。
场景 | 短赋值 := |
一般赋值 = |
---|---|---|
函数内声明新变量 | ✅ | ❌(需配合 var ) |
包作用域变量赋值 | ❌ | ✅ |
仅赋值(变量已声明) | ❌ | ✅ |
多变量赋值(含新变量) | ✅ | ❌ |
显式指定变量类型 | ❌ | ✅(配合 var ) |
流程控制
for
Go 只有一种循环结构:for
循环。基本的 for
循环由三部分组成,它们用分号隔开,表达式外无需小括号 ( )
,而大括号 { }
则是必须的
1 | for i := 0; i < 10; i++ { |
初始化语句和后置语句是可选的,且可以去掉分号,此时for相当于C++中的while
1 | for sum < 1000 { |
如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑 for { }
for
循环的 range
形式可遍历切片或映射
-
每次迭代都会返回两个值。 第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本
也是融合了c++两种for循环写法,索引遍历和迭代器遍历
可以将下标或值赋予 _
来忽略它。
1 | for i, _ := range pow |
if else
Go 的 if
语句与 for
循环类似,表达式外无需小括号 ( )
,而大括号 { }
则是必须的
-
和
for
一样,if
语句可以在条件表达式前执行一个简短语句,该语句声明的变量作用域仅在if
之内。
1 | if v := math.Pow(x, n); v < lim { |
在 if
的简短语句中声明的变量同样可以在对应的任何 else
块中使用。if
和 else
的使用方式除去小括号外与C++一致
swtich case
switch
语句是编写一连串 if - else
语句的简便方法。它运行第一个 case
值 值等于条件表达式的子句。
Go 只会运行选定的 case
,而非之后所有的 case
。 在效果上,Go 的做法相当于这些语言中为每个 case
后面自动添加了所需的 break
语句。 在 Go 中,除非以 fallthrough
语句结束,否则分支会自动终止。
-
case
无需为常量,且取值不限于整数 -
case
语句从上到下顺次执行,直到匹配成功时停止 -
无条件的
switch
同switch true
一样,该形式能将一长串if-then-else
写得更加清晰。
1 | func main() { |
🔥defer推迟
defer 语句会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但只会在外层函数返回前该函数才会被调用
-
推迟调用的函数调用会被压入一个栈中。 当外层函数返回时,被推迟的调用会按照后进先出的顺序调用。
复合数据类型
指针
一如C语言,其保存了值的内存地址
类型 *T
是指向 T
类型值的指针,其零值为 nil
。
1 | var p *int |
&
操作符会生成一个指向其操作数的指针。
1 | i := 42 |
*
操作符表示指针指向的底层值。
1 | fmt.Println(*p) // 通过指针 p 读取 i |
这也就是通常所说的「解引用」或「间接引用」。
与 C 不同,Go 无法对指针进行四则运算,即直接修改地址,能够保障内存的安全性
结构体
一个 结构体(struct
)就是一组 字段(field)
1 | type Vertex struct { |
结构体指针
如果我们有一个指向结构体的指针 p
那么可以通过 (*p).X
来访问其字段 X
。 不过这么写太啰嗦了,所以语言也允许我们使用隐式解引用,直接写 p.X
就可以。
数组
类型 [n]T
表示一个数组,它拥有 n
个类型为 T
的值。
1 | var a [10]int |
会将变量 a
声明为拥有 10 个整数的数组。
-
数组的长度是其类型的一部分,因此数组不能改变大小。
🔥切片
类型 []T
表示一个元素类型为 T
的切片。.
切片通过两个下标来界定,一个下界和一个上界,二者以冒号分隔:
1 | a[low : high] |
它会选出一个半闭半开区间,包括第一个元素,但排除最后一个元素。
-
切片就像数组的引用,切片并不存储任何数据,它只是映射了底层数组,更改切片的元素会修改其底层数组中对应的元素
-
切片下界的默认值为 0,上界则是该切片的长度
切片拥有 长度 和 容量。
-
切片的长度就是它所包含的元素个数
-
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数
-
切片
s
的长度和容量可通过表达式len(s)
和cap(s)
来获取
切片的零值是 nil
-
nil 切片的长度和容量为 0 且没有底层数组
切片可以用内置函数 make
来创建,这也是你创建动态数组的方式。
make
函数会分配一个元素为零值的数组并返回一个引用了它的切片:
1 | a := make([]int, 5) // len(a)=5 |
要指定它的容量,需向 make
传入第三个参数:
1 | b := make([]int, 0, 5) // len(b)=0, cap(b)=5 |
切片可以包含任何类型,当然也包括其他切片,可以使用切片嵌套来创建更高维的数据结构
Go 提供了内置的 append
函数为切片追加新的元素,内置函数的文档对该函数有详细的介绍。
1 | func append(s []T, vs ...T) []T |
append
的第一个参数 s
是一个元素类型为 T
的切片,其余类型为 T
的值将会追加到该切片的末尾
-
当
s
的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。 返回的切片会指向这个新分配的数组。真的像vector吧
1 | var s []int |
map映射
map
映射将键映射到值。
-
映射的零值为
nil
。nil
映射既没有键,也不能添加键。
1 | var m map[string]int |
声明一个key为string类型,value为int类型的map
1 | m := make(map[string]int) |
短赋值一个key为string类型,value为int类型的map
映射的字面量和结构体类似,只不过必须有键名,若顶层类型只是一个类型名,那么你可以在字面量的元素中省略它。
修改映射
在映射 m
中插入或修改元素:
1 | m[key] = elem |
获取元素:
1 | elem = m[key] |
删除元素:
1 | delete(m, key) |
通过双赋值检测某个键是否存在:
1 | elem, ok = m[key] |
若 key
在 m
中,ok
为 true
;否则,ok
为 false
。
若 key
不在映射中,则 elem
是该映射元素类型的零值。
注:若 elem
或 ok
还未声明,你可以使用短变量声明:
1 | elem, ok := m[key] |
函数
函数也是值
-
它们可以像其他值一样传递。
-
函数值可以用作函数的参数或返回值。
Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。 该函数可以访问并赋予其引用的变量值,换句话说,该函数被“绑定”到了这些变量。
1 | func compute(fn func(float64, float64) float64) float64 { |
函数闭包🔥🔥🔥
1 | func adder() func(int) int { |
pos 和 neg 这两个变量,都是通过 adder() 函数创建的闭包
-
adder() 函数返回一个函数 func(int) int
即pos和neg变量都可以视作为一个函数,并且具有一个外部变量sum:
1 | func(x int) int { |
它们引用的闭包中的 sum 变量跟随 main 函数的生命周期,因此每次调用pos和neg时会在上一次加和的基础上进行计算
方法与接口
方法
Go 没有类。不过你可以为类型定义方法。
-
方法就是一类带特殊的 接收者 参数的函数。
-
方法接收者在它自己的参数列表内,位于
func
关键字和方法名之间。
在此例中,Abs
方法拥有一个名字为 v
,类型为 Vertex
的接收者
1 | type Vertex struct { |
方法只是个带接收者参数的函数,上下两段代码等效
1 | type Vertex struct { |
也可以为非结构体类型声明方法,但接收者的类型定义和方法声明必须在同一包内
接收者参数需要在函数内再进行一次赋值才能进行迭代,若是直接对接收者参数操作则只会产生一个新的局部变量,无法真正影响到接收者参数的值
1 | type List[T any] struct { |
接口
接口类型 的定义为一组方法签名,接口类型的变量可以持有任何实现了这些方法的值。
接口也是值。它们可以像其它值一样传递。
-
接口值可以用作函数的参数或返回值。
-
在内部,接口值可以看做包含值和具体类型的元组:
1 | (value, type) |
-
接口值保存了一个具体底层类型的具体值。
-
接口值调用方法时会执行其底层类型的同名方法。
接口的本质:
-
接口就像是一个"合同"或"协议"
-
它定义了一组方法,但只声明这些方法应该做什么,而不具体实现
-
任何类型只要实现了接口中定义的所有方法,就自动实现了这个接口
即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用, 保存了 nil 具体值的接口其自身并不为 nil。
nil 接口值(即仅声明不赋值)既不保存值也不保存具体类型,为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。
指定了零个方法的接口值被称为空接口:
1 | interface{} |
空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)
空接口被用来处理未知类型的值。例如,fmt.Print
可接受类型为 interface{}
的任意数量的参数。
类型断言
类型断言 提供了访问接口值底层具体值的方式。
1 | t := i.(T) |
该语句断言接口值 i
保存了具体类型 T
,并将其底层类型为 T
的值赋予变量 t
。
若 i
并未保存 T
类型的值,该语句就会触发一个 panic。
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
1 | t, ok := i.(T) |
若 i
保存了一个 T
,那么 t
将会是其底层值,而 ok
为 true
。
否则,ok
将为 false
而 t
将为 T
类型的零值,程序并不会产生 panic。
Stringer
1 | type Stringer interface { |
Stringer
是一个可以用字符串描述自己的类型。fmt
包(还有很多包)都通过此接口来打印值。
1 | func (p Person) String() string { |
可以直接通过Stringer
自定义输出类型
错误
Go 程序使用 error
值来表示错误状态。
与 fmt.Stringer
类似,error
类型是一个内建接口:
1 | type error interface { |
-
error 是一个接口,它要求实现者必须提供一个 Error() string 方法。
-
任何类型只要实现了 Error() string 方法,就可以作为 error 类型使用。
(与 fmt.Stringer
类似,fmt
包也会根据对 error
的实现来打印值。)
通常函数会返回一个 error
值,调用它的代码应当判断这个错误是否等于 nil
来进行错误处理。
1 | i, err := strconv.Atoi("42") |
error
为 nil 时表示成功;非 nil 的 error
表示失败。
Readers
io
包指定了 io.Reader
接口,它表示数据流的读取端。
Go 标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。
io.Reader
接口有一个 Read
方法:
1 | func (T) Read(b []byte) (n int, err error) |
b []byte
表示Read输入数据所使用的buffer,返回的 int
为buffer长度(填充的字节数),error
为错误值,在遇到数据流的结尾时,它会返回一个 io.EOF
错误。
示例代码创建了一个 strings.Reader
并以每次 8 字节的速度读取它的输出。
嵌套Reader
1 | type rot13Reader struct { |
一个实现了 io.Reader
并从另一个 io.Reader
中读取数据的 rot13Reader
,通过应用 rot13 代换密码对数据流进行修改。
泛型
泛型函数
可以使用类型参数编写 Go 函数来处理多种类型。 函数的类型参数出现在函数参数之前的方括号之间。
1 | func Index[T comparable](s []T, x T) int |
此声明意味着 s
是满足内置约束 comparable
的任何类型 T
的切片。 x
也是相同类型的值。
comparable
是一个有用的约束,它能让我们对任意满足该类型的值使用 ==
和 !=
运算符。在此示例中,我们使用它将值与所有切片元素进行比较,直到找到匹配项。 该 Index
函数适用于任何支持比较的类型。
泛型类型
除了泛型函数之外,Go 还支持泛型类型。 类型可以使用类型参数进行参数化,这对于实现通用数据结构非常有用。
并发
Go 程(goroutine)
由 Go 运行时管理的轻量级线程。
1 | go f(x, y, z) |
会启动一个新的 Go 协程并执行
1 | f(x, y, z) |
f
, x
, y
和 z
的求值发生在当前的 Go 协程中,而 f
的执行发生在新的 Go 协程中。
信道
信道是带有类型的管道,元素先进先出,你可以通过它用信道操作符 <-
来发送或者接收值。
1 | ch <- v // 将 v 发送至信道 ch。 |
(“箭头”就是数据流的方向。)
和映射与切片一样,信道在使用前必须创建:
1 | ch := make(chan int) |
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
带缓冲的信道
信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make
来初始化一个带缓冲的信道:
1 | ch := make(chan int, 100) |
仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。
缓冲区若填满会产生死锁