Go语言之旅
基础
包、变量、函数
包
每个Go程序都有包构成,程序从main包开始运行,它标志着程序的入口点,定义一个可执行程序,从main函数开始运行执行程序,如果写的是一个库或者工具包,其包名一般根据其功能命名,但这些包不会包含main函数,因此不能作为程序入口
即如果要运行就需要该文件是package main,并且含有main函数
如果多个文件属于同一个包,它们之间是可以直接访问的,不需要显式的导入
按照约定,包名和导入路径的最后一个元素一致,即要调用包内的函数时,需要以最后一个元素开头
本程序通过导入路径”fmt“和”math/rand“来使用这两个包
1
2
3
4
5
6
7
8
9
10
11
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println(rand.Intn(10))
}
导入
此代码用圆括号将导入的包分为一组,这是“分组”形式的导入语句
1
2
3
4
5
6
7
8
9
10
11
package main
import (
"fmt"
"math"
)
func main() {
fmt.Printf("现在你有了 %g 个问题。\n", math.Sqrt(7))
}
导出名
在Go中,如果一个名字以大写字母开头,那么它就是已导出的名,例如Pi,就是导出自math包
值得注意的是同一个目录下只能包含一个包,即在同一个目录中不应该有多个package声明
如果要导入其他包内的函数,需要注意这个包需要是一个可以导入的库包,不能是一个可执行程序,即不能包含main函数,导入的时候是导入库函数所在的目录,接着在程序中用目录名.函数的方式调用,函数首字母必须大写,才表示公开的,否则只在定义它的包内可见
1
2
3
4
5
6
7
8
9
10
11
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Pi)
}
函数
注意类型在变量名后面,这是因为如果按照c的类型定义在前面的方式,遇到比较复杂的定义时就会显得很复杂,比如定义一个函数指针int (fp)(int (ff)(int x,int y),int b),这就会显得很复杂,如果删除函数名称也会很难理解,所以go采用放后面的形式,这样从左往右读起来很好
(ps:当连续多个已命名行参类型相同时,除最后一个类型外,其他都可以省略,例如add2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
fmt.Println(add(1, 2))
}
func add(x int, y int) int {
return x + y
}
func add2(x, y int) int {
return x + y
}
多返回值
函数可以返回任意数量的返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a := "hello"
b := "world"
fmt.Println("before:", a, b)
fmt.Println(swap(a, b))
}
带名字的返回值
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))
}
变量
var语句用于声明一系列变量,和函数的参数列表一样,类型放在最后
变量的声明可以包含初始值,每个变量对应一个,如果提供了初始值,则类型可以省略,变量可以从初始值中推断出类型
在函数中,短赋值语句:=可隐式确定类型替代使用var,但在函数外是不能使用:=的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
var c, python, java bool
//函数外声明变量只能使用var
var x=1
func main() {
var i int
//函数内使用
y:=2
//0 false false false 1 2
fmt.Println(i, c, python, java, x, y)
}
基本类型
Go的基本类型有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名
rune // int32 的别名
// 表示一个 Unicode 码位
float32 float64
complex64 complex128
没有明确初始化的变量声明会被赋予对应类型的零值,比如数值类型的零值是0,布尔类型的零值是false,字符串为空字符串
类型转化
Go在不同类型的项之间赋值时需要显示转化
例如下面在Sqrt函数中就需要显式转化为float64
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
"fmt"
"math"
)
func main() {
var x, y int = 3, 4
var f float64 = math.Sqrt(float64(x*x + y*y))
var z = (f)
fmt.Println(x, y, z)
}
在声明一个变量而不指定其类型时,变量的类型会通过右值推断出来
1
2
var i int
j := i // j 也是一个 int
但当右边包含未指明类型的数值常量时,新变量的类型就可能是int、float64或complex128了,这取决于常量的精度,例如
1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
func main() {
i := 42 // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128
fmt.Printf("type:%T,%T,%T\n", i, f, g)
}
常量
常量的声明和变量类似,只不过是使用const关键字
常量可以是字符,字符串,布尔值或者数值
常量不能使用:=语法声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
const Pi = 3.14
func main() {
const World = "世界"
fmt.Println("Hello", World)
fmt.Println("Happy", Pi, "Day")
const Truth = true
fmt.Println("Go rules?", Truth)
}
数值常量
数值常量是高精度的值,一个未指定类型的常量由上下文来决定其类型
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
package main
import "fmt"
const (
// 将 1 左移 100 位来创建一个非常大的数字
// 即这个数的二进制是 1 后面跟着 100 个 0
Big = 1 << 100
// 再往右移 99 位,即 Small = 1 << 1,或者说 Small = 2
Small = Big >> 99
)
func needInt(x int) int {
return x*10 + 1
}
func needFloat(x float64) float64 {
return x * 0.1
}
func main() {
fmt.Println(needInt(Small))
fmt.Println(needFloat(Small))
fmt.Println(needFloat(Big))
}
流程控制语句
for
Go只有一种循环结构:for循环
基本的for循环由三部分组成,用分号隔开
和java等语言不同的是Go的for语句后面的三个构成部分没有小括号,大括号{}则是必须的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "fmt"
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
//初始化语句和后置语句是可选的
for ; sum < 1000; {
sum += sum
}
//对于上面这种就可以直接去掉分号,类似于while
for sum<10000 {
sum+=sum
}
//省略循环条件就是无限循环
for{
}
fmt.Println(sum)
}
if
Go的if语句和for循环类似,表达式外无需小括号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
"math"
)
func sqrt(x float64) string {
if x < 0 {
return sqrt(-x) + "i"
}
return fmt.Sprint(math.Sqrt(x))
}
func main() {
fmt.Println(sqrt(2), sqrt(-2))
}
和for一样,if语句可以在条件表达式前执行一个简短语句,且该语句声明的变量作用域仅在if内和其对应的任何else块中
1
2
3
4
5
6
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}
练习:利用牛顿公式写求平方根方法
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
package main
import (
"fmt"
"math"
)
func Sqrt(x float64) float64 {
z := 1.0
for i := 1; i < 10; {
z -= (z*z - x) / (2 * z)
fmt.Println(z)
i++
}
return z
}
func Sqrt1(x float64) float64 {
z := x/2
for math.Abs(x-z*z) > 0.00000000001 {
z -= (z*z - x) / (2 * z)
fmt.Println(z)
}
return z
}
func main() {
fmt.Println(Sqrt1(10))
}
switch分支
与Java等语言不同,Go只会运行选定的case,而非之后的所有case,相当于自动添加了break,且Go的case无需未常量,且取值不限于整数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Print("Go 运行的系统环境:")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("macOS.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}
case语句从上到下依次顺序执行,知道匹配成功时停止
switch也可以无条件执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("早上好!")
case t.Hour() < 17:
fmt.Println("下午好!")
default:
fmt.Println("晚上好!")
}
}
defer
defer语句会将函数推迟到外层函数返回之后执行
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用
1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
func main() {
//hello
//world
defer fmt.Println("world")
fmt.Println("hello")
}
其本质是推迟调用的函数会被压入一个栈中,当外层函数返回时,被推迟的调用会按照先进后出的顺序调用
更多类型
指针
指针保存了值的内存地址,类型*T是执行T类型值的指针,其零值是nil
&会生成一个指向其操作数的指针,*操作符表示指针指向的底层值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import "fmt"
func main() {
i, j := 42, 2701
p := &i // 指向 i
fmt.Println(*p) // 通过指针读取 i 的值
*p = 21 // 通过指针设置 i 的值
fmt.Println(i) // 查看 i 的值
p = &j // 换了一个指向,改成指向 j
*p = *p / 37 // 通过指针对 j 进行除法运算
fmt.Println(j) // 查看 j 的值
不过与c不同,Go没有指针运算
结构体
一个结构体就是一组字段
结构体字段可通过.来访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() {
//{1 2}
fmt.Println(Vertex{1, 2})
v := Vertex{1, 2}
v.X = 4
//4
fmt.Println(v.X)
}
结构体字段可通过结构体指针来访问,如果有一个指向结构体的指针,则可通过(*p).X访问,为了简洁,语言也允许使用隐式解引用,直接写p.X也可以访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
//如果不用&,那么修改p.X是不会影响v的
p := &v
p.X = 1e9
//{1000000000 2}
fmt.Println(v)
}
在对结构体内字段赋值的时候,可以仅对其中的一部分进行赋值,其他剩余的字段会被隐式地赋予零值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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() {
//{1 2} &{1 2} {1 0} {0 0}
fmt.Println(v1, p, v2, v3)
}
数组
类型[n]T表示一个数组,数组的长度是其类型的一部分,因此数组不能改变大小
和java不同的是go如果输出数组名会输出整个数组,而不是数组的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
//也可以在声明的时候初始化
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}
切片
数组的大小是固定的,所以引入了切片,为数组元素提供了动态大小的、灵活的视角
1、通过切割数组创建
1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
func main() {
//定义一个数组
primes := [6]int{2, 3, 5, 7, 11, 13}
//对数组进行分割,这里是左开右闭
var s []int = primes[1:4]
fmt.Println(s)
}
但如果是这种方式创造切片的话,切片就相当于数组的引用,即更改切片元素也会修改其底层数组中对应的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
func main() {
s := [4]int{1, 2, 3, 4}
s1 := s[0:2]
s2 := s[1:3]
s2[0] = 0
//[1 0 3 4]
fmt.Println(s)
//[1 0]
fmt.Println(s1)
//[0 3]
fmt.Println(s2)
}
2、通过切片字面量创建
这个步骤是先创建一个数组,然后在构建一个引用了这个数组的切片
1
x:=[]bool{true, true, false}
创建的切片可以是任何类型,甚至可以是结构体类型,或者其他切片
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
package main
import "fmt"
type x struct {
a, b int
}
func main() {
q := []int{2, 3, 5, 7, 11, 13}
fmt.Println(q)
ss := []struct {
a, b int
}{
{1, 2},
{3, 4},
}
fmt.Println(ss)
ss1 := []x{
{1, 2},
{2, 3},
}
fmt.Println(ss1)
//创建一个井字棋
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
// 两个玩家轮流打上 X 和 O
board[0][0] = "X"
board[2][2] = "O"
board[1][2] = "X"
board[1][0] = "O"
board[0][2] = "X"
}
进行切片的时候,可以利用其默认行为来忽略上下界,即a[0:10]、a[:10]、a[0:]等
切片是一个引用,所以可以不断更换引用,类似于下面的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
//创建切片[3 5 7]
s = s[1:4]
fmt.Println(s)
//在上一个切片的基础上进行切割,创建切片[3 5]
s = s[:2]
fmt.Println(s)
//[5]
s = s[1:]
fmt.Println(s)
}
切片拥有长度和常量,切片的长度就是它所包含元素的个数,可通过len和cap来获取长度和容量
可通过重新切片来扩展一个切片,给它提供足够的容量
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
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
//len=6 cap=6 [2 3 5 7 11 13]
printSlice(s)
// 截取切片使其长度为 0
s = s[:0]
//len=0 cap=6 []
printSlice(s)
// 扩展其长度
s = s[:3]
//len=3 cap=6 [2 3 5]
printSlice(s)
// 舍弃前两个值
s = s[2:]
//len=1 cap=4 [5]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
切片零值nil,nil切片的长度和容量为0且没有底层数组
1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
3、用make创建切片
切片可以用内置函数make创建,会分配一个元素为零值的数组并返回一个引用了它的切片
1
make([]T, length, capacity)
这里面T是切片中元素类型,length是切片初始长度,表示切片当前包含元素数量,capacity是初始容量,表示切片底层数组的大小,决定了切片最多可以容纳多少个元素
容量决定了底层数组的大小,即使长度为0,容量也是固定的,当切片长度超过当前容量的时候,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
package main
import "fmt"
type x struct {
a, b int
}
func testSlice() {
//可以用结构体作为类型
tests := make([]x, 5)
fmt.Println(tests)
}
func main() {
//只写了一个5,代表长度和容量都为5
a := make([]int, 5)
//a len=5 cap=5 [0 0 0 0 0]
printSlice("a", a)
b := make([]int, 0, 5)
//b len=0 cap=5 []
printSlice("b", b)
c := b[:2]
//c len=2 cap=5 [0 0]
printSlice("c", c)
d := c[2:5]
//d len=3 cap=3 [0 0 0]
printSlice("d", d)
}
func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n",
s, len(x), cap(x), x)
}
为切片追加元素
append函数可以为切片追加新的元素,append的第一个参数s是一个元素类型T的切片,其余类型为T的值将会追加到该切片的末尾
append的结果是一个包含原切片所有元素加上新添加元素的切片,如果原数组容量不够就会自动分配一个更大的数组,返回的切片会指向这个新分配的数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
func main() {
s := make([]int, 0, 10)
s = append(s, 4, 5)
printSlice1(s)
}
func printSlice1(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
range遍历
for循环的range形式可遍历切片或映射
使用range时,每次迭代都会返回两个值,第一个值为当前元素的下标,第二个值为该下标所对应的一份副本
1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
var num = []int{1, 2, 3, 4, 5}
func main() {
for i, v := range num {
fmt.Println(i, v)
}
}
可以使用_来忽略下标或者副本
1
2
3
for _, v := range num {
fmt.Println(i, v)
}
练习:实现 Pic
。它应当返回一个长度为 dy
的切片,其中每个元素是一个长度为 dx
,元素类型为 uint8
的切片。当你运行此程序时,它会将每个整数解释为灰度值 (好吧,其实是蓝度值)并显示它所对应的图像。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "golang.org/x/tour/pic"
func Pic(dx, dy int) [][]uint8 {
image := make([][]uint8, dy)
for y := 0; y < dy; y++ {
row := make([]uint8, dx)
for x := 0; x < dx; x++ {
row[x] = uint8((x * y) % 256)
}
image[y] = row
}
return image
}
func main() {
pic.Show(Pic)
}
这里要用make来创建切片,不能用image := [][]uint8{}的方式,因为虽然切片是动态的,但是这种方式不能直接通过索引访问超出当前长度的元素
map映射
map映射将键映射到值,可以使用make函数来初始化映射,分配必要的内存,使其可以存储键值对
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
package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
//这里只是声明,但没有初始化,此时m的值是nil,一个nil的映射无法存储键值对
//因此必须使用make或字面量初始化映射,使其指向一个有效空间
var m map[string]Vertex
func main() {
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"])
mm := map[string]Vertex{
"Bell Labs": {
Lat: 40.68433,
Long: -74.39967,
},
"Google": {
Lat: 37.42202,
Long: -122.08408,
},
}
fmt.Println(mm["Bell Labs"])
}
映射和结构体类似,但区别在于结构体的键在初始化的时候可以选择性的赋值,如果忽略某些字段就会使用该类型的零值,而映射必须显示的执行每个键及其对应的值
修改映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import "fmt"
func main() {
m := make(map[string]int)
//增
m["答案"] = 42
fmt.Println("值:", m["答案"])
//改
m["答案"] = 48
fmt.Println("值:", m["答案"])
//删
delete(m, "答案")
//值: 0
fmt.Println("值:", m["答案"])
v, ok := m["答案"]
//值: 0 是否存在? false
fmt.Println("值:", v, "是否存在?", ok)
}
练习:实现 WordCount
。它应当返回一个映射,其中包含字符串 s
中每个“单词”的个数。 函数 wc.Test
会为此函数执行一系列测试用例,并输出成功还是失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"golang.org/x/tour/wc"
"strings"
)
func WordCount(s string) map[string]int {
ss:=strings.Split(s," ")
words := make(map[string]int)
for i := 0; i < len(ss); i++ {
words[string(ss[i])]++
}
return words
}
func main() {
wc.Test(WordCount)
}
函数值
函数也是值,可以像其他值一样传递,函数值可以用作函数的参数或返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
"math"
)
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
//传递了一个函数,接着用compute里面的值通过传递的函数进行计算
fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}
函数闭包
Go函数可以是一个闭包,闭包是一个函数值,其引用了其函数体之外的变量,该函数可以访问并赋予其引用的变量值
闭包的创建方式:通过函数内部返回一个匿名函数(或者说返回一个函数值),并且这个匿名函数能够访问外部函数的变量。在Go语言中,闭包的创建非常灵活,通常的方式就是通过一个函数返回另一个函数,同时这个返回的函数可以访问并操作外部函数的局部变量
例1:下面的adder函数返回了一个闭包,每个闭包都被绑定在其各自的sum变量上
- 内部变量sum用于累加传入的值
return func(x int) int { ... }
返回一个闭包,这个匿名函数可以访问并修改外部函数adder
中的变量sum
。- 每次调用返回的函数时,都会将传入的参数
x
加到sum
上,然后返回更新后的sum
值。 - 这种设计使得每个由
adder
生成的函数都有自己的sum
变量,互不干扰。
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
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),
)
}
}
//输出
/*
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90
*/
例2:使用闭包维护状态
adder(x) 是一个工厂函数,它返回一个匿名函数,该匿名函数每次被调用时会加上
x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
// 返回一个加法器闭包
func adder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
func main() {
add5 := adder(5) // 创建一个加5的闭包,x就
fmt.Println(add5(3)) // 输出 8
fmt.Println(add5(10)) // 输出 15
add10 := adder(10) // 创建一个加10的闭包
fmt.Println(add10(5)) // 输出 15
}
例3:事件处理,闭包也常用于事件驱动的编程中,例如处理异步回调、事件监听等,下面是模拟一个异步任务回调:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
// 模拟异步任务,任务完成后调用回调
func asyncTask(callback func(result string)) {
// 模拟任务完成
result := "Task Completed!"
callback(result) // 执行回调
}
func main() {
// 创建一个闭包来处理任务结果
callback := func(result string) {
fmt.Println("Callback received:", result)
}
asyncTask(callback) // 执行异步任务
}
练习:斐波那契闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "fmt"
// fibonacci 是返回一个「返回一个 int 的函数」的函数
func fibonacci() func() int {
//这代表了记住x,y,会一直维持当前状态
x := 0
y := 1
return func() int {
next := x
x, y = y, x+y
return next
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
方法与接口
方法与接口
方法
Go没有类的概念,不过可以为类型定义方法,方法就是一类带特殊的接受者参数的函数
方法接收者在它自己的参数列表内,位于func关键字和方法名之间
这和java中的类很像,类中的成员方法可以使用类中的成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}
方法的定义就是为了方便使用类中的变量等资源,即方法只是个带接受者参数的函数,如果把上面的结构体的位置放到方法的括号中,就是普通函数了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(Abs(v))
}
非结构体类型声明方法
在下面代码中,可以看到一个带Abs方法的数值类型的MyFloat,只能为同一个包中定义的接收者类型声明方法,而不能为其他别的包中定义类型和声明方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
"math"
)
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}
指针类型的接受者声明方法
对于某类型T,接收者的类型可用*T的方式(T本身不能是指针,比如不能是 *int),这样的好处在于可以结构体中的变量的值,这样在其他方法中也可以使用修改之后的值,否则这个变量值是不互通的
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
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
//这个方法的作用是将结构体中变量的值扩大10倍
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
//如果使用的是指针类型,那么这个扩大10后的变量在Abs也是扩大10倍后的,否则就不会影响
v.Scale(10)
//带*输出50,不带*输出5
fmt.Println(v.Abs())
}
总结:若使用值接受者,那么Scale方法会对原始Vertex值的副本进行操作,如果是指针接受者就可以更改main函数中声明的Vertex的值
也可以将上面的例子中的方法改为函数,如下所示,
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
package main
import (
"fmt"
"math"
)
type Vertex1 struct {
X, Y float64
}
func Abs(v Vertex1) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func Scale(v *Vertex1, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex1{3, 4}
Scale(&v, 10)
fmt.Println(Abs(v))
}
注意⚠️:上面由于结构体是值类型,所以当结构体作为参数传递给函数时,默认传递的是结构体的副本,所以上面传递的是结构体的指针。但对于切片来说,切片是一个引用类型,所以当切片作为参数传递给函数时,传递的是切片本身的拷贝,即函数中修改是对切片底层数组的修改会影响到原切片
方法与指针重定向
对于函数而言,如果是带指针的参数,传入参数是需要带&的,但对于方法而言,接收者即能是值也能是指针
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
package main
import "fmt"
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func ScaleFunc(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
//作为接收者v可以是值类型
v.Scale(2)
//但作为参数传递时,v作为值类型就不可以了,就需要加入&
ScaleFunc(&v, 10)
//p定义是一个引用类型
p := &Vertex{4, 3}
//这个时候既可以作为接收者直接调用,也可以作为参数直接传递
p.Scale(3)
ScaleFunc(p, 8)
fmt.Println(v, p)
}
使用指针接收者的好处在于这样的话方法能够修改其接收者指向的值,并且这样就可以避免每次调用方法时赋值该值,所以以后定义方法时,可以都写成指针接收型
接口
接口类型的定义为一组方法签名,即接口是通过列出方法签名来定义的
1
2
3
4
5
type Shape interface {
Area() float64
Perimeter() float64
}
上面,任何类型,只要实现了Area和Perimeter两个方法,就可以被认为是Shape类型
接口类型的变量可以持有任何实现了这些方法的值,即接口是一个抽象层,接口变量可以存储实现了接口的任何类型的值,只要实现了接口中的方法,就可以持有
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
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
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)
}
因为只需要通过实现接口中所有的方法就可以实现这个接口,不需要专门显示声明,也就没有“implements”关键字,这样的好处在于从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()
}
接口也是值,即可以像值一样传递,可以用作函数的参数或返回值
接口值保存了一个具体底层类型的具体值,调用方法时会执行其底层类型的同名方法,有点像java的多态
下面代码接口I被赋值了两种不同的类型(*T和F),每次赋值,接口变量i的动态类型和动态值都会更新
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
package main
import (
"fmt"
"math"
)
type I interface {
M()
}
type T struct {
S string
}
//因为这里方法的接收者是指针类型,所以方法的绑定需要用&来创建指针的值
func (t *T) M() {
fmt.Println(t.S)
}
type F float64
//这里接收者是值类型,所以传递给接口的也是值类型
func (f F) M() {
fmt.Println(f)
}
func main() {
var i I
//方法是指针类型
i = &T{"Hello"}
describe(i)
i.M()
//方法是值类型
i = F(math.Pi)
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
底层值为nil的接口值
即便接口内的具体值为nil,方法仍然会被nil接收者调用,这在一些语言中会触发一个空指针异常,所以可以像下面M方法一样加一个if判断
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
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是一个空指针
i = t
describe(i)
//⚠️所以在调用M方法时,会进行空值判断,因为这个是指针,所以要加上一个if进行空指针判断
//如果最开始方法接收者是值类型,那么赋值给接口的时候也是值类型,就可以不加if也不会报错
//所以这些都取决于方法是什么接收者类型,传给接口的就是什么类型
i.M()
i = &T{"hello"}
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
nil接口值
nil接口值既不保存值也不保存具体类型,为nil接口调用方法会产生运行时错误,因为接口的远足内并未指定该调用哪个具体方法的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
type I interface {
M()
}
func main() {
var i I
//(<nil>, <nil>)
describe(i)
//报运行时错误
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
空接口
指定了零个方法的接口值被称为空接口,因为是零方法,所以空接口可以保存任何类型的值,可被用来处理位置类型的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
}
类型断言
提供了访问接口值底层具体值的方式
1
t := i.(T)
该语句断言接口值i保存了具体类型T,并将其底层类型为T的值赋予变量t
若i不是T类型的值,该语句会触发一个panic(panic是go中运行时异常机制,用于表示程序执行过程中遇到了无法继续的错误情况,触发后,程序正常执行流程将中断,并开始执行defer)
所以为了判断一个接口值是否是某种类型,并且不触发panic,可以使用下面的方式,这样就可以返回两个值:其底层值以及一个报告断言是否成功的布尔值
1
t, ok := i.(T)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "fmt"
func main() {
var i interface{} = "hello"
s := i.(string)
//hello
fmt.Println(s)
s, ok := i.(string)
//hello true
fmt.Println(s, ok)
f, ok := i.(float64)
//0 false
fmt.Println(f, ok)
f = i.(float64) // panic
fmt.Println(f)
}
类型选择
类型选择是一种按顺序从几个类型断言中选择分支的结构
类型选择与一般的switch语句相似,不过类型选择中的case为类型,针对给定接口值所存储的值的类型进行比较
类型选择中的声明与类型断言i.(T) 的语法相同,只是具体类型 T
被替换成了关键字 type
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"
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() {
//二倍的 21 是 42
do(21)
//"hello" 长度为 5 字节
do("hello")
//我不知道类型 bool!
do(true)
}
Stringer
fmt包中定义的Stringer是最普遍的接口之一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z)
}
练习:Stringer
通过让 IPAddr
类型实现 fmt.Stringer
来打印点号分隔的地址。
例如,IPAddr{1, 2, 3, 4}
应当打印为 "1.2.3.4"
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import "fmt"
type IPAddr [4]byte
// TODO: 为 IPAddr 添加一个 "String() string" 方法。
func (ip IPAddr) String() string {
return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
}
func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
fmt.Println(ip.String())
}
}
错误
Go使用error值来表示错误状态,error类型是一个内建接口
通常函数会返回一个error值,调用它的代码应该判断这个错误是否等于nil来进行错误处理
像error这种接口,可以定义专门的接口体去实现接口,来达到特定的效果,这是接口的常用的方式
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
package main
import (
"fmt"
"time"
)
type MyError struct {
When time.Time
What string
}
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}
func run() error {
//这里因为error是个接口,并且方法接收类型是指针型,所以下面要用&
//如果是个普通结构体类型,就可以不用&
return &MyError{
time.Now(),
"it didn't work",
}
}
func main() {
if err := run(); err != nil {
//at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn't work
fmt.Println(err)
}
}
练习:错误
修改sqrt方法,使其能返回error值
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
package main
import (
"fmt"
"math"
)
type ErrSqrt float64
func (e ErrSqrt) Error() string {
return fmt.Sprintf("cannot Sqrt negative number: %f", e)
}
func Sqrt(x float64) (float64, error) {
if x < 0 {
return x, ErrSqrt(x)
}
return math.Sqrt(x), nil
}
func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}
Readers
io包指定了io.Reader接口,表示数据流的读取端
Read用数据填充给定的字节切片并返回填充的字节数和错误值,在遇到数据流结尾时,会返回一个io.EOF错误
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
package main
import (
"fmt"
"io"
"strings"
)
func main() {
//创建一个strings.Reader 类型的实例 r,它实现了 io.Reader 接口,允许我们像读取文件一样逐步读取字符串
r := strings.NewReader("Hello, Reader!")
b := make([]byte, 8)
//启动无限循环,用于从r中读取数据,直到读取完成
for {
//每次从r中读取8个字节,并将数据存储在b中
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
}
}
}
练习:Reader
实现一个 Reader
类型,它产生一个 ASCII 字符 'A'
的无限流。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "golang.org/x/tour/reader"
type MyReader struct{}
// TODO: 为 MyReader 添加一个 Read([]byte) (int, error) 方法。
func (r MyReader) Read(s []byte) (int, error) {
// 将传入的 p 数组填充为 'A'
for i := range s {
s[i] = 'A'
}
// 返回填充的字节数
return len(s), nil
}
func main() {
reader.Validate(MyReader{})
}
练习:rot13Reader
有种常见的模式是一个 io.Reader
包装另一个 io.Reader
,然后通过某种方式修改其数据流。
例如,gzip.NewReader
函数接受一个 io.Reader
(已压缩的数据流)并返回一个同样实现了 io.Reader
的 *gzip.Reader
(解压后的数据流)。
编写一个实现了 io.Reader
并从另一个 io.Reader
中读取数据的 rot13Reader
,通过应用 rot13 代换密码对数据流进行修改。
rot13Reader
类型已经提供。实现 Read
方法以满足 io.Reader
。
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
package main
import (
"io"
"os"
"strings"
)
type rot13Reader struct {
r io.Reader
}
func (r rot13Reader) Read(p []byte) (n int, err error) {
n, err = r.r.Read(p)
if err != nil {
return n, err
}
//对读取的数据进行ROT13转化
for i := 0; i < n; i++ {
if p[i] >= 'a' && p[i] <= 'z' {
p[i] = 'a' + (p[i]-'a'+13)%26
} else if p[i] >= 'A' && p[i] <= 'Z' {
p[i] = 'A' + (p[i]-'A'+13)%26
}
}
return n, err
}
func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!")
r := rot13Reader{s}
io.Copy(os.Stdout, &r)
}
图像
image包定义了Image接口
1
2
3
4
5
6
7
package image
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
注意: Bounds
方法的返回值 Rectangle
实际上是一个 image.Rectangle
,它在 image
包中声明
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
"image"
)
func main() {
m := image.NewRGBA(image.Rect(0, 0, 100, 100))
fmt.Println(m.Bounds())
fmt.Println(m.At(0, 0).RGBA())
}
练习:图像
定义你自己的 Image
类型,实现必要的方法并调用 pic.ShowImage
。
Bounds
应当返回一个 image.Rectangle
,例如 image.Rect(0, 0, w, h)
。ColorModel
应当返回 color.RGBAModel
。At
应当返回一个颜色。上一个图片生成器的值 v
对应于此次的 color.RGBA{v, v, 255, 255}
。
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
package main
import (
"golang.org/x/tour/pic"
"image"
"image/color"
)
// Image 类型的定义
type Image struct {
w, h int // 图像的宽度和高度
}
// Bounds 方法返回图像的矩形边界
func (m Image) Bounds() image.Rectangle {
// 返回一个从 (0, 0) 到 (m.w, m.h) 的矩形边界
return image.Rect(0, 0, m.w, m.h)
}
// ColorModel 方法返回颜色模型
func (m Image) ColorModel() color.Model {
// 使用 RGBA 颜色模型
return color.RGBAModel
}
// At 方法返回给定 (x, y) 坐标的颜色
func (m Image) At(x, y int) color.Color {
// 根据题目要求,返回一个 RGBA 颜色,其中 R, G 都是 x 和 y 的和,B 固定为 255
// 使用 color.RGBA 结构体
return color.RGBA{
R: uint8(x + y), // 红色通道为 x 和 y 的和
G: uint8(x + y), // 绿色通道为 x 和 y 的和
B: 255, // 蓝色通道固定为 255
A: 255, // Alpha 通道固定为 255
}
}
func main() {
// 创建一个 Image 类型实例
m := Image{w: 256, h: 256}
// 使用 pic.ShowImage 显示图片
pic.ShowImage(m)
}
泛型
类型参数
使用类型参数编写Go函数来处理多种类型,函数的类型参数出现在函数参数之前的方括号之间
1
func Index[T comparable](s []T, x T) int
comparable是一个有用的约束,能让对任意满足该类型的值使用==和!=运算符,就是使用泛型,就不用对于每个类型都写一个的函数
在下面例子中,使用它将值和所有切片元素进行比较,直到找到匹配项,该index函数适用于任何支持比较的类型
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
package main
import "fmt"
// Index 返回 x 在 s 中的下标,未找到则返回 -1。
func Index[T comparable](s []T, x T) int {
for i, v := range s {
// v 和 x 的类型为 T,它拥有 comparable 可比较的约束,
// 因此我们可以使用 ==。
if v == x {
return i
}
}
return -1
}
func main() {
// Index 可以在整数切片上使用
si := []int{10, 20, 15, -10}
fmt.Println(Index(si, 15))
// Index 也可以在字符串切片上使用
ss := []string{"foo", "bar", "baz"}
fmt.Println(Index(ss, "hello"))
}
泛型类型
除了泛型函数外,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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package main
import (
"fmt"
)
// List 表示一个可以保存任何类型的值的单链表。
type List[T comparable] struct {
next *List[T]
val T
}
// Append 向链表末尾追加新值
func (l *List[T]) Append(val T) {
// 遍历到链表的末尾
current := l
for current.next != nil {
current = current.next
}
// 创建新节点并追加到末尾
current.next = &List[T]{val: val}
}
// Insert 在指定位置插入新值(位置从 0 开始)
func (l *List[T]) Insert(index int, val T) {
if index == 0 {
// 在链表头插入
newNode := &List[T]{next: l.next, val: l.val}
l.val = val
l.next = newNode
return
}
// 遍历到指定位置前一个节点
current := l
for i := 0; i < index-1 && current.next != nil; i++ {
current = current.next
}
// 插入新节点
newNode := &List[T]{next: current.next, val: val}
current.next = newNode
}
// Delete 删除指定位置的节点
func (l *List[T]) Delete(index int) {
if index == 0 {
// 删除头节点
if l.next != nil {
l.val = l.next.val
l.next = l.next.next
}
return
}
// 遍历到指定位置前一个节点
current := l
for i := 0; i < index-1 && current.next != nil; i++ {
current = current.next
}
// 删除节点
if current.next != nil {
current.next = current.next.next
}
}
// Find 查找链表中是否存在某值,返回布尔值
func (l *List[T]) Find(val T) bool {
current := l
for current != nil {
if current.val == val {
return true
}
current = current.next
}
return false
}
// Print 打印链表的所有值
func (l *List[T]) Print() {
current := l
for current != nil {
fmt.Printf("%v -> ", current.val)
current = current.next
}
fmt.Println("nil")
}
func main() {
// 创建链表
list := &List[int]{val: 1}
// 追加节点
list.Append(2)
list.Append(3)
list.Append(4)
list.Print() // 输出: 1 -> 2 -> 3 -> 4 -> nil
// 插入节点
list.Insert(2, 99)
list.Print() // 输出: 1 -> 2 -> 99 -> 3 -> 4 -> nil
// 删除节点
list.Delete(2)
list.Print() // 输出: 1 -> 2 -> 3 -> 4 -> nil
// 查找节点
fmt.Println(list.Find(3)) // 输出: true
fmt.Println(list.Find(99)) // 输出: false
}
并发
Go协程
Go程(goroutine)是由Go运行时管理的轻量级线程
Go程在相同的地址空间中运行,因此在访问共享内存时必须进行同步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
信道
带有类型的管道,可以通过它用信道操作符<-来发送或者接收值
1
2
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
和映射与切片一样,信道在使用前必须创建
1
ch := make(chan int)
默认情况下,发送和接收操作在另一端准备好之前都会阻塞,这使得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
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 发送 sum 到 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
//将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从 c 接收
fmt.Println(x, y, x+y)
}
带缓冲的信道
在创建信道的时候,将缓冲长度作为第二个参数提供给make来初始化一个带缓冲的信道
1
ch := make(chan int, 100)
发送操作的行为:
- 当缓冲区未满时,
ch <- value
不会阻塞(即发送者可以继续执行)。 - 当缓冲区已满时,
ch <- value
会阻塞,直到缓冲区中有空间。
接收操作的行为:
- 当缓冲区非空时,
<-ch
可以立即接收到一个值,不会阻塞。 - 当缓冲区为空时,
<-ch
会阻塞,直到缓冲区中有值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
//这里信道最多为2,所以不能再加值,否则就会死锁
//ch <- 3
fmt.Println(<-ch)
ch <- 3
//最后按照加入顺序输出
fmt.Println(<-ch)
fmt.Println(<-ch)
}
range和close
发送者可通过 close
关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完
1
v, ok := <-ch
此时 ok
会被设置为 false
循环 for i := range c
会不断从信道接收值,直到它被关闭
⚠️:只应由发送者关闭信道,向一个已经关闭的信道发送数据会引发程序panic
⚠️:信道与文件不同,一般不用主动去关,只有在必须告诉接收者不再有需要发送的值时才有必要关闭,比如终止一个range循环
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"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
//用于返回一个 信道(channel) 的 缓冲区大小,即信道可以容纳的最大元素数量
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
select语句
select语句使一个goroutine可以等待多个通信操作
select会阻塞到某个分支可以继续执行为止,这时就会执行该分支,当多个分支都准备好时,会随机选一个执行
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
package main
import "fmt"
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
}
}
}
func main() {
/*
0
1
1
2
3
5
8
13
21
34
quit
*/
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
当select中其他分支都没有准备好时,default分支就会执行,所以为了防止不发生阻塞,可使用default分支
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"
"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)
}
}
}
练习:等价二叉树
检查两个二叉树是否保存了相同序列的函数都相当复杂。 我们将使用 Go 的并发和信道来编写一个简单的解法。
本例使用了 tree
包,它定义了类型:
1
2
3
4
5
type Tree struct {
Left *Tree
Value int
Right *Tree
}
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
56
57
58
59
60
61
62
63
package main
import (
"fmt"
"golang.org/x/tour/tree"
)
// Walk 遍历树 t,并树中所有的值发送到信道 ch。
func Walk(t *tree.Tree, ch chan int) {
if t == nil {
return
}
// 递归遍历左子树
Walk(t.Left, ch)
// 将当前节点的值发送到信道
ch <- t.Value
// 递归遍历右子树
Walk(t.Right, ch)
}
// Same 判断 t1 和 t2 是否包含相同的值。
func Same(t1, t2 *tree.Tree) bool {
// 创建两个信道用于接收遍历结果
ch1 := make(chan int)
ch2 := make(chan int)
// 使用 goroutine 并发遍历两棵树
go Walk(t1, ch1)
go Walk(t2, ch2)
// 比较两棵树中的所有值
for v1 := range ch1 {
v2, ok := <-ch2
if !ok || v1 != v2 {
return false
}
}
// 检查第二棵树是否还有剩余值
_, ok := <-ch2
return !ok
}
func main() {
// 创建树并遍历
ch := make(chan int)
//函数 tree.New(k) 用于构造一个随机结构的已排序二叉查找树,它保存了值 k, 2k, 3k, ..., 10k。
//创建一个新的信道 ch 并且对其进行步进:
go Walk(tree.New(1), ch)
// 打印树中所有的值
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
// 测试 Same 函数
fmt.Println(Same(tree.New(1), tree.New(1))) // true
fmt.Println(Same(tree.New(1), tree.New(2))) // false
}
sync.Mutex
信道在多线程之间非常适合通信,但如果只想保证每次只有一个Go线程能够访问一个共享变量,就可以使用sync.Mutex互斥锁类型及其两个方法lock和unlock
⚠️:可以用 defer
语句来保证互斥锁一定会被解锁
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
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"))
}
练习:web爬虫
使用 Go 的并发特性来并行化一个 Web 爬虫,修改 Crawl
函数来并行地抓取 URL,并且保证不重复
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package main
import (
"fmt"
"sync"
)
// Fetcher 接口定义了抓取 URL 并返回内容和链接的功能。
type Fetcher interface {
// Fetch 返回 URL 所指向页面的 body 内容,并将该页面上找到的所有 URL 放到一个切片中。
Fetch(url string) (body string, urls []string, err error)
}
// Crawl 用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度,并行抓取 URL 并防止重复抓取。
func Crawl(url string, depth int, fetcher Fetcher) {
// 用来存储已经抓取过的 URL
visited := make(map[string]bool)
// 用 sync.Mutex 保护 visited map,防止并发冲突
var mu sync.Mutex
// 用 WaitGroup 等待所有的 goroutine 执行完毕
var wg sync.WaitGroup
// 用 channel 来传递待抓取的 URL
ch := make(chan string)
// 启动一个 goroutine 来抓取 URL
go func() {
// 启动一个新的抓取任务
wg.Add(1)
defer wg.Done()
// 递归抓取页面
var crawl func(string, int)
crawl = func(url string, depth int) {
mu.Lock()
if visited[url] {
mu.Unlock()
return // 如果已经访问过该 URL,则直接返回
}
visited[url] = true // 标记该 URL 已经访问
mu.Unlock()
// 如果深度为 0,则停止爬取
if depth <= 0 {
return
}
// 执行 fetch 操作
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
// 输出当前抓取的 URL 和 body 内容
fmt.Printf("found: %s %q\n", url, body)
// 将下一级的 URL 添加到 channel 中
for _, u := range urls {
ch <- u
}
}
// 对传入的 URL 进行抓取
crawl(url, depth)
}()
// 从 channel 中读取 URL 并递归抓取
go func() {
// 并发处理抓取的任务
for u := range ch {
wg.Add(1)
go func(u string) {
defer wg.Done()
Crawl(u, depth-1, fetcher)
}(u)
}
}()
// 等待所有的 goroutine 完成任务
wg.Wait()
}
func main() {
// 从根 URL 开始爬取,深度为 4
Crawl("https://golang.org/", 4, fetcher)
}
// fakeFetcher 模拟一个 Fetcher 实现
type fakeFetcher map[string]*fakeResult
type fakeResult struct {
body string
urls []string
}
func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}
// 模拟一个 fetcher 数据源
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}