前言
Go语言之旅是官方出品的非常好的学习手册。
这篇文章将总结其中提到的Go语言特性,方便日后查询。
所有的练习总结在了另一篇文章中.
自由总结
- 语句无需以分号结尾,但分号可以用来替代换行符。
- 左大括号
{
不能独占一行。
包、变量和函数
1. 包
- 包:每个go程序都是由包构成的,程序从main包开始运行。
- 导入:通过导入来使用其它包中的内容。可以使用圆括号来分组导入。
- 如果一个名字以大写字母开头,那么就是导出的,可以在包外访问。
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Pi)
}
2. 函数
- 函数通过关键字func定义,形如
func 函数名(参数列表) 返回值 {函数体}
- 函数可以返回任意数量的返回值。
- 函数返回值可被命名,会被视作在定义在函数顶部的变量。
- 返回值已命名的函数可以直接return.(会影响可读性)
- 程序总是由main函数开始。
- 形参的类型在名字之后,且连续多个形参类型相同时,可以只保留最后一个。
package main
import "fmt"
func add(x, y int) int {
return x + y
}
func swap(x, y string) (string, string) {
return y, x
}
func divide(a, b int) (x, y int) {
x = a / b
y = a % b
return
}
func main() {
fmt.Println(add(1, 2))
fmt.Println(swap("hello", "world"))
fmt.Println(divide(17, 3))
}
3. 变量(一)
- var语句用于声明一个变量列表,形如
var 变量名 类型
。 - var语句可以出现在包或函数级别。
- 变量声明可以包含初始值,每个变量对应一个。
var 变量名 类型 = 初始值
- 如果初始值已经存在,可以省略类型。
var 变量名 = 初始值
,变量的类型由右值推导而出。 - 省略类型后,这些变量的类型可以不同。
- 在函数中,
:=
可以代替var声明。
package main
import "fmt"
func main() {
var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"
fmt.Println(i, j, k, c, python, java)
}
4. 变量(二)
- 基本类型:
int、uint、uintptr的长度与系统有关。bool string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte //uint8的别名 rune //uint32的别名,表示一个Unicode码点 float32 float64 complex64 complex128
- 变量声明也可以分组。
- 没有明确初始值的变量会被赋予零值:
- 数值类型:0
- 布尔类型:false
- 字符串:""(空串)
- 其他:nil(空)
- 表达式T(v)将值v转换成类型T,不同类型的项之间进行运算或者赋值时需要显式转换。(你甚至不能把一个int变量和一个int32变量相加)
- 通过
fmt.Printf("%T", v)
来输出变量类型 - 常量由const关键字声明,不能用
:=
语法。 - 声明时未指定类型的数值常量是高精度的值,由上下文决定类型。
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
流程控制语句:for、if、else、switch 和 defer
1. 循环
- Go中的循环只有for一种,三个组成部分(初始化、判断、后置)用分号隔开,外面没有小括号,但紧接着的大括号是必须的。
- 初始化与后置部分是可选的。并且如果都不写的话,可以把两个分号省略(对应其它语言的while)。
- 无限循环可以写成
for{}
。 - if语句的表达式外无需小括号,而大括号是必须的。
- if语句可以在表达式前执行一个语句,与表达式用分号隔开。
- go的switch语句非常灵活,从上到下顺序执行,直到匹配成功时停止。注意只会运行第一个匹配成功的case,除非它的末尾有fallthrough关键字。
- 无条件的switch等同于
switch true
,判断条件在case中写,等效于一长串的if-else.
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
2. defer
- defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。 - 推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。
- defer语句的常见用法是在打开文件时即设置文件的关闭.
- panic是go的内置函数,可以终止当前函数并将一个panic返回给上级函数,但被defer的语句仍然会执行。
- recover是go的内置函数,可以从终止panic,它必须写在defer中。如果调用recover时没有panic,就会返回nil.
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
输出结果是
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.
更多类型:struct、slice 和映射
1. 指针
- Go拥有指针,通过
&
来取指针,通过*
来取值; - Go没有指针运算。
- 类型T的指针类型是*T,指针的零值是nil.
var i *int;
j := &p;
2. 结构体
- 一个结构体是一组字段(field),通过
type <name> struct
定义。 - 结构体字段使用点号来访问,结构体指针也可以使用点号来访问字段。
- 通过列出结构体的值可以分配一个结构体。也可以仅列出部分值。
package main
import "fmt"
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)
func main() {
fmt.Println(v1, p, v2, v3)
fmt.Println(v1.X, p.X)
}
3.数组
- 类型
[n]T
表示拥有n个T类型的值的数组 var a [10]int
会将变量a声明为长度为10的整数数组,下标从0开始.- 数组的长度是它的类型的一部分,所以大小是固定的。
- 数组长度如果写成
...
,会由编译器指定,如b := [...]string{"Penn", "Teller"}
会定义一个两个元素的字符串数组。 - 与C语言不同,数组本身也是值,如果将其作为参数传递,会拷贝整个数组。如果不想拷贝,可以传入指针或者切片。
4. 切片
- 切片类似于数组中某一段的引用,类型
[]T
表示一个元素类型为T的切片。 - 可以通过两个下标来从数组或切片创建切片
a[low:high]
,左闭右开。上下界都可省略,下界默认为0,上界默认为长度。 [3]bool{true, true, false}
会创建一个数组,[]bool{true, true, false}
会创建一个和前者相同的数组,然后返回一个它的切片。- 切片具有属性:长度和容量,长度是指它包含的元素个数,容量是指从它的第一个元素开始,到底层数组末尾的元素个数。可以用
len(s)
和cap(s)
获得。 - 切片的长度可以扩展,但不能超过容量。如果截去切片右侧,它的长度减少容量不变;如果截去切片左侧,它的长度和容量都会减少。
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// 截取切片使其长度为 0
s = s[:0]
printSlice(s)
// 拓展其长度
s = s[:4]
printSlice(s)
// 舍弃前两个值
s = s[2:]
printSlice(s)
// 舍弃前两个值
s = s[2:]
printSlice(s)
// 扩展长度
s = s[:2]
printSlice(s)
// 扩展长度,panic
s = s[:4]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
- 切片的零值是nil,长度和容量都是0且没有底层数组。
- 可以用make函数创建切片,
make(数组类型,长度,容量=长度)
,如make([]int,5)
、make([]int,0,5)
- 创建二维切片
board := [][]string{ []string{"_", "_", "_"}, []string{"_", "_", "_"}, []string{"_", "_", "_"}, }
- 使用
append([]T, T...)
可以向切片末尾追加元素。如果切片的长度小于容量,会将底层数组的值修改并增加长度;如果底层数组不足以容纳,就会分配一个更大的数组。 - for循环的range形式可以用来遍历切片或映射,第一个值是元素下标,第二个值是元素副本.不需使用时可以用
_
来占位。只写一个时默认为元素下标。
package main
import "fmt"
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)
}
}
- 函数
copy(dst, src []T) int
会将切片src中的元素拷贝到dst中,并返回拷贝元素个数(两者长度的较小值)。这个函数能够正确处理src和dst对应同一个底层数组时的情况。 - 使用切片可能会遇到的一个坑:一个很大的数组,我们只需要它中间的一小部分作为切片使用,但这个大数组并不会被gc回收。解决方法是新建一个切片然后进行一次拷贝。
- 切片本质上是一个结构体
地址、长度,容量
。
参见:Go语言中的切片类型
5. 映射
map[K]V
表示类型映射,映射的零值是nil,nil不能添加键,需要用make初始化。
package main;
import "fmt"
type Vertex struct{
X,Y int
}
var m map[string]Vertex;
func main(){
m = make(map[string]Vertex)
m["qwq"] = Vertex{1,2};
fmt.Println(m)
}
- 映射也可以通过列举初始元素来创建,注意最后一个元素后要么紧跟右花括弧,要么是一个逗号,不能为换行符。
package main
import "fmt"
type Vertex struct {
X,Y int
}
var m = map[string]Vertex{
"qaq": {1,2},
"qwq": Vertex{3,4},
"quq": Vertex{X:1},
}
func main() {
fmt.Println(m)
}
- 插入修改略,删除
delete(m,key)
,检查存在elem, ok = m[key]
.如果不存在,那么ok是false,elem是key类型的零值。与C++的不同之处在于下标访问本身不代表插入。
高阶函数
- 函数也是值,可以作为参数、变量、返回值。
package main
import "fmt"
func compute(fn func(int,int) int, x, y int) int {
return fn(x,y)
}
func main() {
add := func(x, y int) int {
return x + y;
}
mul := func(x, y int) int {
return x * y;
}
sub := func(x, y int) int {
return x - y;
}
fmt.Println(compute(add, 3, 4))
fmt.Println(compute(mul, 4, 5))
fmt.Println(compute(sub, 5, 6))
}
- 闭包: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 关键字和方法名之间。
在此例中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。
package main
import "fmt"
type Vertex struct{
X, Y int
}
func (v Vertex) sum() int {
return v.X + v.Y
}
func main() {
s := Vertex{1, 2}
fmt.Println(s.sum())
}
- 本质上,方法就是把函数的一个参数移动到了接收者参数里,注意接收者参数也是值传递;
- 只能为同一包中定义的类型声明方法,可以通过类型别名如
type MyFloat float64
来声明方法; - 指针接收者:
- 可以声明具有指针接收者的方法,这样定义可以修改接收者指向的值。
- 用法上和值接收者完全相同,因为指针也可以通过
.
来访问字段。 - 传入参数时,既可以传入指针,也可以传入值,会按接收者类型来决定是指针还是值。
- 这也意味着,方法
func (T) f()
与func (*T) f()
是不可共存的。 - 注意,函数
func f(T)
与func f(*T)
是可以共存的,第三点也不成立,值只能接受值,指针只能接受指针。 - 指针接收者和值接收者只在一种概念上有区分,就是实现接口时。比如接口a定义了方法f,类型
*T
实现了方法f,那么a类型的接口变量可以存储一个*T
变量,而不能存储T
变量。
package main
import (
"fmt"
"math"
)
type Abser interface {
Abs() float64
}
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f // a MyFloat 实现了 Abser
a = &v // a *Vertex 实现了 Abser
// 下面一行,v 是一个 Vertex(而不是 *Vertex)
// 所以没有实现 Abser。
a = v
fmt.Println(a.Abs())
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
接口
- 接口类型是由一组方法签名定义的集合。接口类型的变量可以保存任何实现了这些方法的值。
type Abser interface {
Abs() float64
}
- 类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明,也就没有“implements”关键字。
隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。
因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。
package main
import "fmt"
type I interface {
M()
}
type T struct {
S string
}
// 此方法表示类型 T 实现了接口 I,但我们无需显式声明此事。
func (t T) M() {
fmt.Println(t.S)
}
func main() {
var i I = T{"hello"}
i.M()
}
- 接口值。接口值是一个二元组
<底层值,底层类型>
,接口值调用方法时会调用其底层类型对应的方法。 - 底层值可以为nil,底层类型仍然存在,仍然可以调用对应的方法,但需要处理nil。
package main
import "fmt"
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
func main() {
var i I
var t *T
i = t
describe(i)
i.M()
i = &T{"hello"}
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
- 如果接口值本身为nil,调用方法就会产生运行时错误。
- 空接口也就是没有指定方法的接口,可以保存任意类型的值,常用来处理未知类型的参数。(下面这个例子实现了动态类型?)
package main
import "fmt"
func main() {
var i interface{}
describe(i)
i = 42
describe(i)
i = "hello"
describe(i)
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
接口的类型判断
- 类型断言:
t := i.(T)
断言接口值i保存了一个T类型的值,并将其赋予t。
若 i 并未保存 T 类型的值,该语句就会触发一个恐慌。 - 为了判断一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
t, ok := i.(T)
若 i 保存了一个 T,那么 t 将会是其底层值,而 ok 为 true。
否则,ok 将为 false 而 t 将为 T 类型的零值,程序并不会产生恐慌。 - 类型选择 是一种按顺序从几个类型断言中选择分支的结构。
类型选择与一般的 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 的接口类型和值相同。
package main
import "fmt"
func show(i interface{}) {
switch v := i.(type){
case int:
fmt.Printf("oh, double of it is %v !\n", v)
case string:
fmt.Printf("%q has length %v !\n", v, len(v))
default:
fmt.Printf("stupid lhz don't know anything about %T\n", v)
}
}
func main(){
show(21)
show("qwq")
show(struct{X,Y int} {3,4})
}
接口举例
- fmt.Stringer接口,用字符串描述自己,用于格式化输出。
type Stringer interface { String() string }
- error接口。通常函数会返回一个error值,可以通过检查error是否为nil来进行错误处理。
我对error的理解是,制定了一个公用的接口,使得想要返回异常值的函数直接返回error即可。并且Error()方法返回的字符串也具有描述自己的作用。type error interface { Error() string }
- 定义结构体,实现Error方法
- 函数中遇到问题时返回error结构体(或指针)。
- 上级函数判断error变量是否为空,进行对应操作(如fmt.Printf)
- io.Reader接口。表示从数据流的末尾进行读取。它有一个Read方法。
Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误。func (T) Read(b []byte) (n int, err error)
package main import ( "fmt" "io" "strings" ) func main() { r := strings.NewReader("Hello, Reader!") b := make([]byte, 8) for { n, err := r.Read(b) fmt.Printf("n = %v err = %v b = %v\n", n, err, b) fmt.Printf("b[:n] = %q\n", b[:n]) if err == io.EOF { break } } }
- image 包定义了 Image 接口:
注意: Bounds 方法的返回值 Rectangle 实际上是一个 image.Rectangle,它在 image 包中声明。package image type Image interface { ColorModel() color.Model Bounds() Rectangle At(x, y int) color.Color }
color.Color 和 color.Model 类型也是接口,但是通常因为直接使用预定义的实现 image.RGBA 和 image.RGBAModel 而被忽视了。这些接口和类型由 image/color 包定义。
并发
Go程
- Go 程(goroutine)是由 Go 运行时管理的轻量级线程。
go f(x, y, z)
将创建一个新的go程并执行f(x,y,z)
,其中f、x、y、z的求值发生在当前go程,执行发生在新的go程中。- go程在相同的地址空间中运行。
信道
- 信道是带有类型的管道,可以通过信道操作符
<-
来发送或接受值。(箭头就是数据流向)ch <- v // 将 v 发送至信道 ch。 v := <-ch // 从 ch 接收值并赋予 v。
- 信道通过make创建:
ch := make(chan int)
. - 默认情况下,发送和接受端在另一端准备好前会阻塞。
package main import "fmt" func cal(arr []int, ch chan int) { res := 0 for _, v := range arr{ res += v } ch <- res } func main() { a := []int{1, 5, 2, 4, 3, 6} ch := make(chan int) go cal(a[:len(a)/2], ch) go cal(a[len(a)/2:],ch) x, y := <-ch, <-ch fmt.Println(x+y) }
- 信道可以是带缓冲的,缓冲区大小由make的第二个参数指定。当缓冲区满时,发送方会被阻塞;当缓冲区空时,接收方会被阻塞。
- 发送者可通过 close函数 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完
v, ok := <-ch
后ok的值将变为false. - 循环
for i := range c
会不断从信道接受数据,直到它被关闭。 - 注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。
select
- select 语句使一个 Go 程可以等待多个通信操作。
select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。 - 当 select 中的其它分支都没有准备好时,default 分支就会执行。
为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:select { case i := <-c: // 使用 i default: // 从 c 中接收会阻塞时执行 }
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
sync.Mutex
- 当多个go程不需要通信,只需要同时访问一个变量且避免冲突,即互斥的情况下,就需要引入互斥锁(mutex).
- Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:Lock、Unlock。
- 在代码前调用Lock,代码后调用Unlock即可保证代码的互斥执行;可以使用defer来保证互斥锁一定会被解锁。
- 下面的例子是一个线程安全的map.
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
c.mux.Unlock()
}
// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.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"))
}
注:所有练习总结于 A Tour of Go - 练习篇