Go语言基础(一)
Go语言基础(一)
Go语言介绍
- Go是一门面向过程的语言
- Go为并发而生
- Go简单易学
- Go内置高并发和健壮的标准库
Go语言环境搭建
下载GoLang并且配置环境变量
- 下载GoLang
下载地址
配置环境变量
Linux系统
在用户目录下的
.profile
或者.bashrc
文件下配置即可配置
GOROOT
1
2export GOROOT=/home/user/go
export PATH=$PATH:$GOROOT/bin配置
GOPATH
1
2export GOPATH=/home/user/GoProject
export PATH=$PATH:$GOPATH/binWindows系统
配置
GOROOT
在系统环境变量
PATH
下配置配置
GOPATH
Go配置
通过
go env -w
命令来配置,-w
这个参数的意思就是改变默认环境变量设置,其配置的值会默认保存在一个文件中,当然也可以通过go env -w GOENV=path
来更改其存储位置,可以通过go env
查看其所有的配置
这里的配置优先级低于环境变量的配置
Windows保存位置
1
C:\Users\user\AppData\Roaming\go\env
Linux保存位置
1
/home/user/.config/go/env
Go模块化配置
1
go env -w GO111MODULE=on
七牛云代理
1
go env -w GOPROXY=https://goproxy.cn,direct
Go的二进制文件
go install
命令生成的二进制文件会生成在GOBIN
下1
go env -w GOBIN=D:\vscode\GoProject\bin
配置基本到此就结束了!
下载vscode并配置Go语言插件
- 下载vscode
下载地址
- 下载Go插件
下载Code Runner
运行
Hello World
在任意位置新建
GoLearn
文件夹,然后终端进入该文件夹运行go mod init
, 然后用vscode
打开,然后新建main.go
文件,vscode
会激活Go
插件,然后点击右下角install All
会自动安装Go插件到GoBIN
目录下,然后运行右上角的三角形运行代码1
2
3
4
5package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}Debug
打断点,然后f5即可
Go语言语法
浏览器上写代码
学习阶段主要是这种方法
运行go install golang.org/x/website/tour@latest
, 然后终端运行tour
vscode上写代码
写项目的时候主要采用这种办法,步骤如下:
- 新建一个文件夹,然后运行
go mod init github.com/用户名/项目名称
- 然后新建一个目录,目录下边新建一个go文件, 然后写相应的代码
- 然后项目目录下新建一个
main.go
文件调用该方法
实例:
- 新建
learn01
文件夹 进入该文件,运行
go mod init edy.tyut/learn01
,会生成一个go.mod
文件,包含以下代码1
2module edu.tyut/learn01 // 模块名称
go 1.19 // go版本新建一个
user
文件夹,进入该文件夹,新建一个user01.go
文件写入以下代码
1
2
3
4package user
func Hello() string {
return "Hello 世界!"
}package user
, 这里的包名与user
文件夹无关,但是建议一致- 然后进入
learn01
目录下新建main.go
文件,键入以下代码1
2
3
4
5
6
7
8package main
import (
"fmt"
"edu.tyut/learn01/user"
)
func main() {
fmt.Println(user.Hello())
}一个项目里边只能有一个
main
函数,edu.tyut/learn01/user
=go.mod
的module
+package user
的user
- 运行项目将打印
Hello 世界!
1. 标识符、关键字、命名规则
1.1 标识符
标识符的组成
- 只能由数字、字母、下划线组成
- 只能用字母和下划线开头
区分大小写
举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main
import (
"fmt"
)
func main() {
var name string
var age int
var _str string
fmt.Printf("age: %v\n", age)
fmt.Printf("name: %v\n", name)
fmt.Printf("_str: %v\n", _str)
}Go语言不使用变量的话会报错
1.2 关键字、预定义标识符
25个关键字
break | default | func | interface | select |
---|---|---|---|---|
case | defer | go | map | struct |
chan | else | goto | package | switch |
const | fallthrough | if | range | type |
continue | for | import | return | var |
36个预定义标识符
append | bool | byte | cap | close | complex | complex64 | complex128 | uint16 |
---|---|---|---|---|---|---|---|---|
copy | false | float32 | float64 | imag | int | int8 | int16 | unit32 |
int32 | int64 | itoa | len | make | new | nil | panic | uint64 |
println | real | recover | string | true | unit | unit8 | uintptr |
1.3 命名规范
- 包名称 简短、有意义、小写、不要使用下划线
- 文件命名 简短、有意义、小写、使用下划线分割每个单词
- 结构体命名 使用驼峰命名法
- 接口命名 与结构体类似、单个函数的接口以
er
结尾 - 变量命名 首字母小写的驼峰命名法
- 常量命名 全部大写、多个单词以下划线分割
- 函数命名 建议驼峰命名法,大写驼峰命名法代表公开,小写驼峰命名法代表私有
- 单元测试命名 测试文件命名规范为
example_test.go
, 测试函数为TestExample
2. 包、函数、变量
2.1.1 包
每个Go程序都是由包构成的。
程序从main
包开始运行。
下面这个程序通过导入路径fmt
和math/rand
来使用这两个个包。
按照约定,包名和导入路径的最后一个元素一致。例如,math/rand
包中的源码均以package rand
语句开始。
注意,此程序的运行环境是固定的,因此rand.Intn
总是会返回相同的数字。(要得到不同的数字,需要为生成器提供不同的种子数,参见rand.Seed
。 练习场中的时间为常量,因此你需要用其它的值作为种子数。)
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
39package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().Unix()) // 获取时间戳,设置随机种子数
fmt.Printf("time.Now().Unix(): %v\n", time.Now().Unix()) // 在沙箱中的时间为常量,2009-11-10 23:00:00 UTC
fmt.Printf("rand.Intn(10): %v\n", rand.Intn(10000))
// rand.Seed(5) // 设置种子值,产生固定的随机数
fmt.Println("My favorite number is", rand.Intn(10000))
// 随机选取一句话
answers := []string{
"It is certain",
"It is decidedly so",
"Without a doubt",
"Yes definitely",
"You may rely on it",
"As I see it yes",
"Most likely",
"Outlook good",
"Yes",
"Signs point to yes",
"Reply hazy try again",
"Ask again later",
"Better not tell you now",
"Cannot predict now",
"Concentrate and ask again",
"Don't count on it",
"My reply is no",
"My sources say no",
"Outlook not so good",
"Very doubtful",
}
fmt.Println("Magic 8-Ball says:", answers[rand.Intn(len(answers))])
}
2.1.2 分组导入
1 | package main |
2.1.3 导出名
在Go
中,如果一个名字以大写字母开头,那么他就是导出的,例如某个包的变量Pizza
,就是个已导出名,Pi
也同样,它到出自math
包。
在某个包内pizza1
和pi1
, 在包内并未以大写字母开头所以它们是未导出的。
在导入一个包时,你只能引用其中已导出的名字,即以大写字母开头的变量或者函数。任何未导出的名字,在该包外均无法访问。
执行以下代码,观察错误输出。
然后将math.pi
改为math.Pi
再试一次
1
2
3
4
5
6
7
8package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.pi) // 错误: ./prog.go:9:19: undefined: math.pi
}
2.2.1 函数定义
1 | package main |
2.2.2 函数的多值返回
1 | package main |
2.2.3 命名返回值
Go的返回值可以被命名,它们会被视作定义在函数顶部的变量。
返回值的名称应该具有一定的意义,它可以作为文档使用。
没有参数的return
语句,返回已命名的返回值,也就是直接返回。
直接返回语句应当仅在下面这样的短函数中。在场函数中,它们会影响代码的可读性。
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
36package main
import "fmt"
func split(sum int) (x, y, z int) {
x = sum * 4 / 9
fmt.Println(x)
y = sum - x
fmt.Println(y)
z = 1000
fmt.Println(z)
return
}
func split1(sum int) (x, y int) {
x = sum * 4 / 9
fmt.Println(x)
y = sum - x
fmt.Println(y)
var z int = 1000
fmt.Println(z)
return
}
func main() {
fmt.Println(split(17))
fmt.Println("============")
fmt.Println(split1(17))
}
/* 运行结果
7
10
1000
7 10 1000
============
7
10
1000
7 10
*/
2.3.1 变量定义
var
(variable的缩写)语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后。
就像下面这个例子中看到的一样,var
语句可以出现在包或者函数级别。
1
2
3
4
5
6
7
8package main
import "fmt"
// 这类似于函数参数列表
var c, python, java bool
func main() {
var i int
fmt.Println(i, c, python, java)
}
2.3.2 变量初始化
变量声明可以包含初始值,每个变量对应一个。
如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型。
1
2
3
4
5
6
7package main
import "fmt"
var i, j int = 1, 2
func main() {
var c, python, java = true, false, "no!"
fmt.Println(i, j, c, python, java)
}
2.3.3 短变量声明
在函数中,简洁赋值语句 :=
可在类型明确的地方代替 var
声明。
函数外的每个语句都必须以关键字开始(var
, func
等等),因此 :=
结构不能在函数外使用。
1
2
3
4
5
6
7
8package 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)
}
2.3.4 基本类型
布尔类型: 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
类型,除非你有特殊的理由使用固定大小或无符号的整数类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package main
import (
"fmt"
"math/cmplx"
)
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
func main() {
fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe)
fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt)
fmt.Printf("Type: %T Value: %v\n", z, z)
}
2.3.5 零值
没有明确初始值的变量声明会被赋予它们的 零值。
零值是:
数值类型为 0,
布尔类型为 false
,
字符串为 “”(空字符串)。
1
2
3
4
5
6
7
8
9package main
import "fmt"
func main() {
var i int
var f float64
var b bool
var s string
fmt.Printf("%v %v %v %q\n", i, f, b, s)
}
2.3.6 类型转换
表达式 v
转换为类型 T
。
一些关于数值的转换:
1
2
3var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
或者,更加简单的形式:
1
2
3i := 42
f := float64(i)
u := uint(f)
与 C
不同的是,Go
在不同类型的项之间赋值时需要显式转换。试着移除例子中 float64
或 uint
的转换看看会发生什么。
换句话说,必须强制转换
2.3.7 类型推导
在声明一个变量而不指定其类型时(即使用不带类型的 :=
语法或 var =
表达式语法),变量的类型由右值推导得出。
当右值声明了类型时,新变量的类型与其相同:
1
2var i int
j := i // j 也是一个 int
不过当右边包含未指明类型的数值常量时,新变量的类型就可能是 int
, float64
或 complex128
了,这取决于常量的精度:
1
2
3i := 42 // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128
尝试修改示例代码中 v
的初始值,并观察它是如何影响类型的。
1
2
3
4
5
6
7
8
9
10package main
import (
"fmt"
)
func main() {
v := 1.0 // 修改这里!只能在局部变量这样写
var x = 1.0 // 修改这里!全局变量亦可这样写
fmt.Printf("v is of type %T\n", v)
fmt.Printf("x is of type %T\n", x)
}
2.3.8 常量
常量的声明与变量类似,只不过是使用 const
关键字。
常量可以是字符、字符串、布尔值或数值。
常量不能用 :=
语法声明。
1
2
3
4
5
6
7
8
9
10package 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)
}
2.3.9 数值常量
数值常量是高精度的 值。
一个未指定类型的常量由上下文来决定其类型。
再尝试一下输出 needInt(Big)
吧。
(int
类型最大可以存储一个 64
位的整数,有时会更小。)
(int
可以存放最大64位的整数,根据平台不同有时会更少。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package 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(Big) // Big = 2^100 > 2^64 会报错
fmt.Println(Small) // Small = 2
// fmt.Println(needInt(Big)) // Big = 2^100 > 2^64 会报错
fmt.Println(needInt(Small)) // 2 * 10 + 1 = 21
fmt.Println(needFloat(Small)) // 2 * 0.1 = 0.2
// 2^100 = 1267650600228229401496703205376(31位数) * 0.1 = 1.2676506002282295e+29 高精度转化为float64
fmt.Println(needFloat(Big))
}
3. 流程控制
3.1.1 for
循环
Go
只有一种循环结构:for
循环。
基本的 for
循环由三部分组成,它们用分号隔开:
- 初始化语句:在第一次迭代前执行
- 条0件表达式:在每次迭代前求值
后置语句:在每次迭代的结尾执行
初始化语句通常为一句短变量声明,该变量声明仅在 for 语句的作用域中可见。一旦条件表达式的布尔值为 false,循环迭代就会终止。
注意:和
C
、Java
、JavaScript
之类的语言不同,Go
的for
语句后面的三个构成部分外没有小括号, 大括号{ }
则是必须的。1
2
3
4
5
6
7
8
9package main
import "fmt"
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
}for
循环的初始化语句和后置语句是可选的。1
2
3
4
5
6
7
8
9
10package main
import "fmt"
func main() {
sum := 1
// 去掉两个;就是while循环
for ; sum < 1000; {
sum += sum
}
fmt.Println(sum)
}
3.1.2 while
循环
for
是 Go
中的 while
此时你可以去掉分号,因为 C
的 while
在 Go
中叫做 for
。
1
2
3
4
5
6
7
8
9package main
import "fmt"
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
3.1.2 无限循环
如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑。
1
2
3
4
5
6
7
8
9package main
import(
"fmt"
)
func main() {
for {
fmt.Printf("1");
}
}
3.2.1 if
条件判断
Go 的 if
语句与 for
循环类似,表达式外无需小括号 ( )
,而大括号 { }
则是必须的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main
import (
"fmt"
"math"
)
func sqrt(x float64) string {
if x < 0 {
return sqrt(-x) + "i" // 如果x小于零返回虚数
}
return fmt.Sprint(math.Sqrt(x))
}
func main() {
fmt.Println(sqrt(2), sqrt(-4))
}
3.2.2 if
的简短语句
同 for 一样, if
语句可以在条件表达式前执行一个简单的语句。
该语句声明的变量作用域仅在 if
之内。
(在最后的 return
语句处使用 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
27package main
import (
"fmt"
"math"
)
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}
// 等价于
func pow1(x, n, lim float64) float64 {
v := math.Pow(x, n)
if v < lim {
return v
}
return lim
}
func main() {
fmt.Println(
pow(3, 2, 10),
pow(3, 3, 20),
pow1(3, 2, 10),
pow1(3, 3, 20),
)
}
3.2.2 if
和else
在 if
的简短语句中声明的变量同样可以在任何对应的 else
块中使用。
(在 main
的 fmt.Println
调用开始前,两次对 pow
的调用均已执行并返回其各自的结果。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main
import (
"fmt"
"math"
)
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
} else {
fmt.Printf("%g >= %g\n", v, lim)
}
// 这里开始就不能使用 v 了
return lim
}
func main() {
fmt.Println(
pow(3, 2, 10),
pow(3, 3, 20),
)
}
3.2.3 练习:循环与函数
前置知识:
《数值分析》(研究生课程,我肯定没学过,数学蒟蒻O.o)的牛顿法,这里先不深入,直接贴百科的解释,后续有机会,直接把这本书过一遍,写在博客里。
牛顿法(英语:Newton’s method)又称为牛顿-拉弗森方法(英语:Newton-Raphson method),它是一种在实数域和复数域上近似求解方程的方法。方法使用函数
查看牛顿法图片
方法说明
首先,选择一个接近函数
零点的$x{0} f(x_0) f’(x_0) f’ f (x{0},f(x_{0})) f’(x_0) x x$坐标,也就是求如下方程的解: 我们将新求得的点的
坐标命名为$x1 x_1 x{0} f(x)=0 x_1$开始下一轮迭代。迭代公式可化简为如下所示: 动态图解
已有证明牛顿迭代法的二次收敛必须满足以下条件:
; 对于所有 ,其中 为区间 ,且$x{0} I r\geqslant \left|a-x{0}\right| {\displaystyle x\in I} f’’(x) x_{0} α$。 下面这个应该是证明
案例
第一个案例:
求方程
的根。令 ,两边求导,得 。由于 ,则 ,即 ,可知方程的根位于 和 之间。我们从 开始。 查看案例一图片
第二个案例:
牛顿法亦可发挥与泰勒展开式,对于函式展开的功能。
求的 次方根。 设
, 而
的 次方根,亦是 的解, 以牛顿法来迭代:
(或 )
应用
求解最值问题
牛顿法也被用于求函数的极值。由于函数取极值的点处的导数值为零,故可用牛顿法求导函数的零点,其迭代式为
求拐点的公式以此类推
引例:
用牛顿法求解平方根:
如果要求
例子:求
因此
代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package main
import (
"fmt"
)
func main() {
// 求S = 125348的平方根
// 1. 选取1 < x0 < S
// x0 = 3^6 = 729.00
// 2. 迭代5次
var S float64 = 125348
var x float64 = 729
for i := 0; i < 5; i ++ {
x = 1 / 2.0 * (x + S / x)
}
fmt.Printf("x: %v\n", x)
}
结论:
不难看出
等价于:
在数学上是等价的,在计算机上int
所表示的范围,变成+Inf
将
我们来推导出这个公式:
设
, , 证明二次收敛:
; 对于所有 ,其中 为区间 ,设近似根为$x0 x{0} I {\displaystyle x\in I} f’’(x) x_{0} α α$是实际的根。 根据定义将
,代入 因为二次收敛,所以等式两边除以
,然后移项得 - 则可以得到其迭代公式
- 代入求解得推导完毕!
有了以上基础,下面就非常简单了
为了练习函数与循环,我们来实现一个平方根函数:用牛顿法实现平方根函数。
计算机通常使用循环来计算
重复调整的过程,猜测的结果会越来越精确,得到的答案也会尽可能接近实际的平方根。
在提供的 func Sqrt
中实现它。无论输入是什么,对 z
的一个恰当的猜测为 1
。 要开始,请重复计算 10
次并随之打印每次的 z
值。
观察对于不同的值
提示:用类型转换或浮点数语法来声明并初始化一个浮点数值:
1 | z := 1.0 |
然后,修改循环条件,使得当值停止改变(或改变非常小)的时候退出循环。观察迭代次数大于还是小于 10
。 尝试改变 z
的初始猜测,
如 x
或 x/2
。你的函数结果与标准库中的 math.Sqrt
接近吗?
(注: 如果你对该算法的细节感兴趣,上面的 z² − x
是 z²
到它所要到达的值(即 x
)的距离, 除以的 2z
为 z²
的导数,
我们通过 z²
的变化速度来改变 z
的调整量。 这种通用方法叫做牛顿法。 它对很多函数,特别是平方根而言非常有效。)
平方根函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package main
import (
"fmt"
"math"
)
func Sqrt(x float64) float64 {
// z最好在 1 < z < 2 内取值
z := 1.5 // 迭代四次就够了
for i := 0; i < 4; i ++ {
z -= (z * z - x) / (2 * z)
fmt.Println(z)
}
return z
}
func main() {
fmt.Println(Sqrt(2))
fmt.Println("================")
fmt.Println(math.Sqrt(2))
}
同理再实现一个立方根函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main
import (
"fmt"
)
func subtriplicate(x float64) float64 {
z := 1.0
for i := 0; i < 10; i ++ {
z = z - z / 3.0 + x / (3.0 * z * z);
}
return z
}
func main() {
fmt.Printf("subtriplicate(7): %v\n", subtriplicate(7))
}
总结:牛顿法收敛速度是二次方级别的,比二分法快多了
3.2.4 switch
switch
是编写一连串 if - else
语句的简便方法。它运行第一个值等于条件表达式的 case
语句。
Go
的 switch
语句类似于 C
、C++
、Java
、JavaScript
和 PHP
中的,不过 Go
只运行选定的 case
,而非之后所有的 case
。 实际上,Go
自动提供了在这些语言中每个 case
后面所需的 break
语句。 除非以 fallthrough
语句结束,否则分支会自动终止。 Go
的另一点重要的不同在于 switch
的 case
无需为常量,且取值不必为整数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package 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.")
case "windows":
fmt.Println("windows")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}
3.2.5 switch
的求值顺序
switch
的 case
语句从上到下顺次执行,直到匹配成功时停止。
例如:
1
2
3
4switch i {
case 0:
case f():
}
在 i==0
时 f
不会被调用。
注意: Go 练习场中的时间总是从 2009-11-10 23:00:00 UTC 开始,该值的意义留给读者去发现。
老是强调这个时间,虽然我能猜到这是啥意义,但是我还是查了,^_^。Go于2009年11月正式宣布推出
1 | package main |
case
后面可以放函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package main
import (
"fmt"
)
func f() int {
return 1
}
func main() {
i := 1
switch i {
case 0:
fmt.Println(i)
case f(): // 比较的时候会调用函数f(),返回值和i相等case里面的语句才会执行
fmt.Println("f()")
}
}
3.2.6 没有条件的 switch
没有条件的 switch
同 switch true
一样。
这种形式能将一长串 if-then-else
写得更加清晰。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
fmt.Println(t.Hour())
// 相当于if-then-else
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
3.2.7 defer
,中文意思是 延期,推迟
defer
语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
1
2
3
4
5
6
7
8
9package main
import "fmt"
func main() {
// 不立即执行,而是相当于先压栈
defer fmt.Println("world")
fmt.Println("hello")
}
3.2.7 defer
栈
推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。
1
2
3
4
5
6
7
8
9
10
11package main
import "fmt"
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
4. 指针、结构体、切片、映射
4.1.1 指针
Go
拥有指针。指针保存了值的内存地址。
类型 *T
是指向 T
类型值的指针。其零值为 nil
。
1
var p *int
&
操作符会生成一个指向其操作数的指针。
1
2i := 42
p = &i
*
操作符表示指针指向的底层值。
1
2fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i
这也就是通常所说的“间接引用”或“重定向”。
与 C
不同,Go
没有指针运算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14package 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 的值
}
4.2.1 结构体
一个结构体(struct
)就是一组字段(field)。
1
2
3
4
5
6
7
8
9package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() {
fmt.Println(Vertex{1, 2})
}
4.2.2 结构体字段
结构体字段使用点号来访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X)
}
4.2.3 结构体指针
结构体字段可以通过结构体指针来访问。
如果我们有一个指向结构体的指针 p
,那么可以通过 (*p).X
来访问其字段 X
。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X
就可以。
计算机语言的发明者非常伟大,这里体现了
Go
语言的简洁
1 | package main |
4.2.4 结构体文法(翻译好怪,但是有特性)
结构体文法通过直接列出字段的值来新分配一个结构体。
使用 Name:
语法可以仅列出部分字段。(字段名的顺序无关。)
特殊的前缀 &
返回一个指向结构体的指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package 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)
}
4.3.1 数组
类型 [n]T
表示拥有 n
个 T
类型的值的数组。
表达式
1
var a [10]int
会将变量 a
声明为拥有 10
个整数的数组。
数组的长度是其类型的一部分,因此数组不能改变大小。这看起来是个限制,不过没关系,Go
提供了更加便利的方式来使用数组。
1
2
3
4
5
6
7
8
9
10
11
12package 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)
}
4.3.2 切片
每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。
类型 []T
表示一个元素类型为 T
的切片。
切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:
a[low : high]
它会选择一个半开区间(左闭右开),包括第一个元素,但排除最后一个元素。
以下表达式创建了一个切片,它包含 a
中下标从 1
到 3
的元素:
a[1:4]
1
2
3
4
5
6
7
8
9package main
import "fmt"
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)
}
4.3.3 切片就像数组的引用
切片并不存储任何数据,它只是描述了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。
与它共享底层数组的切片都会观测到这些修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package main
import "fmt"
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
4.3.4 切片文法
切片文法类似于没有长度的数组文法。
这是一个数组文法:
[3]bool{true, true, false}
下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片:
[]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
22package main
import "fmt"
func main() {
q := []int{2, 3, 5, 7, 11, 13}
fmt.Println(q)
r := []bool{true, false, true, true, false, true}
fmt.Println(r)
s := []struct {
i int
b bool
}{
{2, true},
{3, false},
{5, true},
{7, true},
{11, false},
{13, true},
}
fmt.Println(s)
}
切面文法将创建数组和创建切片的过程合二为一了,尤其注意,如果
[]
中有数字,就是普通的数组了
4.3.5 切片的默认行为
在进行切片时,你可以利用它的默认行为来忽略上下界。
切片下界的默认值为 0,上界则是该切片的长度。
对于数组
var a [10]int
来说,以下切片是等价的:
1
2
3
4a[0:10]
a[:10]
a[0:]
a[:]
1 | package main |
4.3.6 切片的长度与容量
切片拥有 长度 和 容量。
切片的长度就是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
切片 s
的长度和容量可通过表达式 len(s)
和 cap(s)
来获取。当然普通数组a
的长度和容量可通过表达式 len(a)
和 cap(a)
来获取
你可以通过重新切片来扩展一个切片,给它提供足够的容量。试着修改示例程序中的切片操作,向外扩展它的容量,看看会发生什么。
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
61package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// 截取切片使其长度为 0
s = s[:0] // 第一个元素下标是0
printSlice(s)
// 拓展其长度
s = s[:4]
printSlice(s)
// 舍弃前两个值
s = s[2:] // 第一个元素下标是2
printSlice(s)
s = s[2:] // 第一个元素下标是2
printSlice(s)
// 扩容1
fmt.Printf("%s\n", "=========扩容1======")
s = s[:] // 第一个元素下标是0
s = append(s, 1)
printSlice(s)
// 扩容2
fmt.Printf("%s\n", "=========扩容2======")
s = s[:] // 第一个元素下标是0
s = append(s, 2)
printSlice(s)
// 扩容3
fmt.Printf("%s\n", "=========扩容3======")
s = s[:] // 第一个元素下标是0
s = append(s, 3)
printSlice(s)
// 扩容4
fmt.Printf("%s\n", "=========扩容4======")
s = s[:] // 第一个元素下标是0
s = append(s, 4)
printSlice(s)
// 扩容5
fmt.Printf("%s\n", "=========扩容5======")
s = s[:] // 第一个元素下标是0
s = append(s, 5)
printSlice(s)
// 重新切片
s = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
看下面案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package main
import "fmt"
func main() {
// 定义一个切片文法
s := []int{1, 2, 3, 4, 5, 6}
printSlice(s)
// 定义一个切片
s1 := s[1:3]
printSlice(s1)
s2 := s[4:]
printSlice(s2)
s3 := s[6:]
printSlice(s3)
s3 = append(s3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
printSlice(s3)
printSlice(s)
}
func printSlice(s []int) { // 参数是一个切片
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
可见,当切片容量小于1024时,数组容量是成倍扩充,当然需要扩充的容量大于翻倍后的容量,那么直接让容量等于需要扩充的容量,当切片容量大于等于1024时,同理,只不过数组容量扩大
4.3.7 nil
切片
切片的零值是 nil
。
nil
切片的长度和容量为 0 且没有底层数组。
1
2
3
4
5
6
7
8
9
10
11package main
import "fmt"
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
4.3.7 用 make
创建切片
切片可以用内建函数 make
来创建,这也是你创建动态数组的方式。
make
函数会分配一个元素为零值的数组并返回一个引用了它的切片:
1
a := make([]int, 5) // len(a)=5
要指定它的容量,需向 make
传入第三个参数:
1
2
3
4b := make([]int, 0, 5) // len(b)=0, cap(b)=5
b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package main
import "fmt"
func main() {
a := make([]int, 5)
printSlice("a", a)
b := make([]int, 0, 5)
printSlice("b", b)
c := b[:2]
printSlice("c", c)
d := c[2:5]
printSlice("d", d)
}
func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n",
s, len(x), cap(x), x)
}
4.3.8 切片的切片
切片可包含任何类型,甚至包括其它的切片。
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
31package main
import (
"fmt"
"strings"
)
func main() {
// 创建一个井字板(经典游戏)
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"
for i := 0; i < len(board); i++ {
fmt.Printf("%s\n", strings.Join(board[i], " "))
}
}
/*
X _ X
O _ X
_ _ O
*/
4.3.9 向切片追加元素
为切片追加新的元素是种常用的操作,为此 Go
提供了内建的 append
函数。内建函数的文档对此函数有详细的介绍。
func append(s []T, vs ...T) []T
append
的第一个参数 s
是一个元素类型为 T
的切片,其余类型为 T
的值将会追加到该切片的末尾。
append 的结果是一个包含原切片所有元素加上新添加元素的切片。
当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。
(要了解关于切片的更多内容,请阅读文章 Go 切片:用法和本质。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package main
import "fmt"
func main() {
var s []int
printSlice(s)
// 添加一个空切片
s = append(s, 0)
printSlice(s)
// 这个切片会按需增长
s = append(s, 1)
printSlice(s)
// 可以一次性添加多个元素
s = append(s, 2, 3, 4)
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
4.3.10 Go
切片:用法和本质(引用自Go
官网)
引言
Go的切片类型为处理同类型数据序列提供一个方便而高效的方式。 切片有些类似于其他语言中的数组,但是有一些不同寻常的特性。 本文将深入切片的本质,并讲解它的用法。
数组
Go的切片是在数组之上的抽象数据类型,因此在了解切片之前必须要先理解数组。
数组类型定义了长度和元素类型。例如, [4]int
类型表示一个四个整数的数组。 数组的长度是固定的,长度是数组类型的一部分( [4]int
和 [5]int
是完全不同的类型)。 数组可以以常规的索引方式访问,表达式 s[n]
访问数组的第 n
个元素。
1
2
3
4var a [4]int
a[0] = 1
i := a[0]
// i == 1
数组不需要显式的初始化;数组的零值是可以直接使用的,数组元素会自动初始化为其对应类型的零值:
1
// a[2] == 0, int 类型的零值
类型 [4]int
对应内存中四个连续的整数:
Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针(不像 C 语言的数组)。 当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。 (为了避免复制数组,你可以传递一个指向数组的指针,但是数组指针并不是数组。) 可以将数组看作一个特殊的struct,结构的字段名对应数组的索引,同时成员的数目固定。
数组的字面值像这样:
1 | b := [2]string{"Penn", "Teller"} |
当然,也可以让编译器统计数组字面值中元素的数目:
1
b := [...]string{"Penn", "Teller"}
这两种写法, b
都是对应 [2]string
类型。
切片
数组虽然有适用它们的地方,但是数组不够灵活,因此在Go
代码中数组使用的并不多。 但是,切片则使用得相当广泛。切片基于数组构建,但是提供更强的功能和便利。
切片类型的写法是 []T
, T
是切片元素的类型。和数组不同的是,切片类型并没有给定固定的长度。
切片的字面值和数组字面值很像,不过切片没有指定元素个数:
1
letters := []string{"a", "b", "c", "d"}
切片可以使用内置函数 make
创建,函数签名为:
1 | func make([]T, len, cap) []T |
其中T
代表被创建的切片元素的类型。函数 make
接受一个类型、一个长度和一个可选的容量参数。 调用 make
时,内部会分配一个数组,然后返回数组对应的切片。
1
2
3var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}
当容量参数被忽略时,它默认为指定的长度。下面是简洁的写法:
1
s := make([]byte, 5)
可以使用内置函数 len
和 cap
获取切片的长度和容量信息。
1
2len(s) == 5
cap(s) == 5
接下来的两个小节将讨论长度和容量之间的关系。
切片的零值为 nil
。对于切片的零值, len
和 cap
都将返回0
。
切片也可以基于现有的切片或数组生成。切分的范围由两个由冒号分割的索引对应的半开区间指定。 例如,表达式 b[1:4]
创建的切片引用数组 b
的第1
到3
个元素空间(对应切片的索引为0
到2
)。
1
2b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b
切片的开始和结束的索引都是可选的;它们分别默认为零和数组的长度。
1
2
3// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b
下面语法也是基于数组创建一个切片:
1
2x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x
切片的内幕
一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。
前面使用
make([]byte, 5)
创建的切片变量 s
的结构如下:长度是切片引用的元素数目。容量是底层数组的元素数目(从切片指针开始)。 关于长度和容量和区域将在下一个例子说明。
我们继续对 s
进行切片,观察切片的数据结构和它引用的底层数组:
1
s = s[2:4]
切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。
1
2
3
4
5
6d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}
前面创建的切片 s
长度小于它的容量。我们可以增长切片的长度为它的容量:
1
s = s[:cap(s)]
切片增长不能超出其容量。增长超出切片容量将会导致运行时异常,就像切片或数组的索引超 出范围引起异常一样。同样,不能使用小于零的索引去访问切片之前的元素。
切片的生长(copy
and append
函数)
要增加切片的容量必须创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。 整个技术是一些支持动态数组语言的常见实现。下面的例子将切片 s
容量翻倍,先创建一个2
倍 容量的新切片 t
,复制 s
的元素到 t
,然后将 t
赋值给 s
:
1
2
3
4
5t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0
for i := range s {
t[i] = s[i]
}
s = t
循环中复制的操作可以由 copy
内置函数替代。copy
函数将源切片的元素复制到目的切片。 它返回复制元素的数目。
1
func copy(dst, src []T) int
copy
函数支持不同长度的切片之间的复制(它只复制较短切片的长度个元素)。 此外, copy
函数可以正确处理源和目的切片有重叠的情况。
使用 copy
函数,我们可以简化上面的代码片段:
1
2
3t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
一个常见的操作是将数据追加到切片的尾部。下面的函数将元素追加到切片尾部, 必要的话会增加切片的容量,最后返回更新的切片:
1
2
3
4
5
6
7
8
9
10
11
12
13
14// ...类似于Java可变长数组
func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) { // if necessary, reallocate
// allocate double what's needed, for future growth.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}
下面是 AppendByte
的一种用法:
1
2
3p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}
类似 AppendByte
的函数比较实用,因为它提供了切片容量增长的完全控制。 根据程序的特点,可能希望分配较小的或较大的块,或则是超过某个大小再分配。
但大多数程序不需要完全的控制,因此Go提供了一个内置函数 append , 用于大多数场合;它的函数签名:
1
func append(s []T, x ...T) []T
append
函数将 x
追加到切片 s
的末尾,并且在必要的时候增加容量。
1
2
3
4a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}
如果是要将一个切片追加到另一个切片尾部,需要使用 … 语法将第2个参数展开为参数列表。
1
2
3
4a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to (等同于) "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}
由于切片的零值 nil
用起来就像一个长度为零的切片,我们可以声明一个切片变量然后在循环 中向它追加数据:
1
2
3
4
5
6
7
8
9
10
11// Filter returns a new slice holding only
// the elements of s that satisfy fn()
func Filter(s []int, fn func(int) bool) []int {
var p []int // == nil
for _, v := range s {
if fn(v) {
p = append(p, v)
}
}
return p
}
可能的“陷阱”
正如前面所说,切片操作并不会复制底层的数组。整个数组将被保存在内存中,直到它不再被引用。 有时候可能会因为一个小的内存引用导致保存所有的数据。
例如, FindDigits
函数加载整个文件到内存,然后搜索第一个连续的数字,最后结果以切片方式返回。
1
2
3
4
5
6var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
这段代码的行为和描述类似,返回的 []byte
指向保存整个文件的数组。因为切片引用了原始的数组, 导致 GC
不能释放数组的空间;只用到少数几个字节却导致整个文件的内容都一直保存在内存里。
要修复整个问题,可以将感兴趣的数据复制到一个新的切片中:
1
2
3
4
5
6
7func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}
可以使用 append
实现一个更简洁的版本。这留给读者作为练习。
延伸阅读(大部分都是英文放弃了…)
实效 Go
编程 包含了对 切片 和 数组 更深入的探讨; Go
编程语言规范 对 切片类型 和 数组类型 以及与它们 相关的 辅助 函数 进行了定义。
4.3.11 Range
for
循环的 range
形式可遍历切片或映射。
当使用 for
循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
1
2
3
4
5
6
7
8package 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)
}
}
4.3.12 Range
(续)
可以将下标或值赋予 _
来忽略它。1
2for i, _ := range pow
for _, value := range pow
若你只需要索引,忽略第二个变量即可。1
for i := range pow
1
2
3
4
5
6
7
8
9
10
11
12
13package main
import "fmt"
func main() {
pow := make([]int, 10)
for i := range pow {
pow[i] = 1 << uint(i) // == 2**i
}
for _, value := range pow {
fmt.Printf("%d\n", value)
}
}
4.3.13 练习:切片
尝试实现一个函数 Pic
:返回一个长度为 dy
的切片,切片元素为长度为 dx
的 8
位无符号整数切片。 这个切片,其实就是一个二维阵列,可以用来表示一张图片的像素。当你运行此程序时,它会将每个整数解释为灰度值(好吧,其实是蓝度值)并显示它所对应的图像。
图像的选择由你来定。几个有趣的函数包括 (x+y)/2
, x*y
, x^y
, x*log(y)
和 x%(y+1)
。
(提示:需要使用循环来分配 [][]uint8
中的每个 []uint8
;请使用 uint8(intValue)
在类型之间转换;你可能会用到 math
包中的函数。)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
48package main
import (
"golang.org/x/tour/pic"
)
func Pic(dx, dy int) [][]uint8 {
s := make([][]uint8, dy)
for y := 0; y < dy; y ++ {
sx := make([]uint8, dx)
for x := 0; x < dx; x ++ {
// sx[x] = uint8((x + y) / 2) // 类似于半圆
// sx[x] = uint8(x * y) // 花里胡哨
// sx[x] = uint8(x^y) // x异或y,无数个嵌套小正方形
// sx[x] = uint8(x - y) // 很好看
// sx[x] = uint8(x / (y + 1)) // 纯蓝全是0
// sx[x] = uint8(x + y) // 与x - y差不多
// sx[x] = uint8(x * x + y * y) // 能跑
// sx[x] = uint8(x * x + y) // 纯蓝全是0
// sx[x] = uint8(x + y * y) // 激光束
// sx[x] = uint8(2x + 2y) // 纯蓝全是0
// sx[x] = uint8(x^22 + y^22) // 看着很晕
// sx[x] = uint8(x * 3 + y * 3)
// sx[x] = uint8(x * x + y * y) // wc,病毒
// sx[x] = uint8(x|x + y|y) // 与 或 非 异或 左移 右移全试一遍
// sx[x] = uint8(x*y + x*y) // 花里胡哨
sx[x] = uint8(x+y + x-y) // + - * / 混合运算
// 我还想玩...(😏
// 不过时间不允许了...
}
s[y] = sx
}
return s
}
func main() {
/*
当在Go Playground上执行时,
Show显示由函数f定义的图片。
f应该返回一个长度为dy的切片,
每一个元素都是dx个8位无符号整数的一个切片
这些整数被
解释为蓝尺度值,
0表示全蓝色,
255表示全白。
*/
pic.Show(Pic)
}
4.4.1 映射
映射将键映射到值。
映射的零值为 nil
。nil
映射既没有键,也不能添加键。make
函数会返回给定类型的映射,并将其初始化备用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
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"])
}
4.4.2 映射的文法
映射的文法与结构体相似,不过必须有键名。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
func main() {
fmt.Println(m)
}
4.4.3 映射的文法(续)
若顶级类型只是一个类型名,你可以在文法的元素中省略它。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
26package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
var m1 = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
func main() {
fmt.Println(m)
fmt.Println(m1)
}
4.4.4 修改映射(类别其他语言的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]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package main
import "fmt"
func main() {
m := make(map[string]int)
m["Answer"] = 42
fmt.Println("The value:", m["Answer"])
m["Answer"] = 48
fmt.Println("The value:", m["Answer"])
delete(m, "Answer")
fmt.Println("The value:", m["Answer"])
v, ok := m["Answer"]
fmt.Println("The value:", v, "Present?", ok)
}
4.4.5 练习:映射
实现 WordCount
。它应当返回一个映射,其中包含字符串 s
中每个“单词”的个数。函数 wc.Test
会对此函数执行一系列测试用例,并输出成功还是失败。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package main
import (
"strings"
"golang.org/x/tour/wc"
)
func WordCount(s string) map[string]int {
// 定义一个切片
x := strings.Fields(s)
m := make(map[string]int, len(x))
for _, v := range x{
m[v] ++
}
return m
}
func main() {
wc.Test(WordCount)
}
你会发现 strings.Fields
很有帮助。1
func Fields(s string) []string
Fields
将字符串s
分隔在unicode
中定义的一个或多个连续空白字符的每个实例周围。Is Space,返回s
的子字符串数组,如果s只包含空白,则返回空列表。1
2
3
4
5
6
7
8
9
10package main
import (
"fmt"
"strings"
)
func main() {
// strings.Fields说白了,按空格拆分,返回一个[]string切片
fmt.Printf("strings.Fields(\"hello World 世界\"): %v\n", strings.Fields("hello World 世界")[0:1])
}
5 方法和接口
5.1.1 函数值
函数也是值。它们可以像其它值一样传递。
函数值可以用作函数的参数或返回值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package 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))
fmt.Println(compute(hypot))
fmt.Println(math.Pow(2, 10))
fmt.Println(compute(math.Pow))
}
5.1.2 函数的闭包
Go
函数可以是一个闭包。(另一种翻译: 闭包是一个引用外部变量的函数值。)闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。
例如,函数 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
27package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
// scala的闭包亦是如此
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
)
}
fmt.Println("============")
for i := 0; i < 10; i++ {
fmt.Println(
neg(-2*i),
)
}
}
5.1.3 练习:斐波纳契闭包
让我们用函数做些好玩的事情。
实现一个 fibonacci
函数,它返回一个函数(闭包),该闭包返回一个斐波纳契数列 (0, 1, 1, 2, 3, 5, ...)
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package main
import "fmt"
// 返回一个“返回int的函数”
// 参考上一个案例
func fibonacci() func() int {
a, b := 0, 1
return func() int {
t := a
a, b = b, b + a
return t
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
5.2.1 方法
Go
没有类。不过你可以为结构体类型定义方法。
方法就是一类带特殊的 接收者 参数的函数。
方法接收者在它自己的参数列表内,位于 func
关键字和方法名之间。
在此例中,Abs
方法拥有一个名为 v
,类型为 Vertex
的接收者。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package 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())
}
5.2.2 方法即函数
记住:方法只是个带接收者参数的函数。
现在这个 Abs
的写法就是个正常的函数,功能并没有什么变化。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
30package 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 Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (v Vertex) Add(x, y float64) float64 {
return x + y
}
func Add(x, y float64) float64 {
return x + y
}
func main() {
v := Vertex{3, 4}
fmt.Println(Abs(v))
fmt.Println(v.Abs())
fmt.Println(v.Add(v.X, v.Y))
fmt.Println(Add(v.X, v.Y))
}
5.2.3 方法(续)
你也可以为非结构体类型声明方法。
在此例中,我们看到了一个带 Abs 方法的数值类型 MyFloat。
你只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。
(译注:就是接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package main
import (
"fmt"
"math"
)
type MyFloat float64 // 类似于C++ typedef 关键字 例子 typedef long long LL
func (f MyFloat) Abs() float64 {
// 返回一个正数
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
f := MyFloat(-math.Sqrt(2)) // math.Sqrt(2)调用函数,math.Sqrt2 是常量
fmt.Println(f.Abs())
}
5.3.1 指针接收者
你可以为指针接收者声明方法。
这意味着对于某类型 T,接收者的类型可以用 *T
的文法。(此外,T
不能是像 *int
这样的指针。)
例如,这里为 *Vertex
定义了 Scale
方法。
指针接收者的方法可以修改接收者指向的值(就像 Scale
在这做的)。由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。
试着移除第 16
行 Scale
函数声明中的 *
,观察此程序的行为如何变化。
并不会改变结构体中的X
和Y
若使用值接收者,那么 Scale
方法会对原始 Vertex
值的副本进行操作。(对于函数的其它参数也是如此。)Scale
方法必须用指针接受者来更改 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
26package 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 (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}
5.3.2 指针与函数
现在我们要把 Abs
和 Scale
方法重写为函数。
同样,我们先试着移除掉第 16 的 *。你能看出为什么程序的行为改变了吗?要怎样做才能让该示例顺利通过编译?
(若你不确定,继续往下看。)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
26package 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 Scale(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
Scale(&v, 10)
fmt.Println(Abs(v))
}
5.3.3 方法与指针重定向
比较前两个程序,你大概会注意到带指针参数的函数必须接受一个指针:1
2
3var v Vertex
ScaleFunc(v, 5) // 编译错误!
ScaleFunc(&v, 5) // OK
而以指针为接收者的方法被调用时,接收者既能为值又能为指针:1
2
3
4var v Vertex
v.Scale(5) // OK
p := &v
p.Scale(10) // OK
对于语句 v.Scale(5)
,即便 v
是个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale
方法有一个指针接收者,为方便起见,Go
会将语句 v.Scale(5)
解释为 (&v).Scale(5)
。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
32package 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.Scale(2)
ScaleFunc(&v, 10)
p := &Vertex{4, 3}
p.Scale(3)
ScaleFunc(p, 8)
fmt.Println(v, p)
}
/*
{60 80} &{96 72}
*/
5.3.4 方法与指针重定向(续)
同样的事情也发生在相反的方向。
接受一个值作为参数的函数必须接受一个指定类型的值:1
2
3var v Vertex
fmt.Println(AbsFunc(v)) // OK
fmt.Println(AbsFunc(&v)) // 编译错误!
而以值为接收者的方法被调用时,接收者既能为值又能为指针:1
2
3
4var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK
这种情况下,方法调用 p.Abs()
会被解释为 (*p).Abs()
。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
28package 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 AbsFunc(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
fmt.Println(AbsFunc(v))
// (*p)是一个值
p := &Vertex{4, 3}
fmt.Println(p.Abs())
fmt.Println(AbsFunc(*p))
}
5.3.5 选择值或指针作为接收者
使用指针接收者的原因有二:
首先,方法能够修改其接收者指向的值。
其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。
在本例中,Scale
和 Abs
接收者的类型为 *Vertex
,即便 Abs
并不需要修改其接收者。
通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。(我们会在接下来几页中明白为什么。)
直接建议统一以指针为接收者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
26package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := &Vertex{3, 4}
fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
v.Scale(5)
fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}
5.4.1 接口
接口类型是由一组方法签名定义的集合。
接口类型的变量可以保存任何实现了这些方法的值。
注意: 示例代码的 22 行存在一个错误。由于 Abs 方法只为 *Vertex (指针类型)定义,因此 Vertex(值类型)并未实现 Abser。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
44package main
import (
"fmt"
"math"
)
type Abser interface {
Abs() float64
}
func main() {
// 类似于Java的接口,很容易理解
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 { // MyFloat接收者实现了Abs()
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 { // *Vertex接收者实现了Abs()
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
5.4.2 接口与隐式实现
类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明,也就没有“implements”关键字。
隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。
因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package 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()
}
缺点,看这个方法不知道实现了哪个接口,明确的接口定义是一个优点,但是Go
不具备
5.4.3 接口值
接口也是值。它们可以像其它值一样传递。
接口值可以用作函数的参数或返回值。
在内部,接口值可以看做包含值和具体类型的元组:
(value, 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41package 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)
}
5.4.4 底层值为 nil
的接口值
即便接口内的具体值为 nil
,方法仍然会被 nil
接收者调用。
在一些语言中,这会触发一个空指针异常,(Java
:我感觉在说我…)但在 Go
中通常会写一些方法来优雅地处理它(如本例中的 M
方法)。
注意: 保存了 nil
具体值的接口其自身并不为 nil
。类似包含空集的集合并不为空集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
37package 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)
}
5.4.5 nil
接口值
nil
接口值既不保存值也不保存具体类型。
为 nil
接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package main
import "fmt"
type I interface {
M()
}
func main() {
var i I // 类似空指针异常
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
5.4.6 空接口
指定了零个方法的接口值被称为 空接口:interface{}
空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)
空接口被用来处理未知类型的值。例如,fmt.Print
可接受类型为 interface{}
的任意数量的参数。
1 | package main |
5.4.7 类型断言
类型断言 提供了访问接口值底层具体值的方式。
t := i.(T)
该语句断言接口值 i
保存了具体类型 T
,并将其底层类型为 T
的值赋予变量 t
。
若 i
并未保存 T
类型的值,该语句就会触发一个panic
。
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
t, ok := i.(T)
若 i
保存了一个 T,那么 t
将会是其底层值,而 ok
为 true
。
否则,ok
将为 false
而 t
将为 T
类型的零值,程序并不会产生panic
。
请注意这种语法和读取一个映射时的相同之处。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package main
import "fmt"
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
f = i.(float64) // 报错(panic)
fmt.Println(f)
}
5.4.8 类型选择
类型选择 是一种按顺序从几个类型断言中选择分支的结构。
类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。1
2
3
4
5
6
7
8switch 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
的接口类型和值相同。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package main
import "fmt"
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("%T Twice %v is %v\n", v, v, v*2)
case string:
fmt.Printf("%T %q is %v bytes long\n", v, v, len(v))
default:
fmt.Printf("%T I don't know about type %T!\n", v, v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
/*
int Twice 21 is 42
string "hello" is 5 bytes long
bool I don't know about type bool!
*/
5.4.9 Stringer
fmt
包中定义的 Stringer
是最普遍的接口之一。1
2
3type Stringer interface {
String() string
}Stringer
是一个可以用字符串描述自己的类型。fmt
包(还有很多包)都通过此接口来打印值。
类似于Java
中的toString()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package main
import "fmt"
type Person struct {
Name string
Age int
}
/*
暗中实现了
type Stringer interface {
String() string
}
但是你却不知道
*/
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)
}
5.4.10 练习: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
20package 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)
}
}
5.5.1 错误
Go
程序使用 error
值来表示错误状态。
与 fmt.Stringer
类似,error
类型是一个内建接口:1
2
3type error interface {
Error() string
}
(与 fmt.Stringer
类似,fmt
包在打印值时也会满足 error
。)
通常函数会返回一个 error
值,调用的它的代码应当判断这个错误是否等于 nil
来进行错误处理。1
2
3
4
5
6i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)error
为 nil
时表示成功;非 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
32package 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 {
return &MyError{
time.Now(),
"it didn't work",
}
}
func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}
/*
at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn't work
*/
5.5.2 练习:错误
从之前的练习中复制 Sqrt
函数,修改它使其返回 error
值。Sqrt
接受到一个负数时,应当返回一个非 nil
的错误值。复数同样也不被支持。
创建一个新的类型1
type ErrNegativeSqrt float64
并为其实现1
func (e ErrNegativeSqrt) Error() string
方法使其拥有 error
值,通过 ErrNegativeSqrt(-2).Error()
调用该方法应返回 cannot Sqrt negative number: -2
。
注意: 在 Error
方法内调用 fmt.Sprint(e)
会让程序陷入死循环。可以通过先转换 e
来避免这个问题:fmt.Sprint(float64(e))
。这是为什么呢?
修改 Sqrt
函数,使其接受一个负数时,返回 ErrNegativeSqrt
值。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
28package main
import (
"fmt"
"math"
)
/*
type error interface {
Error() string
}
*/
type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string{
return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e)) // 必须将ErrNegativeSqrt转换成 float64
}
func Sqrt(x float64) (float64, error) { // 返回错误
if(x < 0){
return 0, ErrNegativeSqrt(x) // 将float64转为ErrNegativeSqrt
}
return math.Sqrt(x), nil
}
func main() {
fmt.Println(Sqrt(2)) // 打印 math.Sqrt(x), nil
fmt.Println(Sqrt(-2))
fmt.Println(ErrNegativeSqrt(-2).Error())
}
特别注意,在 Error
函数内调用 fmt.Sprintf
函数,必须先将错误值转换成 float64
。否则 fmt
包会根据 error
接口继续调用 Error
方法,陷入死循环。感兴趣的小伙伴可以去看一下源码。
5.6.1 Reader
io
包指定了 io.Reader
接口,它表示从数据流的末尾进行读取。
Go
标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。
io.Reader
接口有一个 Read
方法:1
func (T) Read(b []byte) (n int, err error)
Read
用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF
错误。
示例代码创建了一个 strings.Reader
并以每次 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package 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
}
}
}
5.6.2 练习:Reader
实现一个 Reader
类型,它产生一个 ASCII
字符 'A'
的无限流。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package main
import "golang.org/x/tour/reader"
type MyReader struct{}
// TODO: 给 MyReader 添加一个 Read([]byte) (int, error) 方法
func (r MyReader) Read(b []byte) (int, error){
n := len(b)
for i := 0; i < n; i ++ {
b[i] = 'A'
}
return n, nil
}
func main() {
reader.Validate(MyReader{})
}
5.6.3 练习:rot13Reader
有种常见的模式是一个 io.Reader
包装另一个 io.Reader
,然后通过某种方式修改其数据流。
例如,gzip.NewReader
函数接受一个 io.Reader
(已压缩的数据流)并返回一个同样实现了 io.Reader
的 *gzip.Reader
(解压后的数据流)。
编写一个实现了 io.Reader
并从另一个 io.Reader
中读取数据的 rot13Reader
,通过应用 rot13
代换密码对数据流进行修改。
rot13Reader
类型已经提供。实现 Read
方法以满足 io.Reader
。
rot13
介绍:
套用ROT13
到一段文字上仅仅只需要检查字符字母顺序并取代它在13
位之后的对应字母,有需要超过时则重新绕回26
英文字母开头即可。 A
换成N
、B
换成O
、依此类推到M
换成Z
,然后序列反转:N
换成A
、O
换成B
、最后Z
换成M
。只有这些出现在英文字母里头的字符受影响;数字、符号、空白字符以及所有其他字符都不变。因为只有在英文字母表里头只有
ROT13
函数是它自己的逆反:
对任何字符
换句话说,两个连续的ROT13
应用函数会回复原始文字(在数学上,这有时称之为对合(involution);在密码学上,这叫做点对点加密(reciprocal cipher))。
转换可以利用查找表完成,如下例所示:1
2ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm
例如,下面的英文笑话,精华句为ROT13
所隐匿:1
2
3How can you tell an extrovert from an
introvert at NSA? Va gur ryringbef,
gur rkgebireg ybbxf ng gur BGURE thl'f fubrf.
透过ROT13
表格转换整片文字,该笑话的解答揭露如下:1
2
3Ubj pna lbh gryy na rkgebireg sebz na
vagebireg ng AFN? In the elevators,
the extrovert looks at the OTHER guy's shoes.
第二次ROT13
函数将转回原始文字。
完整句子1
2
3
4How can you tell an extrovert from an
introvert at NSA?
In the elevators,
the extrovert looks at the OTHER guy's shoes.
先实现一个这个函数玩一玩:
A
替换为N
,B
替换为O
, 以此类推… 直到M
到Z
, 然后N
到A
- 总结:加密字母替换为该字母在字母表的后第13个字母,超过26取模,然后只能加密大小写字母,两次加密恢复原状。
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
36package main
import (
"fmt"
)
func rot13(b byte) byte {
if b >= 'a' && b <= 'z' {
return 'a' + (b - 'a' + 13) % 26
} else if b >= 'A' && b <= 'Z' {
return 'A' + (b - 'A' + 13) % 26
} else {
return b
}
}
func main() {
s := "HELLO"
for _, v := range s {
fmt.Printf("v: %v\n", rot13(byte(v)))
}
fmt.Printf("\"==========\": %v\n", "==========")
b := make([]byte, 5)
for i, v := range s {
b[i] = rot13(byte(v))
}
str := string(b)
for i, v := range str {
b[i] = rot13(byte(v))
}
fmt.Printf("str: %v\n", str)
fmt.Printf("\"==========\": %v\n", "==========")
str = string(b)
fmt.Printf("str: %v\n", str)
}
练习: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
43package main
import (
"io"
"os"
"strings"
)
type rot13Reader struct {
r io.Reader
}
func rot13(b byte) byte {
if b >= 'a' && b <= 'z' {
return 'a' + (b - 'a' + 13) % 26
} else if b >= 'A' && b <= 'Z' {
return 'A' + (b - 'A' + 13) % 26
} else {
return b
}
}
/*
io.Reader 接口有一个 Read 方法:
func (T) Read(b []byte) (n int, err error)
*/
func (r rot13Reader) Read(b []byte) (int, error) {
n, err := r.r.Read(b) // 调用
if err != nil {
return n, err
}
for i := 0; i < n; i++ {
b[i] = rot13(b[i])
}
return n, nil
}
func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!") // io.Reader
r := rot13Reader{s}
io.Copy(os.Stdout, &r)
}
5.7.1 图像
image
包定义了 Image
接口:1
2
3
4
5
6
7package image
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
注意: Bounds
方法的返回值 Rectangle
实际上是一个 image.Rectangle
,它在 image
包中声明。Bounds
方法返回的 Rectangle
类型,也定义在 image
包里面。它代表一个 image.Rectangle
,由坐标最小的顶点 Min
和坐标最大的顶点 Max
界定。
(请参阅文档了解全部信息。)
color.Color
和 color.Model
类型也是接口,但是通常因为直接使用预定义的实现 image.RGBA
和 image.RGBAModel
而被忽视了。这些接口和类型由 image/color
包定义。1
2
3
4
5
6
7
8
9
10
11
12
13package 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())
}
部分文档:
type Image
1 | type Image interface { |
Image
is a finite rectangular grid(有限的长方形网格) of color.Color
values taken from a color model.
type Rectangle
1 | type Rectangle struct { |
A Rectangle contains the points with Min.X <= X < Max.X, Min.Y <= Y < Max.Y. It is well-formed if Min.X <= Max.X and likewise for Y. Points are always well-formed. A rectangle’s methods always return well-formed outputs for well-formed inputs.
翻译一遍:
一个Rectangle
包含Min.X <= X < Max.X
,Min.Y <= Y < Max.Y
的点。如果Min.X <= Max.X
,则为良构。X和y也是一样,点总是形式良好的。一个rectangle
的方法对于格式良好的的输入总是返回格式良好的输出。1
var ZR Rectangle
ZR is the zero Rectangle(
type Point1
2
3type Point struct {
X, Y int
}
A Point is an X, Y coordinate pair. The axes increase right and down.1
var ZP Point
ZP is the zero Point.
Deprecated: Use a literal image.Point{}
instead.
5.7.2 练习:图像
还记得之前编写的图片生成器吗?我们再来编写另外一个,不过这次它将会返回一个 image.Image
的实现而非一个数据切片。
定义你自己的 Image 类型,实现必要的方法并调用 pic.ShowImage
。
Bounds
应当返回一个 image.Rectangle
,例如 image.Rect(0, 0, w, h)
。
ColorModel
应当返回 color.RGBAModel
。
At
应当返回一个颜色。上一个图片生成器的值 v
对应于此次的 color.RGBA{v, v, 255, 255}
。
1 | package main |
6. 泛型
6.1.1 类型参数
使用 类型参数 ,Go
语言能够编写可以处理多种类型的函数。函数的类型参数位于参数列表前,以中括号括起来:1
func Index[T comparable](s []T, x T) int
这个声明意味着s
是T
类型的切片,它实现了内建约束comparable
,x
也是相同的类型comparable
是一种很有用的类型约束,满足这种约束的类型 T
可以使用 ==
和 !=
运算符进行比较。下面这个例子,我们通过逐一比较,从切片元素中找出指定值。通过类型参数 T
,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
27
28
29
30package main
import "fmt"
// Index returns the index of x in s, or -1 if not found.
// Index 返回x在s中的下标,如果不存在返回-1
func Index[T comparable](s []T, x T) int {
for i, v := range s {
// v and x are type T, which has the comparable
// v和x是类型T,这是可以比较的
// constraint, so we can use == here.
// 因为是约束,所有我们可以使用==
if v == x {
return i
}
}
return -1
}
func main() {
// Index works on a slice of ints
// Index函数可以用在int类型切片,也可以用在string类型切片
si := []int{10, 20, 15, -10}
fmt.Println(Index(si, 15))
// Index also works on a slice of strings
ss := []string{"foo", "bar", "baz"}
fmt.Println(Index(ss, "hello"))
}
6.1.2 泛型类型
除了泛型函数,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
108package main
import (
"fmt"
)
// List represents a singly-linked list that holds
// values of any type.
// 定义一个节点结构体
type List[T any] struct {
next *List[T]
val T
}
// create nil list of type T
// 创建空列表
func NewList[T any] () *List[T] {
return nil
}
// insert val before list
// 头插,直接返回该节点指针
func (list *List[T]) Insert(val T) (*List[T]) {
return &List[T]{ // 返回新头节点
next: list, // next 指向 头节点
val: val,
}
}
// insert val after list
// 当前值后面插入
func (list *List[T]) Append(val T) (*List[T]) {
if (list == nil) {
return list.Insert(val)
}
// 新建一个节点
node := &List[T] {
next: list.next, // 指向头节点的下一个节点
val: val,
}
list.next = node // 头节点指向新的节点
return list
}
// return new list with elements reversed
// 反转链表
func (list *List[T]) Reverse() *List[T] {
var reversed *List[T]
for cursor := list; cursor != nil; cursor = cursor.next {
reversed = reversed.Insert(cursor.val)
}
return reversed
}
// traverse list one by one
// 遍历链表
func (list *List[T]) Traverse(f func (val T, l *List[T])) {
for cursor := list; cursor != nil; cursor=cursor.next {
f(cursor.val, cursor)
}
}
// print list
// 打印链表
func (list *List[T]) Print() {
list.Traverse(func (val T, _ *List[T]) {
fmt.Printf("%v ", val)
})
}
// print list with line break
// 重写Println()方法 打印链表+换行
func (list *List[T]) Println() {
list.Print()
fmt.Println()
}
func main() {
// initialize a new list
l := NewList[string]().Insert("apple").Insert("banana").Append("cat1").Append("cat2").Append("cat3").Append("cat4").Append("cat5")
// print it
l.Println()
// reverse it
l = l.Reverse()
// print it again
l.Println()
fmt.Printf("\"======================\": %v\n", "======================")
// initialize a new list
l1 := NewList[string]().
Insert("apple").Insert("banana").Append("cat1").Append("cat2")
l1 = l1.Append("cat3").Append("cat4").Append("cat5")
// print it
l1.Println()
// reverse it
l1 = l1.Reverse()
// print it again
l1.Println()
}
/*
banana cat apple
apple cat banana
*/
7. 并发
7.1.1 协程(goroutine
)
协程(goroutine)是由 Go
运行时管理的轻量级线程。1
go f(x, y, z)
会启动一个新的 协程 并执行1
f(x, y, z)
f
, x
, y
和 z
的求值发生在当前的 协程 中,而 f
的执行发生在新的 协程 中。
协程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync
包提供了这种能力,不过在 Go
中并不经常用到,因为还有其它的办法(见下一页)。
1 | package main |
7.2.1 信道(Channels
)
信道是带有类型的管道,你可以通过它用信道操作符 <-
来发送或者接收值。1
2ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
(“箭头”就是数据流的方向。)
和映射与切片一样,信道在使用前必须创建:1
ch := make(chan int)
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 协程 可以在没有显式的锁或竞态变量的情况下进行同步。
以下示例对切片中的数进行求和,将任务分配给两个 协程 。一旦两个 协程 完成了它们的计算,它就能算出最终的结果。
1 | package main |
7.2.2 带缓冲的信道
信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make
来初始化一个带缓冲的信道:1
ch := make(chan int, 100)
仅当信道的缓冲区填满后,向其发送数据时才会阻塞(发送方)。当缓冲区为空时,接受方会阻塞。
修改示例填满缓冲区,然后看看会发生什么。1
2
3
4
5
6
7
8
9
10
11
12
13package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
// ch <- 3 // 会死锁
fmt.Println(<-ch) // 将ch信道的值传递给fmt.Println()的参数
fmt.Println(<-ch)
}
7.2.3 range
和 close
发送者可通过 close
关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完1
v, ok := <-ch
之后 ok
会被设置为 false
。
循环 for i := range c
, c
是一个信道 会不断从信道接收值,直到它被关闭。
注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序panic
。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range
循环。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package 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) // 创建int信道,缓冲为10
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
7.2.4 select
语句
select
语句使一个 协程 可以等待多个通信操作。
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
29package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x: // 会先执行10次
x, y = y, x+y
case <-quit: // 执行一次
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
7.2.5 默认选择
当 select
中的其它分支都没有准备好时,default
分支就会执行。
为了在尝试发送或者接收时不发生阻塞,可使用 default
分支:1
2
3
4
5
6select {
case i := <-c:
// 使用 i
default:
// 从 c 中接收会阻塞时执行
}
前置知识:
func Tick
1 | func Tick(d Duration) <-chan Time |
介绍
Tick
是NewTicker
的方便包装器,仅提供对ticking
信道的访问。虽然Tick
对于不需要关闭Ticker
的客户端很有用,但请注意,如果没有关闭它的方法,底层的Ticker
就不能被垃圾回收器恢复;它“泄漏”。与NewTicker
不同,如果d <= 0
, Tick
将返回nil
。(会被GC
回收)
下面例子,
1 | package main |
func After
1
func After(d Duration) <-chan Time
介绍
After waits for the duration to elapse and then sends the current time on the returned channel. It is equivalent to NewTimer(d).C. The underlying Timer is not recovered by the garbage collector until the timer fires. If efficiency is a concern, use NewTimer instead and call Timer.Stop if the timer is no longer needed.
(英语不是很好😁)
After等待持续时间流逝,然后在返回的通道上发送当前时间。它等价于NewTimer(d).C
。底层计时器直到计时器触发才被垃圾收集器恢复。如果效率是一个问题,使用NewTimer代替,并调用Timer。如果不再需要计时器,则停止。
func NewTimer(计时器)1
func NewTimer(d Duration) *Timer
NewTimer
creates a new Timer
that will send the current time on its channel after at least duration d.
例如经过发送当前时间
New*
的命名方式类似于其他语言new
对象
我们来实现这个案例
1 | package main |
1 | package main |
案例代码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
41package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond) // 0.1秒
boom := time.After(500 * time.Millisecond) // 0.5 秒
for {
select {
case <-tick:
fmt.Println("tick.") // 执行4次
case <-boom:
fmt.Println("BOOM!") // 第5次执行After而不执行Tick
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond) // 0.05 秒
}
}
}
/*
.
.
tick.
.
.
tick.
.
.
tick.
.
.
tick.
.
.
BOOM!
*/
7.2.5 练习:等价二叉查找树(又称二叉排序树)
这个不太会大佬教教我🤩🤩
不同二叉树的叶节点上可以保存相同的值序列。例如,以下两个二叉树都保存了序列 1,1,2,3,5,8,13
。
这里说的应该是前序遍历
在大多数语言中,检查两个二叉树是否保存了相同序列的函数都相当复杂。 我们将使用 Go
的并发和信道来编写一个简单的解法。
本例使用了 tree
包,它定义了类型:1
2
3
4
5type Tree struct {
Left *Tree
Value int
Right *Tree
}
实现
Walk
函数。测试
Walk
函数。
函数 tree.New(k)
用于构造一个随机结构的已排序二叉查找树,它保存了值 k, 2k, 3k, ..., 10k
。
创建一个新的信道 ch
并且对其进行步进:1
go Walk(tree.New(1), ch)
然后从信道中读取并打印 10 个值。应当是数字 1, 2, 3, ..., 10
。
用
Walk
实现Same
函数来检测t1
和t2
是否存储了相同的值。测试
Same
函数。
Same(tree.New(1), tree.New(1))
应当返回 true
,而 Same(tree.New(1), tree.New(2))
应当返回 false
。
Tree
的文档可在这里找到。
type Tree1
2
3
4
5type Tree struct {
Left *Tree
Value int
Right *Tree
}
A Tree is a binary tree with integer values.
func New1
func New(k int) *Tree
New returns a new, random binary tree holding the values k, 2k, ..., 10k
.
func (*Tree) String
1 | func (t *Tree) String() string |
Source Files
tree.go
1 | // Copyright 2011 The Go Authors. All rights reserved. |
练习
1 | package main |
7.2.6 sync.Mutex
我们已经看到信道非常适合在各个 协程 间进行通信。
但是如果我们并不需要通信呢?比如说,若我们只是想保证每次只有一个 协程 能够访问一个共享的变量,从而避免冲突?
这里涉及的概念叫做 互斥(mutualexclusion) ,我们通常使用 互斥锁(Mutex) 这一数据结构来提供这种机制。
Go
标准库中提供了 sync.Mutex
互斥锁类型及其两个方法:1
2Lock
Unlock
我们可以通过在代码前调用 Lock
方法,在代码后调用 Unlock
方法来保证一段代码的互斥执行。参见 Inc
方法。
我们也可以用 defer
语句来保证互斥锁一定会被解锁。参见 Value
方法。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
40package 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"))
}
7.2.7 练习:Web 爬虫
这个不太会大佬教教我🤩🤩
在这个练习中,我们将会使用 Go
的并发特性来并行化一个 Web
爬虫。
修改 Crawl
函数来并行地抓取 URL
,并且保证不重复。
提示:你可以用一个 map
来缓存已经获取的 URL
,但是要注意 map
本身并不是并发安全的!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
131package main
import (
"errors"
"fmt"
"sync"
)
type Fetcher interface {
// Fetch returns the body of URL and
// a slice of URLs found on that page.
Fetch(url string) (body string, urls []string, err error)
}
// fetched tracks URLs that have been (or are being) fetched.
// The lock must be held while reading from or writing to the map.
// See https://golang.org/ref/spec#Struct_types section on embedded types.
var fetched = struct {
m map[string]error
sync.Mutex
}{m: make(map[string]error)}
var loading = errors.New("url load in progress") // sentinel value
// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
if depth <= 0 {
fmt.Printf("<- Done with %v, depth 0.\n", url)
return
}
fetched.Lock()
if _, ok := fetched.m[url]; ok {
fetched.Unlock()
fmt.Printf("<- Done with %v, already fetched.\n", url)
return
}
// We mark the url to be loading to avoid others reloading it at the same time.
fetched.m[url] = loading
fetched.Unlock()
// We load it concurrently.
body, urls, err := fetcher.Fetch(url)
// And update the status in a synced zone.
fetched.Lock()
fetched.m[url] = err
fetched.Unlock()
if err != nil {
fmt.Printf("<- Error on %v: %v\n", url, err)
return
}
fmt.Printf("Found: %s %q\n", url, body)
done := make(chan bool)
for i, u := range urls {
fmt.Printf("-> Crawling child %v/%v of %v : %v.\n", i, len(urls), url, u)
go func(url string) {
Crawl(url, depth-1, fetcher)
done <- true
}(u)
}
for i, u := range urls {
fmt.Printf("<- [%v] %v/%v Waiting for child %v.\n", url, i, len(urls), u)
<-done
}
fmt.Printf("<- Done with %v\n", url)
}
func main() {
Crawl("https://golang.org/", 4, fetcher)
fmt.Println("Fetching stats\n--------------")
for url, err := range fetched.m {
if err != nil {
fmt.Printf("%v failed: %v\n", url, err)
} else {
fmt.Printf("%v was fetched\n", url)
}
}
}
// fakeFetcher is Fetcher that returns canned results.
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 is a populated fakeFetcher.
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/",
},
},
}
接下来深入学习Go
基础
Go
语言圣经Go
中的一等函数Go
编程语言规范- 如何使用
Go
编程 - 时效
Go
编程Effective Go
Go
内存模型- 通过通信共享内存
Go by Example
Go
设计模式Go
项目结构Go
高并发编程
接下里就是继续更新Go
语言基础了,欢迎大家关注😊,你们的关注,就是我最大的动力
还望大家关注一下咱们国产语言,(我们必须拥有自己的语言)
凹语言:
凹语言官网
再了解了解WebAssembly
声明
因为本人才疏学浅,博客中难免有不足之处,还望个位大佬指出,我会及时纠正。
我的博客内容均是摘自Go
语言官网,因为是英语有什么翻译错误的地方,还望大佬指出,非常感谢~