包、变量与函数
包
每个 Go 程序都由包构成。
程序从 main 包开始运行。
按照约定,包名与导入路径的最后一个元素一致。例如,"math/rand" 包中的源码均以 package rand 语句开始。
导出名
在 Go 语言中,导出名是指从包中可以被外部包访问的标识符(变量、常量、函数、类型、结构体字段等)。
-
首字母大写的标识符会被导出(公开)
-
首字母小写的标识符不会被导出(私有)
导出名机制是 Go 语言实现封装和信息隐藏的核心机制,它通过简单的命名约定来管理包的公开 API。
函数
注意类型在变量名的后面。
当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。
函数可以返回任意数量的返回值。
带名字的返回值
Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。
返回值的命名应当能反应其含义,它可以作为文档使用。
没有参数的 return 语句会直接返回已命名的返回值,也就是「裸」返回值。
裸返回语句应当仅用在短函数中。在长的函数中它们会影响代码的可读性。
变量
var 语句用于声明一系列变量。和函数的参数列表一样,类型在最后。
如例中所示,var 语句可以出现在包或函数的层级。
变量的初始化
变量声明可以包含初始值,每个变量对应一个。
如果提供了初始值,则类型可以省略;变量会从初始值中推断出类型。
没有明确初始化的变量声明会被赋予对应类型的零值(数值则为0,布尔则为假,字符串则为空串)。
短变量声明
在函数中,短赋值语句 := 可在隐式确定类型的 var 声明中使用。
函数外的每个语句都 必须 以关键字开始(var、func 等),因此 := 结构不能在函数外使用。
基本类型
Go 的基本类型有:
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名
rune // int32 的别名
// 表示一个 Unicode 码位
float32 float64
complex64 complex128
本例展示了几种类型的变量。 和导入语句一样,变量声明也可以「分组」成一个代码块。
int、uint 和 uintptr 类型在 32-位系统上通常为 32-位宽,在 64-位系统上则为 64-位宽。
当你需要一个整数值时应使用 int 类型, 除非你有特殊的理由使用固定大小或无符号的整数类型。
类型转换
表达式 T(v) 将值 v 转换为类型 T。
与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换。
类型推断
在声明一个变量而不指定其类型时(即使用不带类型的 := 语法 var = 表达式语法),变量的类型会通过右值推断出来。
当声明的右值确定了类型时,新变量的类型与其相同。
常量
常量的声明与变量类似,只不过使用 const 关键字。
常量可以是字符、字符串、布尔值或数值。
常量不能用 := 语法声明。
数值常量
数值常量(字面量)是高精度的值(纯数学意义上的、没有精度限制)。
一个未指定类型的常量由上下文来决定其类型。
流程控制语句
for 循环
Go 只有一种循环结构:for 循环。
基本的 for 循环由三部分组成,它们用分号隔开:
-
初始化语句:在第一次迭代前执行
-
条件表达式:在每次迭代前求值
-
后置语句:在每次迭代的结尾执行
初始化语句通常为一句短变量声明,该变量声明仅在 for 语句的作用域中可见。
一旦条件表达式求值为 false,循环迭代就会终止。
注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的。
初始化语句和后置语句是可选的。此时你可以去掉分号,因为 C++ 的 while 在 Go 中叫做 for。
如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑。
if 判断
Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ),而大括号 { } 则是必须的。
和 for 一样,if 语句可以在条件表达式前执行一个简短语句。
该语句声明的变量作用域仅在 if 之内 和 在对应的任何 else 块中。
switch 分支
switch 语句是编写一连串 if - else 语句的简便方法。它运行第一个 case 值 值等于条件表达式的子句。
Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的,不过 Go 只会运行选定的 case,而非之后所有的 case。
在效果上,Go 的做法相当于这些语言中为每个 case 后面自动添加了所需的 break 语句。
在 Go 中,除非以 fallthrough 语句结束,否则分支会自动终止。
Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不限于整数。
switch 的 case 语句从上到下顺次执行,直到匹配成功时停止。
无条件的 switch 同 switch true 一样。这种形式能将一长串 if-then-else 写得更加清晰。
defer 推迟
defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
推迟调用的函数调用会被压入一个栈中。 当外层函数返回时,被推迟的调用会按照后进先出的顺序调用。
更多类型
指针
Go 拥有指针。指针保存了值的内存地址。
类型 *T 是指向 T 类型值的指针,其零值为 nil。
& 操作符会生成一个指向其操作数的指针。
* 操作符表示指针指向的底层值。
这也就是通常所说的「解引用」或「间接引用」。
与 C 不同,Go 没有指针运算。
结构体
一个 结构体(struct)就是一组 字段(field)。
结构体字段可通过点号 . 来访问。
结构体字段可通过结构体指针来访问。
如果我们有一个指向结构体的指针 p 那么可以通过 (*p).X 来访问其字段 X。
不过这么写太啰嗦了,所以语言也允许我们使用隐式解引用,直接写 p.X 就可以。(这里和C不一样)
数组
类型 [n]T 表示一个数组,它拥有 n 个类型为 T 的值。
表达式 var a [10]int 会将变量 a 声明为拥有 10 个整数的数组。
数组的长度是其类型的一部分,因此数组不能改变大小。 这看起来是个限制,不过没关系,Go 拥有更加方便的使用数组的方式。
切片
每个数组的大小都是固定的。而切片则为数组元素提供了动态大小的、灵活的视角。 在实践中,切片比数组更常用。
类型 []T 表示一个元素类型为 T 的切片。.
切片通过两个下标来界定,一个下界和一个上界,二者以冒号分隔:a[low, high]
它会选出一个左闭右开区间,包括第一个元素,但排除最后一个元素。
这个表达式创建了一个切片,它包含 a 中下标从 1 到 3 的元素:a[1:4]
切片就像数组的引用
切片就像数组的引用。
切片并不存储任何数据,它只是描述了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。
和它共享底层数组的切片都会观测到这些修改。
数组和切片字面量
切片字面量类似于没有长度的数组字面量。
这是一个数组字面量:[3]bool{true, true, false}
下面这样则会创建一个和上面相同的数组,然后再构建一个引用了它的切片:[ ]bool{true, true, false}
切片的默认行为
在进行切片时,你可以利用它的默认行为来忽略上下界。
切片下界的默认值为 0,上界则是该切片的长度。
对于数组 var a [10]int
来说,以下切片表达式和它是等价的:a[0:10]、a[:10]、a[0:]、a[:]
切片的长度与容量
切片的长度就是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。
切片的零值
切片的零值是 nil。
nil 切片的长度和容量为 0 且没有底层数组。
用make创建切片
切片可以用内置函数 make 来创建,这也是你创建动态数组的方式。
make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:a := make([]int, 5) // len(a)=5
要指定它的容量,需向 make 传入第三个参数:b := make([]int, 0, 5) // len(b)=0, cap(b)=5
切片的切片
切片可以包含任何类型,当然也包括其他切片。
向切片追加新元素
为切片追加新的元素是种常见的操作,为此 Go 提供了内置的 append 函数。
func append(s []T, vs ...T) []T
append 的第一个参数 s 是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾。
append 的结果是一个包含原切片所有元素加上新添加元素的切片。
当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。 返回的切片会指向这个新分配的数组。
切片的range遍历
for 循环的 range 形式可遍历切片或映射。
当使用 for 循环遍历切片时,每次迭代都会返回两个值。
第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本(不是引用)。
可以将下标或值赋予 _ 来忽略它。若你只需要索引,忽略第二个变量即可。
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
map 映射
map 映射将键映射到值。
声明语法:var mapName map[keyType]valueType
映射的零值为 nil 。nil 映射既没有键,也不能添加键。
make 函数会返回给定类型的映射,并将其初始化备用。
使用make函数创建:mapName := make(map[keyType]valueType)
映射的字面量和结构体类似,只不过必须有键名。
修改映射
在映射 m 中插入或修改元素:m[key] = elem
获取元素:elem = m[key]
删除元素:delete(m, key)
通过双赋值检测某个键是否存在:elem, ok = m[key]
若 key 在 m 中,ok 为 true ;否则,ok 为 false。
若 key 不在映射中,则 elem 是该映射元素类型的零值。
注:若 elem 或 ok 还未声明,你可以使用短变量声明:elem, ok := m[key]
函数值
函数也是值。它们可以像其他值一样传递。
函数值可以用作函数的参数或返回值。
函数闭包
Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。
该函数可以访问并赋予其引用的变量值,换句话说,该函数被“绑定”到了这些变量。
例如,函数 adder 返回一个闭包。每个闭包都被绑定在其各自的 sum 变量上。
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
方法和接口
方法
Go 没有类。不过你可以为类型定义方法。
方法就是一类带特殊的 接收者(即该方法所属的类和对象) 参数的函数。
方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。
记住:方法只是个带接收者参数的函数。
你也可以为非结构体类型声明方法,接收者的类型定义和方法声明必须在同一包内。
指针类型的接收者
你可以为指针类型的接收者声明方法。
这意味着对于某类型 T,接收者的类型可以用 *T 的写法。 (此外,T 本身不能是指针,比如不能是 *int。)
指针接收者的方法可以修改接收者指向的值。 由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。
若使用值接收者,那么方法会对原始值的副本进行操作。(对于函数的其它参数也是如此)
对于语句 v.Scale(5) 来说,即便 v 是一个值而非指针,带指针接收者的方法也能被直接调用。
也就是说,由于 Scale 方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)。
使用指针接收者的原因有二:
首先,方法能够修改其接收者指向的值。
其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样会更加高效。
通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。
接口
接口类型 的定义为一组方法签名。
任何实现了这些方法(接口中的所有方法)的类型都隐式地实现了该接口。
接口类型的变量可以持有任何实现了这些方法的类型的值。
类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明,也就没有“implements”关键字。
隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。
因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。
接口值
接口也是值。它们可以像其它值一样传递。
接口值可以用作函数的参数或返回值。
在内部,接口值可以看做包含值和具体类型的元组:(value, type)
接口值保存了一个具体底层类型的具体值。
接口值调用方法时会执行其底层类型的同名方法。
底层值为nil的接口
即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。
在一些语言中,这会触发一个空指针异常,但在 Go 中通常会写一些方法来优雅地处理它(如本例中的 M 方法)。
注意: 保存了 nil 具体值的接口其自身并不为 nil。
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
nil接口值
nil 接口值既不保存值也不保存具体类型。
为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。
空接口
指定了零个方法的接口值被称为空接口:interface{}
空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法)
空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。
类型断言
类型断言 提供了访问接口值底层具体值的方式。
t := i.(T)
该语句断言接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t。
若 i 并未保存 T 类型的值,该语句就会触发一个 panic。
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
t, ok := i.(T)
若 i 保存了一个 T,那么 t 将会是其底层值,而 ok 为 true。
否则,ok 将为 false 而 t 将为 T 类型的零值,程序并不会产生 panic。
请注意这种语法和读取一个映射时的相同之处。
类型选择
类型选择 是一种按顺序从几个类型断言中选择分支的结构。
类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。
switch v := i.(type) {
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
}
类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type。
此选择语句判断接口值 i 保存的值类型是 T 还是 S。在 T 或 S 的情况下,变量 v 会分别按 T 或 S 类型保存 i 拥有的值。在默认(即没有匹配)的情况下,变量 v 与 i 的接口类型和值相同。
例子:Stringer 接口
type Stringer interface {
String() string
}
Stringer 是一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。
错误
Go 程序使用 error 值来表示错误状态。
与 fmt.Stringer 类似,error 类型是一个内建接口:
type error interface { // error 是内置类型, 不需要大写导出
Error() string
}
(与 fmt.Stringer 类似,fmt 包也会根据对 error 的实现来打印值。)
通常函数会返回一个 error 值,调用它的代码应当判断这个错误是否等于 nil 来进行错误处理。
例子:Reader 接口
io 包指定了 io.Reader 接口,它表示数据流的读取端。
Go 标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。
io.Reader 接口有一个 Read 方法:
func (T) Read(b []byte) (n int, err error)
Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误。
示例代码创建了一个 strings.Reader 并以每次 8 字节的速度读取它的输出。
package main
import (
"fmt"
"io"
"strings"
)
func main() {
reader := strings.NewReader("Hello, World!")
buffer := make([]byte, 8)
for {
n, err := reader.Read(buffer)
if err != nil {
fmt.Println("读取错误:", err)
break
}
fmt.Printf("读取了 %d 字节: %s\n", n, string(buffer[:n]))
if err == io.EOF {
fmt.Println("读取完成")
break
}
}
}
泛型
类型参数
可以使用类型参数编写 Go 函数来处理多种类型。 函数的类型参数出现在函数参数之前的方括号之间。
func Index[T comparable](s []T, x T) int
此声明意味着 s 是满足内置约束 comparable 的任何类型 T 的切片。 x 也是相同类型的值。
comparable 是一个有用的约束,它能让我们对任意满足该类型的值使用 == 和 != 运算符。在此示例中,我们使用它将值与所有切片元素进行比较,直到找到匹配项。 该 Index 函数适用于任何支持比较的类型。
泛型类型
除了泛型函数之外,Go 还支持泛型类型。 类型可以使用类型参数进行参数化,这对于实现通用数据结构非常有用。
并发
Go协程
协程 是 Go 语言并发编程的核心,它是一种比线程更轻量级的并发执行单元。可以把它理解为一个“轻量级线程”或“用户态线程”。
Goroutine 是由 Go 语言自身的运行时(Runtime) 在用户态进行调度的。这种调度是协作式的,并且与网络 I/O、锁、channel 等紧密集成,切换代价非常低。
启动一个Go程的方法非常简单,只需要在一个函数前面加上 go 关键字,例如:go func(),go关键字会立即返回,程序不会等待func执行完,而是会直接执行后面的代码
Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync 包提供了这种能力,不过在 Go 中并不经常用到,因为还有其它的办法。
信道
信道 是 Go 语言中一种内置的、用于在不同 Goroutine(协程)之间进行通信和同步的核心数据结构。你可以把它想象成一条管道或传送带,Goroutine 可以从一端发送数据,另一个 Goroutine 可以从另一端接收数据。
它的核心思想是:不要通过共享内存来通信;而应通过通信来共享内存。 这意味着,与其让多个 Goroutine 争抢同一块内存数据(需要复杂的锁机制),不如让数据在一个 Goroutine 中计算好后,通过信道直接“传递”给另一个 Goroutine。
信道是类型相关的,你只能向一个 chan int 信道发送整数,也只能从它接收整数。使用make关键字来创建信道。
无缓冲信道
无缓冲信道是同步信道,它没有任何存储数据的能力。
发送操作 ch <- v 会阻塞,直到另一个 Goroutine 在同一个信道上执行了接收操作 <-ch。
接收操作 <-ch 也会阻塞,直到另一个 Goroutine 在同一个信道上执行了发送操作 ch <- v。
发送者拿着数据,必须等到接收者伸出手来接,数据才能传递过去。这个“交接”的过程是瞬间同步完成的。
有缓冲信道
有缓冲信道是异步信道,它有一个固定大小的队列作为缓冲区。
发送操作 ch <- v 只有在缓冲区已满时才会阻塞。
接收操作 <-ch 只有在缓冲区为空时才会阻塞。
关闭信道
发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完 v, ok := <-ch,此时 ok 会被设置为 false。
循环 for i := range c 会不断从信道接收值,直到它被关闭。
注意: 只应由发送者关闭信道,而不应油接收者关闭。向一个已经关闭的信道发送数据会引发程序 panic。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。
select 语句
select 语句使一个 Go 程可以等待多个通信操作。
select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
当 select 中的其它分支都没有准备好时,default 分支就会执行。
为了在尝试发送或者接收时不发生阻塞,可使用 default 分支
互斥锁
我们已经看到信道非常适合在各个 Go 程间进行通信。
但是如果我们并不需要通信呢?比如说,若我们只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?
这里涉及的概念叫做 互斥 ,我们通常使用 互斥锁 这一数据结构来提供这种机制。
Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:Lock 和 Unlock
我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。参见 Inc 方法。
我们也可以用 defer 语句来保证互斥锁一定会被解锁。参见 Value 方法。
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter 是并发安全的
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Inc 对给定键的计数加一
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// 锁定使得一次只有一个 Go 协程可以访问映射 c.v。
c.v[key]++
c.mu.Unlock()
}
// Value 返回给定键的计数的当前值。
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// 锁定使得一次只有一个 Go 协程可以访问映射 c.v。
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
2013

被折叠的 条评论
为什么被折叠?



