Go 语言快速入门
Table of Contents
-
Go 语言快速入门
- 一、简介和准备
- 二、做个计算器
- 三、循环和分支
- 四、变量及作用域
- 五、第一阶段测试题
- 六、实数
- 七、整数类型
- 八、比较大的数
- 九、多语言文本
- 十、类型间转换
- 十一、第二部分测试题
- 十二、函数
- 十三、方法
- 十四、一等函数
- 十五、第三部分习题
- 十六、数组
- 十七、Slice(切片)
- 十八、更大的slice
- 十九、Map
- 二十、第四部分习题
- 二十一、结构
- 二十二、Go语言没有class
- 二十三、组合与转发 Composition and forwarding
- 二十四、接口
- 二十五、第五部分习题
- 二十六、指针
- 二十七、nil
- 二十八、错误
- 二十九、第六部分习题
- 三十、goroutine和并发(concurrent)
- 三十一、并发状态
- 三十二、第七部分习题
Go 语言快速入门
一、简介和准备
什么是 Go?
- Go 语言是一门编译语言。
- 在你运行程序之前,Go首先使用编译器把你的代码转换成机器能够读懂的 1 和 0。
- 它会把你所有的代码编译成一个可执行文件,在编译的过程中,Go编译器能够捕获一些错误。
- 不是所有的编程语言都使用这种方式。
- Python、Ruby 等很多语言都是使用解释器,随着程序的运行,一个语句一个语句的进行翻译。但这也意味着 bug 可能就潜伏在你还没有测试过的路径上。
- 这些就是解释型语言。
Go 的由来和评价
“我们想要一个安全的、静态编译的、高性能的、类似 C++ 和 Java 这样的语言,但是得更轻量级并且要像Python这种动态解释型语言这样有趣。” –– Rob Pike
Go语言是通过对软件开发体验这方面经过大量深思熟虑之后而设计的。大量的程序只需一个命令便在几秒钟内就能完成编译。该语言省略了能导致歧义的特性,鼓励了可预测且易于理解的代码。Go为Java等经典语言所强加的刚性结构,提供了替代品。
Go是一个开源的编程语言,能够构建大规模简单、高效、可靠的软件。
Go Playground
安装 Go 和开发工具
Go语言中文网:https://studygolang.com/
- Go安装包 https://studygolang.com/dl
- Visual Studio Code
- Go 插件 (>go Go: install/update tools)
- Go 代理
- go env -w GO111MODULE=on
- go env -w GOPROXY=https://goproxy.cn,direct
包和函数 package & function
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, playground")
}
作业题
- Go 编译器有哪些优点?
- Go 的程序从哪里开始运行?
- fmt 这个 package 提供了哪些功能?
- 左花括号 { 放在哪不会引起语法错误?
- 编写一个程序:
- 第一行输出 Hello,World
- 第二行输出 Hello。
package main
import "fmt"
// The function declares and initializes variables of different data types in Go.
func main() {
fmt.Println("Hello World!")
fmt.Println("Hello")
}
二、做个计算器
算术运算符
- Go 语言提供了 +、-、*、/、% 来做加减乘除和取余的算术运算
// My weight loss program.
package main
import "fmt"
// main is the function where it all begins.
func main() {
fmt.Print("My weight on the surface of Mars is")
fmt.Print(149.0 * 0.3783)
fmt.Print(" libs, and I would be ")
fmt.Print(41 * 365 / 687)
fmt.Print(" years old.")
fmt.Println("My weight on the surface of Mars is", 149.0*0.3783, "libs, and I would be", 41*365.2425/687, "years old.")
}
运行
Code/go/hello via 🐹 v1.20.3
➜ go run main.go
My weight on the surface of Mars is56.3667 libs, and I would be 21 years old.My weight on the surface of Mars is 56.3667 libs, and I would be 21.79758733624454 years old.
Code/go/hello via 🐹 v1.20.3
➜
fmt.Print和fmt.Println
- 上述例子里使用了Print,Println函数。
- 可以传递若干个参数,之间用逗号分开。
- 参数可以是字符串、数字、数学表达式等等。
格式化打印
- 可以使用Printf来控制打印的输出结果。
- 与Print和Println不同,Printf的第一个参数必须是字符串。
- 这个字符串里包含了像%v这样的格式化动词,它的值由第二个参数的值所代替。
- 如果指定了多个格式化动词,那么它们的值由后边的参数值按其顺序进行替换。
// My weight loss program.
package main
import "fmt"
// main is the function where it all begins.
func main() {
fmt.Printf("My weight on the surface of Mars is %v libs,", 149.0*0.3783)
fmt.Printf(" and I would be %v years old.\n", 41*365/687)
fmt.Printf("My weight on the surface of %v is %v lbs.\n", "Earth", 149.0)
}
运行
Code/go/hello via 🐹 v1.20.3
➜ go run main.go
My weight on the surface of Mars is 56.3667 libs, and I would be 21 years old.
My weight on the surface of Earth is 149 lbs.
Code/go/hello via 🐹 v1.20.3
➜
使用Printf对齐文本
- 在格式化动词里指定宽度,就可以对齐文本。
- 例如,%4v,就是向左填充到足够4个宽度
- 正数,向左填充空格
- 负数,向右填充空格
// My weight loss program.
package main
import "fmt"
// main is the function where it all begins.
func main() {
fmt.Printf("%-15v $%4v\n", "SpaceX", 94)
fmt.Printf("%-15v $%4v\n", "Virgin Galactic", 100)
}
运行
Code/go/hello via 🐹 v1.20.3
➜ go run main.go
SpaceX $ 94
Virgin Galactic $ 100
Code/go/hello via 🐹 v1.20.3
➜
常量 和 变量
- const,用来声明常量
- 常量的值不可以改变
- var,用来声明变量
- 想要使用变量首先需要进行声明
// My weight loss program.
package main
import "fmt"
// main is the function where it all begins.
func main() {
const lightSpeed = 200792 // km/s
var distance = 56000000 // km
fmt.Println(distance/lightSpeed, "seconds")
distance = 401000000
fmt.Println(distance/lightSpeed, "seconds")
}
运行
Code/go/hello via 🐹 v1.20.3
➜ go run main.go
278 seconds
1997 seconds
Code/go/hello via 🐹 v1.20.3
➜
同时声明多个变量
// My weight loss program.
package main
import "fmt"
// main is the function where it all begins.
func main() {
var distance = 56000000
var speed = 100800
var (
distance = 56000000
speed = 100800
)
var distance, speed = 56000000, 100800
const hoursPerDay, minutesPerHour = 24, 60
}
赋值运算符
// My weight loss program.
package main
// main is the function where it all begins.
func main() {
var weight = 149.0
weight = weight * 0.3783
weight *= 0.3783
}
自增运算符
- 但是Go里面没有类似C#的++count这种操作。
// My weight loss program.
package main
// main is the function where it all begins.
func main() {
var age = 41
age = age + 1
age += 1
age++
}
猜数
- 使用rand包,可以生成伪随机数
- 例如,Intn可以返回一个指定范围的随机整数
- import的路径是 “math/rand”
// My weight loss program.
package main
import (
"fmt"
"math/rand"
)
// main is the function where it all begins.
func main() {
var num = rand.Intn(10) + 1
fmt.Println("num = ", num)
num = rand.Intn(10) + 1
fmt.Println("num = ", num)
}
运行
Code/go/hello via 🐹 v1.20.3
➜ go run main.go
num = 10
num = 3
Code/go/hello via 🐹 v1.20.3
作业题
- Malacandra是C.S. Lewis在《太空三部曲》中给火星起的另一个名字。编写程序来确定飞船要在28天内到达Malacandra的行进速度(公里/小时)。假设距离为56,000,000公里。
// My weight loss program.
package main
import (
"fmt"
)
// main is the function where it all begins.
func main() {
var kilometer = 56000000
var day = 28
var speed = kilometer / (day * 24)
fmt.Println("speed = ", speed)
}
三、循环和分支
Boolean类型
- 你可能经常会遇到这样的问题:
- 如果你走出了洞穴,那么请翻到21页
- 那么你是否走出了洞穴?
- 这个问题的答案只有true和false两种情况。
- true和false就是两个已声明好的常量。
- 某些语言(例如js)会把“”这种字符串当作false,其他的字符串当作true。
- 而Go语言则不行,只有true是真的,只有false是假的。
strings.Contains
- 来自strings包的Contains函数可以判断某个字符串是否包含另外一个字符串
package main
import (
"fmt"
"strings"
)
// main is the function where it all begins.
func main() {
fmt.Println("You find yourself in a dimly lit cavern.")
var command = "walk outside"
var exit = strings.Contains(command, "outside")
fmt.Println("You leave the cave:", exit)
}
比较
- 如果我们比较两个值,得到的结果也是true或false
- 比较运算符:
- ==
- <=
- <
- !=
- =>
>
package main
import (
"fmt"
)
// main is the function where it all begins.
func main() {
fmt.Println("There is a sign near the entrance that reads 'No Minors'.")
var age = 41
var minor = age < 18
fmt.Printf("At age %v, am I a minor? %v\n", age, minor)
}
使用if来做分支
package main
import (
"fmt"
)
// main is the function where it all begins.
func main() {
var command = "go east"
if command == "go east" {
fmt.Println("You head further up the mountain.")
} else if command == "go inside" {
fmt.Println("You enter the cave where you live out the rest of your life.")
} else {
fmt.Println("Didn't quite get that.")
}
}
- 在这里else和else if都是可选的
- 而且else if可以重复多个
逻辑运算符
- ||表示或,&&表示与。它们通常用来同时检查多个条件
package main
import (
"fmt"
)
// main is the function where it all begins.
func main() {
fmt.Println("The year is 2100, should you leap?")
var year = 2100
var leap = year%400 == 0 || (year%4 == 0 && year%100 != 0)
if leap {
fmt.Println("Look before you leap!")
} else {
fmt.Println("Keep your feet on the ground.")
}
}
- 这里使用了短路逻辑
取反逻辑运算符
- !,可以把true变为false,反之亦然
package main
import (
"fmt"
)
// main is the function where it all begins.
func main() {
var haveTorch = true
var litTorch = false
if !haveTorch || !litTorch {
fmt.Println("Nothing to see here.")
}
}
使用switch做分支
package main
import (
"fmt"
)
// main is the function where it all begins.
func main() {
fmt.Println("There is cavern entrance here and a path to the east.")
var command = "go inside"
switch command {
case "go east":
fmt.Println("You head further up the mountain.")
case "enter cave", "go inside":
fmt.Println("You find yourself in a dimly lit cavern.")
case "read sign":
fmt.Println("The sign reads 'No Minors'.")
default:
fmt.Println("Didn't quite get that.")
}
}
- switch语句也可以对数字进行匹配
- 还有一个fallthrough关键字,它用来执行下一个case的body部分。这一点与C#、Java等语言不一样。
package main
import (
"fmt"
)
// main is the function where it all begins.
func main() {
var room = "lake"
switch {
case room == "cave":
fmt.Println("You find yourself in a dimly lit cavern.")
case room == "lake":
fmt.Println("The ice seems solid enough.")
fallthrough
case room == "underwater":
fmt.Println("The water is freezing cold.")
}
}
使用循环做重复
- for关键字可以让你的代码重复执行
- for后边没有跟条件,那就是无限循环。
- 可以使用break跳出循环
package main
import (
"fmt"
"time"
)
// main is the function where it all begins.
func main() {
var count = 10
for count > 0 {
fmt.Println(count)
time.Sleep(time.Second)
count--
}
// var count = 5
// for {
// if count < 0 {
// break
// }
// fmt.Println(count)
// time.Sleep(time.Second)
// count--
// }
fmt.Println("Liftoff!")
}
作业题
- 实现一个猜数游戏,首先定义一个1-100的整数。然后让计算机生成一个1-100随机数,并显示计算机猜测的结果是太大了还是太小了,没猜对的话就继续猜,直至猜对。
package main
import (
"fmt"
"math/rand"
)
// main is the function where it all begins.
func main() {
var num int
rand_num := random(1, 100)
for {
fmt.Print("请输入一个1-100的整数:")
fmt.Scanln(&num)
// fmt.Printf("扫描结果 num:%d \n", num)
if num > rand_num {
fmt.Println("您猜大了!")
continue
} else if num < rand_num {
fmt.Println("您猜小了!")
continue
} else {
fmt.Println("您猜对了!")
break
}
}
}
func random(min, max int) int {
return rand.Intn(max-min) + min
}
Vscode font:Fira code
四、变量及作用域
变量的作用域
- 当变量被声明以后,它就进入了作用域(变量就变得可见了)。
- 只要变量在作用域内,你就可以访问它
- 否则,访问它会报错的
- 变量声明的位置,决定了它的作用域。
- 作用域的好处?
- 可以在不同的作用域内使用相同的变量名
- 在Go里面,作用域的范围就是{ }之间的部分
package main
import (
"fmt"
"math/rand"
)
// main is the function where it all begins.
func main() {
var count = 0
for count < 10 {
var num = rand.Intn(10) + 1
fmt.Println(num)
count++
}
}
// count 变量就是在function作用域内
// num的作用域就是for的循环体
短声明
- 在Go里,可以使用var来声明变量:
- var count = 10
- 但是也可以使用短声明:
- count := 10
- 这两种方式的效果是一样的
- 不仅声明语句更短,而且可以在无法使用var的地方使用
使用var声明变量
var count = 0
for count = 10; count > 0; count-- {
fmt.Println(count)
}
fmt.Println(count)
使用短声明
for count := 10; count > 0; count-- {
fmt.Println(count)
}
例子:在if语句里使用短声明来声明变量
if num := rand.Intn(3); num == 0 {
fmt.Println("Space Adventures")
} else if num == 1 {
fmt.Println("SpaceX")
} else {
fmt.Println("Virgin Galactic")
}
例子:在switch语句里使用短声明来声明变量
switch num := rand.Intn(10); num {
case 0:
fmt.Println("Space Adventures")
case 1:
fmt.Println("SpaceX")
case 2:
fmt.Println("Virgin Galactic")
default:
fmt.Println("Random spaceline #", num)
}
package作用域
- era 变量是在main函数外声明的
- 它拥有package作用域
- 如果main package有多个函数,那么era对它们都可见
- 短声明不可用来声明package作用域的变量
package main
import (
"fmt"
"math/rand"
)
var era = "AD"
// main is the function where it all begins.
func main() {
year := 2018
switch month := rand.Intn(12) + 1; month {
case 2:
day := rand.Intn(28) + 1
fmt.Println(era, year, month, day)
case 4, 6, 9, 11:
day := rand.Intn(30) + 1
fmt.Println(era, year, month, day)
default:
day := rand.Intn(31) + 1
fmt.Println(era, year, month, day)
}
}
作业:展示随机日期
package main
import (
"fmt"
"math/rand"
)
var era = "AD"
// main is the function where it all begins.
func main() {
year := 2018
month := rand.Intn(12) + 1
daysInMonth := 31
switch month {
case 2:
daysInMonth = 28
case 4, 6, 9, 11:
daysInMonth = 30
}
day := rand.Intn(daysInMonth) + 1
fmt.Println(era, year, month, day)
}
- 修改这个程序,让其能处理闰年的情况
- 生成随机年份,而不是写死2018
- 二月份:闰年为29天,非闰年为28天
- 使用for循环生成和展示10个日期
package main
import (
"fmt"
"math/rand"
"time"
)
var era = "AD"
// main is the function where it all begins.
func main() {
rand.Seed(time.Now().UnixNano()) // 修改随机数种子
year := rand.Intn(2025-1900) + 1900 // 生成1900年到2025年的随机年份
month := rand.Intn(12) + 1
daysInMonth := 31
if year%4 == 0 {
switch month {
case 2:
daysInMonth = 29
case 4, 6, 9, 11:
daysInMonth = 30
}
} else {
switch month {
case 2:
daysInMonth = 29
case 4, 6, 9, 11:
daysInMonth = 30
}
}
for i := 0; i < 10; i++ {
day := rand.Intn(daysInMonth) + 1
fmt.Println(era, year, month, day)
}
}
优化代码:
package main
import (
"fmt"
"math/rand"
"time"
)
const (
era = "AD"
startYear = 1900
endYear = 2025
)
func main() {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 10; i++ {
year := rand.Intn(endYear - startYear) + startYear
month := rand.Intn(12) + 1
daysInMonth := daysInMonth(year, month)
day := rand.Intn(daysInMonth) + 1
fmt.Println(era, year, month, day)
}
}
func daysInMonth(year, month int) int {
switch month {
case 2:
if isLeapYear(year) {
return 29
} else {
return 28
}
case 4, 6, 9, 11:
return 30
default:
return 31
}
}
func isLeapYear(year int) bool {
if year%4 != 0 {
return false
} else if year%100 != 0 {
return true
} else if year%400 != 0 {
return false
} else {
return true
}
}
五、第一阶段测试题
测试题要求
- 假设我们要去火星,但是有多条航线,每条航线的时间、票的类型(单程、往返)、价格都不同:
- 使用变量、常量、switch、if、for、fmt、math/rand等编写这个程序。
- 一共四列
- Spaceline就是航空公司:
- Space Adventures, SpaceX, or Virgin Galactic
- Days是指到火星单程所需的天数
- 使用2020年7月28日作为出发日期。距离火星的距离为62,100,000公里。速度随机生成16-30 km/s
- Trip Type就是指单程还是往返
- Price的单位是百万美元
- 票价随机生成:3600万-5000万之间(单程)
六、实数
声明浮点型变量
- 下面这三个语句的效果是一样的:
days := 365.2425
var days = 365.2425
var days float64 = 365.2425
- 只要数字含有小数部分,那么它的类型就是float64
声明浮点型变量
- 如果你使用一个整数来初始化某个变量,那么你必须指定它的类型为float64,否则它就是一个整数类型:
var answer float64 = 42
测试
- answer是什么类型?
answer := 42.0
单精度浮点数类型
- Go语言里有两种浮点数类型:
- 默认是float64
- 64位的浮点类型
- 占用8字节内存
- 某些编程语言把这种类型叫做double(双精度)
- float32
- 占用4字节内存
- 精度比float64低
- 有时叫做单精度类型
单精度浮点类型
- 想要使用单精度类型,你必须在声明变量的时候指定该类型
var pi64 = math.Pi
var pi32 float32 = math.Pi
fmt.Println("pi64: ", pi64)
fmt.Println("pi32: ", pi32)
单双精度的使用场景
- 当处理大量数据时,例如3D游戏中的数千个顶点,使用float32牺牲精度来节省内存是很有意义的。
- math包里面的函数操作的都是float64类型,所以应该首选使用float64,除非你有足够的理由不去使用它。
零值
- Go里面每个类型都有一个默认值,它称作零值。
- 当你声明变量却不对它进行初始化的时候,它的值就是零值。
var price float64
fmt.Println("price: ", price)
price1 := 0.0
fmt.Println("price: ", price1)
显示浮点类型
- 使用Print或Println打印浮点类型的时候,默认的行为是尽可能的多显示几位小数
- 如果你不想这样,那么你应该使用Printf函数,结合%f格式化动词来指定显示小数的位数:
third := 1.0 / 3
fmt.Println(third)
fmt.Printf("%v\n", third)
fmt.Printf("%f\n", third)
fmt.Printf("%.3f\n", third)
fmt.Printf("%4.2f\n", third)
运行
Code/go/hello via 🐹 v1.20.3
➜ go run main.go
0.3333333333333333
0.3333333333333333
0.333333
0.333
0.33
Code/go/hello via 🐹 v1.20.3
➜
%f 格式化动词
- 它由两部分组成:
- 宽度:会显示出的最少字符个数(包含小数点和小数)
- 如果宽度大于数字的个数,那么左边会填充空格
- 如果没指定宽度,那么就按实际的位数进行显示
- 精度:小数点后边显示的位数
- 如果想使用0代替空格作为填充:
fmt.Printf("%05.2f\n", third)
浮点类型的精度
third := 1.0 / 3.0
fmt.Println(third + third + third)
piggyBank := 0.1
piggyBank += 0.2
fmt.Println(piggyBank)
可以看到,浮点类型不适合用于金融类计算为了尽量最小化舍入错误,建议先做乘法,再做除法
- 可以看到,浮点类型不适合用于金融类计算
- 为了尽量最小化舍入错误,建议先做乘法,再做除法
celsius := 21.0
fmt.Print((celsius/5.0*9.0)+32, "° F\n")
fmt.Print((9.0/5.0*celsius)+32, "° F\n")
celsius := 21.0
fahrenheit := (celsius * 9.0 / 5.0) + 32.0
fmt.Print(fahrenheit, "° F")
问题
- 如何避免上述的舍入错误?
如何比较浮点类型
piggyBank := 0.1
piggyBank += 0.2
fmt.Println(piggyBank == 0.3)
fmt.Println(math.Abs(piggyBank-0.3) < 0.0001)
作业题
- 编写一个程序:
- 随机地将五分镍币(0.05美元)、一角硬币(0.10美元)和25美分硬币(0.25美元)放入一个空的储蓄罐,直到里面至少有20美元。
- 每次存款后显示存钱罐的余额
- 并以适当的宽度和精度格式化。
七、整数类型
Go语言里的整数类型
- Go提供了10种整数类型:
- 不可以存小数部分
- 范围有限
- 通常根据数值范围来选取整数类型
- 5种整数类型是有符号的
- 能表示正数、0、负数
- 5种整数类型是无符号的
- 能表示正数、0
例子
- 最常用的整数类型是int:
var year int = 2018
- 无符号整数类型是uint:
var month uint = 2
例子 – 使用类型推断
- 下面三个语句是等价的:
year := 2018
var year = 2018
var year int = 2018
8种整数类型
- 整数类型,包括有符号和无符号的,实际上一共是8种类型:
- 他们的取值范围各不相同。
- 与架构无关
int8 uint8 int16 uint16 int32 uint32 int64 uint64
int和uint
-
而int和uint是针对目标设备优化的类型:
- 在树莓派2、比较老的移动设备上,int和uint都是32位的。
- 在比较新的计算机上,int和uint都是64位的。
-
Tip:
- 如果你在较老的32位设备上,使用了超过20亿的整数,并且代码还能运行,那么最好使用int64和uint64来代替int和uint
-
虽然在某些设备上int可以看作是int32,在某些设备上int可以看作是int64,但它们其实是3种不同的类型。
-
int并不是其它类型的别名
小测试
- 哪些整数类型能够包含 –20151021?
打印数据类型
- 在Printf里使用%T就可以打印出数据的类型。
package main
import (
"fmt"
)
// main is the function where it all begins.
func main() {
year := 2018
fmt.Printf("Type %T for %v\n", year, year)
a := "text"
fmt.Printf("Type %T for %[1]v\n", a)
b := 42
fmt.Printf("Type %T for %[1]v\n", b)
c := 3.14
fmt.Printf("Type %T for %[1]v\n", c)
d := true
fmt.Printf("Type %T for %[1]v\n", d)
}
运行
Code/go/hello via 🐹 v1.20.3
➜ go run main.go
Type int for 2018
Type string for text
Type int for 42
Type float64 for 3.14
Type bool for true
Code/go/hello via 🐹 v1.20.3
➜
uint8
- uint8可以用来表示8位的颜色(红绿蓝:0-255)。
var red, green, blue uint8 = 0, 141, 213
- 为什么不使用int?
- uint8的取值范围正好合适,而int则多出来几十亿不合理的数。
- 如果很多颜色数据连续存储,例如未被压缩的图片,那么使用uint8可以节省很多内存。
十六进制表示法
- Go语言里,在数前面加上0x前缀,就可以用十六进制的形式来表示数值。
var red, green, blue uint8 = 0, 141, 213
var red, green, blue uint8 == 0x00, 0x8d, 0xd5
打印十六进制
- 打印十六进制的数,使用%x格式化动词
fmt.Printf("%x %x %x", red, green, blue)
- 也可以指定最小宽度和填充:
var red, green, blue uint8 = 0, 141, 213
fmt.Printf("clolr: #%02x%02x%02x;", red, green, blue)
问题
- 存储一个uint8的变量需要多大内存?
整数环绕
- 所有的整数类型都有一个取值范围,超出这个范围,就会发生“环绕”
var red uint8 = 255
red++
fmt.Println(red) // 0
var number int8 = 127
number++
fmt.Println(number) // -128
打印每个bit
- 使用%b格式化动词
var green uint8 = 3
fmt.Printf("%08b\n", green) // 00000011
green++
fmt.Printf("%08b\n", green) // 00000100
为什么会发生整数环绕?
整数类型的最大、最小值
- math包里,为与架构无关的整数类型,定义了最大、最小值常量:
math.MaxInt16
math.MinInt64
- 而int和uint,可能是32或64位的。
如何避免时间发生环绕
- Unix系统里,时间是以1970年1月1日至今的秒数来表示的。
- 但是在2038年,这个数就会超过20多亿,也就是超过了int32的范围。
- 应使用:int64或uint64
package main
import (
"fmt"
"time"
)
// main is the function where it all begins.
func main() {
future := time.Unix(12622780800, 0)
fmt.Println(future)
}
作业题
- 编写一个新的存钱罐程序,它使用整数来跟踪美分而不是美元的数量。:
- 随机将五分镍币(5美分)、一角硬币(10美分)和25美分硬币(25美分)放入一个空的储蓄罐,直到里面至少有20美元。
- 以美元显示每次存款后存钱罐的余额(例如$1.05)。
八、比较大的数
数太大了怎办?
- 浮点类型可以存储非常大的数值,但是精度不高
- 整型很精确,但是取值范围有限。
- 如果你需要很大的数,而且要求很精确,那么怎么办
- int64可以容纳很大的数,如果还不行,那么:
- uint64可以容纳更大的正数,如果还不行,那么:
- 也可以凑合用浮点类型,但是还有另外一种方法:
- 使用big包。
例子一
package main
import "fmt"
func main() {
const lightSpeed = 299792 // km/s
const secondsPerDay = 86400
var distance int64 = 41.3e12
fmt.Println("Alpha Centauri is", distance, "km away.")
days := distance / lightSpeed / secondsPerDay
fmt.Println("That is", days, "days of travel at light speed.")
}
例子二
package main
import "fmt"
func main() {
var distance uint64 = 24e18 // 报错 cannot use 24e18 (untyped float constant 2.4e+19) as uint64 value in variable declaration (truncated)
var distance float64 = 24e18
fmt.Println(distance)
}
提示
- 如果没有为指数形式的数值指定类型的话,那么Go将会将它视作float64类型
big包
- 对于较大的整数(超过1018):big.Int
- 对于任意精度的浮点类型,big.Float
- 对于分数,big.Rat
big.Int
例子一
package main
import (
"fmt"
"math/big"
)
func main() {
lightSpeed := big.NewInt(299792)
secondsPerDay := big.NewInt(86400)
fmt.Println("lightSpeed is", lightSpeed, "seconds per day", secondsPerDay)
}
例子二
package main
import (
"fmt"
"math/big"
)
func main() {
distance := new(big.Int)
distance.SetString("2400000000000000000000000", 10)
fmt.Println("distance.", distance)
}
例子三
package main
import (
"fmt"
"math/big"
)
func main() {
lightSpeed := big.NewInt(299792)
secondsPerDay := big.NewInt(86400)
distance := new(big.Int)
distance.SetString("240000000000000000000", 10)
fmt.Println("Andromeda Galaxy is", distance, "km away.")
seconds := new(big.Int)
seconds.Div(distance, lightSpeed) // 除法
days := new(big.Int)
days.Div(seconds, secondsPerDay)
fmt.Println("That is", days, "days of travel at light speed.")
}
- 一旦使用了big.Int,那么等式里其它的部分也必须使用big.Int
- NewInt()函数可以把int64转化为big.Int类型
- 如何把24 x 10的18次方转化为big.Int类型?
- 首先new一个big.Int
- 再通过SetString函数把数值的字符串形式,和几进制传递进行即可。
- 缺点:用起来繁琐,且速度慢
测试题
- 用两种方式把86400转化为big.Int类型。
较大数值的常量
- 在Go里面,可以为常量指明类型(这句话会报错):
const distance uint64 = 24000000000000000
- 也可以不指明常量的类型。。。
- 对于变量,Go会使用类型推断;
- 而在Go里面,常量是可以无类型的(untyped),这句话就不会报错:
const distance = 24000000000000000
较大数值的常量
- 常量使用const关键字来声明,程序里的每个字面值都是常量。
- 这意味着:比较大的数值可以直接使用(作为字面值)
- 针对字面值和常量的计算是在编译阶段完成的。
- Go的编译器是用Go编写的,这种无类型的数值字面值就是由big包所支持的。这使你可以操作很大的数(超过18的10¹⁸)
- 只要能够容纳得下,那么常量就可以赋值给变量。
package main
import "fmt"
func main() {
fmt.Println("Andromeda Galaxy is", 2400000000000000/299792/86400, "light days away.")
}
较大数值的常量
- 尽管Go编译器使用big包来处理无类型的数值常量,但是常量和big.Int的值是不能互换的。
小测试
- 常量和字面值的计算是在哪个阶段发生的?
- 函数可以使用无类型的常量作为函数的实际传入的参数吗?
作业题
- 大矮星是已知的距离地球最近的星系,距离我们的太阳236000000000000000公里(尽管有人质疑它是一个星系)。使用常量将此距离转换为光年。
九、多语言文本
声明字符串
- 声明字符串:
peace := "peace"
var peace = "peace"
var peace string = "peace"
- 字符串的零值:
var blank string
字符串字面值/原始字符串字面值
- 字符串字面值可以包含转义字符,例如\n
- 但如果你确实想得到\n而不是换行的话,可以使用`来代替“,这叫做原始字符串字面值。
fmt.Println("peace be upon you\nupon you be peace")
fmt.Println(`strings can span multiple lines with the \n escape sequence`)
fmt.Println(`
peace be upon you
upon you be peace`)
小测试
- 如果你想输出C:\go,那么应该使用字符串字面值还是原始字符串字面值。
- 字符串字面值:string literal
- 原始字符串字面值:raw string literal
字符,code points,runes,bytes
- Unicode联盟为超过100万个字符分配了相应的数值,这个数叫做code point。
- 例如:65代表A,128515 代表 一个表情字符
- 为了表示这样的unicode code point,Go语言提供了rune这个类型,它是int32的一个类型别名。
- 而byte是uint8类型的别名,目的是用于二进制数据。
- byte倒是可以表示由ASCII定义的英语字符,它是Unicode的一个子集(共128个字符)
类型别名
- 类型别名就是同一个类型的另一个名字。
- 所以,rune和int32可以互换使用。
- 也可以自定义类型别名,语法如下:
type byte = uint8
type rune = int32
打印
- 如果想打印字符而不是数值,使用%c格式化动词
- 任何整数类型都可以使用%c打印,但是rune意味着该数值表示了一个字符
package main
import (
"fmt"
)
func main() {
fmt.Println("peace be upon you\nupon you be peace")
fmt.Println(`strings can span multiple lines with the \n escape sequence`)
fmt.Println(`
peace be upon you
upon you be peace`)
var pi rune = 960
var alpha rune = 940
var omega rune = 969
var bang byte = 33
fmt.Printf("%v %v %v %v\n", pi, alpha, omega, bang)
// 将会打印出 code point 的值
fmt.Printf("%c%c%c%c\n", pi, alpha, omega, bang)
// 将会打印出 字符
}
运行
Code/go/started via 🐹 v1.20.3 via 🅒 base
➜ go run main.go
peace be upon you
upon you be peace
strings can span multiple lines with the \n escape sequence
peace be upon you
upon you be peace
960 940 969 33
πάω!
Code/go/started via 🐹 v1.20.3 via 🅒 base
➜
字符
- 字符字面值使用‘’括起来。例如:’A’
- 如果没指定字符类型的话,那么Go会推断它的类型为rune
- 这里的grade仍然包含一个数值,本例中就是65,它是A的code point。
- 字符字面值也可以用byte类型:
var star byte = '*'
var grade rune
grade := 'A'
var grade1 = 'A'
var grade2 rune = 'A'
小测试
- ASCII编码里包含多少个字符?
- byte和rune分别是哪个类型的别名?
- *,é,的code point分别是多少?
string
- 可一个给某个变量赋予不同的string值,但是string本身是不可变的
peace := "shalom"
peace = "salam"
例子二
package main
import (
"fmt"
)
func main() {
message := "shalom"
c := message[5]
fmt.Printf("%c\n", c) // m
message[5] = 'd' // 报错 cannot assign to message[5]
}
小测试
- 写个程序,打印出shalom的每个字符,每个字符独占一行。
Caesar cipher凯撒加密法
- 对于加密信息,一种简单有效的办法就是把每个字母都移动固定长度的位置。
- 例如:a -> d,b -> e
package main
import (
"fmt"
)
func main() {
c := 'a'
c = c + 3
fmt.Printf("%c\n", c)
if c > 'z' {
c = c - 26
}
fmt.Printf("%c\n", c)
}
小测试
- 这个语句的作用是什么?c = c - ‘a’ + ‘A’
- 如果 c 是 ‘g’,那么c最后的结果是什么?
ROT13
- ROT13(旋转13)是凯撒密码在20世纪的变体。
- 它会把字母替换成+13后的对应的字母。
Go的内置函数
- len是 Go 语言的一个内置函数。
message := "uv vagreangvbany fcnpr fgngvba"
fmt.Println(len(message))
- 本例中len返回message所占的byte数。
- Go有很多内置函数,它们不需要import
UTF-8
- Go中的字符串是用UTF-8编码的,UTF-8是Unicode CodePoint的几种编码之一。
- UTF-8是一种有效率的可变长度的编码,每个code point可以是8位、16位或32位的。
- 通过使用可变长度编码,UTF-8使得从ASCII的转换变得简单明了,因为ASCII字符与其UTF-8编码的对应字符是相同的。
- UTF-8是万维网的主要字符编码。它是由Ken Thompson于1992年发明的,他是Go语言的设计者之一。
ROT13
package main
import (
"fmt"
)
func main() {
message := "uv vagreangvbany fcnpr fgngvba"
for i := 0; i < len(message); i++ {
c := message[i]
if c >= 'a' && c <= 'z' {
c = c + 13
if c > 'z' {
c = c - 26
}
}
fmt.Printf("%c", c)
}
}
- 本例中,程序访问的是message这个字符串的每个字节(8位),没有考虑多字节的情况(16、32位)。
- 如何支持西班牙语、俄语、汉语等?
- 把字符解码成rune类型,然后再进行操作。
- 使用utf-8包,它提供可以按rune计算字符串长度的方法。
- DecodeRuneInString 函数会返回第一个字符,以及字符所占的字节数。
- 所以Go里的函数可以返回多个值。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
question := "español?"
fmt.Println(len(question), "bytes")
fmt.Println(utf8.RuneCountInString(question))
c, size := utf8.DecodeRuneInString(question)
fmt.Printf("First rune: %c %v bytes\n", c, size)
}
range
- 使用range关键字,可以遍历各种集合。
package main
import (
"fmt"
)
func main() {
question := "español ?"
for i, c := range question {
fmt.Printf("%v %c\n", i, c)
}
for _, c := range question {
fmt.Printf("%c\n", c)
}
}
作业题
-
L fdph, L vdz, L frqtxhuhg,每个字母向前移动3个位置,能得到什么字符串?
-
把西班牙语 “Hola Estación Espacial Internacional”用ROT13进行加密
- 使用range关键字
- 带重音符号的字母要保留
十、类型间转换
类型不能混着用
- 连接两个字符串,使用+运算符:
countdown := "Launch in T minus " + "10 seconds."
- 如果想连接字符串和数值,是会报错的:
countdown := "Launch in T minus " + 10 + " seconds."
报错
- 整型和浮点类型也不能混着用:
age := 41
marsDays := 687
earthDays := 365.2425
fmt.Println("I am", age*earthDays/marsDays, "years old on Mars.") // 报错
小测试
- Go语言里,“10” – 1这个表达式会报错吗?
数值类型间的转换
- 下例中,如果想把age转化成 浮点类型,需要使用目标类型将其“包裹”起来:
age := 41
marsAge := float64(age)
从浮点类型转为整数类型
- 可以从浮点类型转化为整数类型,小数点后边的部分会被截断,而不是舍入:
earthDays := 365.2425
fmt.Println(int(earthDays))
- 无符号和有符号整数类型之间也需要转换
- 不同大小的整数类型之间也需要转换
类型转换时需要谨慎
1996年,无人驾驶的阿丽亚娜5号火箭偏离飞行路线,解体,发射后仅40秒爆炸。报告的原因是从float64到int16的类型转换错误,其值超过了int16所能容纳的最大值32767。未处理的故障导致飞行控制系统没有方位数据,导致其偏离航向,解体,最终自毁。
类型转换时需谨慎
- “环绕行为”:
var bh float64 = 32768
var h = int16(bh)
fmt.Println(h) // -32768
- 可以通过math包提供的max、min常量,来判断是否超过最大最小值
var bh float64 = 32768
if bh < math.MinInt16 || bh > math.MaxInt16 {
// handle out of range value
}
小测试
- 下列代码输出是什么?
v := 42
if v >= 0 && v <= math.MaxUint8 {
v8 := uint8(v)
fmt.Println("converted:", v8)
}
字符串转换
- 想把rune、byte转化为string,语法是一样的:‘
var pi rune = 960
var alpha rune = 940
var omega rune = 969
var bang byte = 33
fmt.Println(string(pi), string(alpha), string(omega), string(bang))
字符串转换
- 想把数值转化为string,它的值必须能转化为code point。
- strconv包的Itoa函数就可以做这件事:
- Itoa是Integer to ASCII的意思。
- Unicode是ASCII的超集,它们前128个code points是一样的(数字、英文字母、常用标点)。
countdown := 10
str := "Launch in T minus " + strconv.Itoa(countdown) + " seconds."
fmt.Println(str)
- 另外一种把数值转化为string的方式是使用Sprintf函数,和Printf略类似,但是会返回一个string:
countdown := 9
str := fmt.Sprintf("Launch in T minus %v seconds.", countdown)
fmt.Println(str)
-
strconv包里面还有个Atoi(ASCII to Integer)函数。
-
由于字符串里面可能包含任意字符,或者要转换的数字字符串太大,所以Atoi函数可能会发生错误:
- 如果err的值为nil,那么就代表没发生错误。
countdown, err := strconv.Atoi("10")
if err != nil {
// oh no, something went wrong
fmt.Println(err.Error())
}
fmt.Println(countdown)
小测试
- 说出两个函数,它们可以把整数类型转化为string
提示
- Go是静态类型语言,一旦某个变量被声明,那么它的类型就无法再改变了。
布尔类型的转换
- Print家族的函数,会把bool类型的值打印成true/false的文本
launch := false
launchText := fmt.Sprintf("%v", launch)
fmt.Println("Ready for launch:", launchText)
var yesNo string
if launch {
yesNo = "yes"
} else {
yesNo = "no"
}
fmt.Println("Ready for launch:", yesNo)
- 注意:如果你想使用string(false), int(false);bool(1), bool(“yes”)等类似的方式进行转换,那么Go编译器会报错
- 某些语言里,经常把1和0当作true和false,但是在Go里面不行。
小测试
- 写一个程序,把布尔类型转化为整数类型,true -> 1,false -> 0
作业题
- 写一个程序,把字符串转化为布尔类型:
- “true”, “yes”, “1”是true
- “false”, “no”, “0”是false
- 针对其它值,显示错误信息
十一、第二部分测试题
Vigenère cipher维吉尼亚加密算法
-
维吉尼亚加密算法是凯撒加密算法的变种。
-
凯撒算法:
- 之前讲的凯撒算法例子是把字符串中的每个字母都向后移动了3位。
- 假设A=0,B=1 … Z=25。那么,我们就用D=3来表示向后移动3位。
-
维吉尼亚加密算法使用的是密钥(keyword)
-
假设密钥是:GOLANG
第1题
- 编写一个程序来解密下面第一行的文字:
- 它的密钥就是GOLANG
- 所有字母都是大写的英文字母。
第2题
- 编写一个程序:
- 使用维吉尼亚加密算法来加密信息。
- 密钥为GOLANG
- 可能用到的函数:
- strings.Replace
- strings.ToUpper
十二、函数
为什么需要函数
- 做一件事通常需要很多步骤,每个步骤可以分解为独立的函数,这些函数以后可能会复用到。
函数声明
-
Go在标准库文档中列出了标准库每个包中声明的函数。
-
例如:
- rand包的Intn:func Intn(n int) int
- 它的用法:num := rand.Intn(10)
-
使用func关键字声明函数
-
在Go里,大写字母开头的函数、变量或其它标识符都会被导出,对其它包可用。
-
小写字母开头的就不行。
-
形式参数:parameter
-
实际参数:argument
函数声明 – 多个参数
-
函数的参数可以是多个:
- func Unix(sec int64, nsec int64) Time
- 调用:future := time.Unix(12622780800, 0)
-
函数声明时,如果多个形参类型相同,那么该类型只写一次即可:
- func Unix(sec int64, nsec int64) Time
- func Unix(sec, nsec int64) Time
-
这种简化是可选的。
函数声明 – 返回多个值
- Go的函数可以返回多个值:
- countdown, err := strconv.Atoi(“10”)
- 该函数的声明如下:
- func Atoi(s string) (i int, err error)
- 函数的多个返回值需要用括号括起来,每个返回值名字在前,类型在后。声明函数时可以把名字去掉,只保留类型:
- func Atoi(s string) (int, error)
函数声明 – 可变参数函数
- Println是一个特殊的函数,它可以接收一个、二个甚至多个参数,参数类型还可以不同:
- fmt.Println(“Hello, playground”)
- fmt.Println(186, “seconds”)
- Println的声明是这样的:
- func Println(a …interface{}) (n int, err error)
- … 表示函数的参数的数量是可变的。
- 参数a的类型为interface{},是一个空接口。
- … 和空接口组合到一起就可以接受任意数量、类型的参数了
小测试
- 调用函数时使用的是实参还是形参?
- 诸如Contains和contains这两个函数会有什么区别?
- 函数声明中的 … 表示什么意思?
编写函数
- 例子
package main
import "fmt"
// kelvinToCelsius converts °K to °C
func kelvinToCelsius(k float64) float64 {
k -= 273.15
return k
}
func main() {
kelvin := 294.0
celsius := kelvinToCelsius(kelvin)
fmt.Print(kelvin, "° K is ", celsius, "° C")
}
- 函数按值传递参数
- 同一个包中声明的函数在调用彼此时不需要加上包名。
小测试
- 将代码拆分为函数有什么好处?
作业题
package main
import "fmt"
// kelvinToCelsius converts °K to °C
func kelvinToCelsius(k float64) float64 {
k -= 273.15
return k
}
func main() {
kelvin := 294.0
celsius := kelvinToCelsius(kelvin)
fmt.Print(kelvin, "° K is ", celsius, "° C")
}
- 修改这段代码:
- 复用kelvinToCelsius函数,将233K转化为 ℃。
- 编写celsiusToFahrenheit函数,它可将摄氏度转化为华氏度。
- 公式为(c x 9.0/5.0) + 32.0
- 编写kelvinToFahrenheit函数,看看它能否将0K转化为约-459.67℉
十三、方法
声明新类型
- 关键字type可以用来声明新类型:
- type celsius float64
- var temperature celsius = 20
- 虽然Celsius是一种全新的类型,但是由于它和float64具有相同的行为和表示,所以赋值操作能顺利执行。
- 例如加法等运算,也可以像float64那样使用。
- (例子)
package main
import "fmt"
func main() {
type celsius float64
const degrees = 20
var temperature celsius = degrees
temperature += 10
fmt.Println("temperature: ", temperature)
}
- 为什么要声明新类型:极大的提高代码可读性和可靠性
- 不同的类型是无法混用的
- (例子)
type celsius float64
const degrees = 20
var temperature celsius = degrees
temperature += 10
var warmUp float64 = 10
temperature += warmUp // 报错
通过方法添加行为
- 在C#、Java里,方法属于类
- 在Go里,它提供了方法,但是没提供类和对象
- Go比其他语言的方法要灵活
- 可以将方法与同包中声明的任何类型相关联,但不可以是int、float64等预声明的类型进行关联。
type celsius float64
type kelvin float64
func kelvinToCelsius(k kelvin) celsius {
return celsius(k - 273.15)
}
func (k kelvin) celsius() celsius { // celsius 是 kelvin 类型的方法
return celsius(k - 273.15)
}
- 上例中,celsius方法虽然没有参数。但它前面却有一个类型参数的接收者。
- 每个方法可以有多个参数,但只能有一个接收者。
- 在方法体中,接收者的行为和其它参数一样。
方法调用
- 变量.方法()
package main
import "fmt"
func main() {
var k kelvin = 294.0
var c celsius
c = kelvinToCelsius(k)
c = k.celsius()
fmt.Println("c: ", c)
}
type celsius float64
type kelvin float64
func kelvinToCelsius(k kelvin) celsius {
return celsius(k - 273.15)
}
func (k kelvin) celsius() celsius { // celsius 是 kelvin 类型的方法
return celsius(k - 273.15)
}
小测试
- 标识出这个方法声明中的接收者:func (f fahrenheit) celsius() celsius
作业题
- 编写一个程序:
- 它包含三种类型:celsius、fahrenheit、kelvin
- 3种温度类型之间转换的方法
十四、一等函数
一等函数
- 在Go里,函数是头等的,它可以用在整数、字符串或其它类型能用的地方:
- 将函数赋给变量
- 将函数作为参数传递给函数
- 将函数作为函数的返回类型
将函数赋给变量
package main
import (
"fmt"
"math/rand"
)
type kelvin float64
func fakeSensor() kelvin {
return kelvin(rand.Intn(151) + 150)
}
func realSensor() kelvin {
return 0
}
func main() {
sensor := fakeSensor
fmt.Println(sensor())
sensor = realSensor
fmt.Println(sensor())
}
- 变量sensor就是一个函数,而不是函数执行的结果
- 无论sensor的值是fakeSensor还是realSensor,都可以通过sensor()来调用
- sensor这个变量的类型是函数,该函数没有参数,返回一个kelvin类型的值。
- 换一种声明形式的话:
- var sensor func() kelvin
小测试
- 如何区分“将函数本身赋给变量”和“将函数执行的结果赋给变量”这两种行为?
- 如果存在一个返回celsius温度的groundSensor函数,我们可以把它赋给上例中的sensor变量吗?
将函数传递给其它函数
- (例子)
package main
import (
"fmt"
"math/rand"
"time"
)
type kelvin float64
func measureTemperature(samples int, sensor func() kelvin) {
for i := 0; i < samples; i++ {
k := sensor()
fmt.Printf("%v° K\n", k)
time.Sleep(time.Second)
}
}
func fakeSensor() kelvin {
return kelvin(rand.Intn(151) + 150)
}
func main() {
measureTemperature(3, fakeSensor)
}
小测试
- 拥有向其它函数传递函数的能力有什么好处?
声明函数类型
- 为函数声明类型有助于精简和明确调用者的代码。
- 例如:type sensor func() kelvin
- 所以:func measureTemperature(samples int, s func() kelvin)
- 可以精简为:func measureTemperature(samples int, s sensor)
小测试
- 请使用函数类型重写一下函数的签名:
- func drawTable(rows int, getRow func(row int) (string, string))
闭包和匿名函数
- 匿名函数就是没有名字的函数,在Go里也称作函数字面值。
- 因为函数字面值需要保留外部作用域的变量引用,所以函数字面值都是闭包的。
- (例子)
例子一:
package main
import "fmt"
var f = func() {
fmt.Println("Dress up for the masquerade.")
}
func main() {
f()
}
例子二
package main
import "fmt"
func main() {
f := func(message string) {
fmt.Println(message)
}
f("Go to the party.")
}
例子三
package main
import "fmt"
func main() {
func() {
fmt.Println("Functions anonymous")
}()
}
例子四
package main
import "fmt"
type kelvin float64
// sensor function type
type sensor func() kelvin
func realSensor() kelvin {
return 0
}
func calibrate(s sensor, offset kelvin) sensor {
return func() kelvin {
return s() + offset
}
}
func main() {
sensor := calibrate(realSensor, 5)
fmt.Println(sensor())
}
- 闭包(closure)就是由于匿名函数封闭并包围作用域中的变量而得名的。
例子五
package main
import "fmt"
type kelvin float64
func main() {
var k kelvin = 294.0
sensor := func() kelvin {
return k
}
fmt.Println(sensor())
k++
fmt.Println(sensor())
}
小测试
- 匿名函数在Go中的另一个名字是什么?
- 闭包提供了哪些普通函数不具备的特性?
作业题
package main
import "fmt"
type kelvin float64
// sensor function type
type sensor func() kelvin
func realSensor() kelvin {
return 0
}
func calibrate(s sensor, offset kelvin) sensor {
return func() kelvin {
return s() + offset
}
}
func main() {
sensor := calibrate(realSensor, 5)
fmt.Println(sensor())
}
- 修改这段程序:
- 声明一个变量,并将其用作calibrate函数的offset实参,而不是使用字面值数字5。在此之后,即使修改变量,调用sensor()的结果也仍然为5。这是因为offset形参接受的是实参的副本而不是引用,也就是所谓的按值传递。
- 使用calibrate函数和今天讲的fakeSensor函数以创建新的sensor函数,然后多次调用这个新的sensor函数,看看它是否每次都会调用fakeSensor函数并产生随机的读数。
十五、第三部分习题
温度表
- 编写一个温度转换表格程序:
- 画两个表格:
- 第一个表格有两列,第一列是摄氏度,第二列是华氏度。
- 从-40℃打印到100℃,间隔为5℃,并将摄氏度转化为华氏度。
- 第二个表格就是第一个表格的两列互换一下,从华氏度转化为摄氏度。
- 第一个表格有两列,第一列是摄氏度,第二列是华氏度。
- 负责画线和填充值的代码都应该是可复用的。画表格和计算温度应该用不同的函数分别来实现。
- 实现一个drawTable函数,它接受一个一等函数作为参数,调用该函数就可以绘制每一行的温度。传入不同的函数就可以产生不同的输出数据。
十六、数组
数组
- 数组是一种固定长度且有序的元素集合。
var planets [8]string
访问数组元素
- 数组中的每个元素都可以通过[ ]和一个从0开始的索引进行访问。
package main
import "fmt"
func main() {
var planets [8]string
planets[0] = "Mercury"
planets[1] = "Venus"
planets[2] = "Earth"
earth := planets[2]
fmt.Println(earth)
}
- 数组的长度可由内置函数len来确定。
package main
import "fmt"
func main() {
var planets [8]string
planets[0] = "Mercury"
planets[1] = "Venus"
planets[2] = "Earth"
earth := planets[2]
fmt.Println(earth)
fmt.Println(len(planets))
fmt.Println(planets[3] == "")
}
- 在声明数组时,未被赋值元素的值是对应类型的零值。
小测试
- 如何才能访问planets数组的第一个元素?
- 对新创建的整数数组来说,其元素的默认值是什么?
数组越界
- Go编译器在检测到对越界元素的访问时会报错
package main
func main() {
var plantes [8]string
planets[8] = "Pluto" // 报错
pluto := planets[8] // 报错
}
- 如果Go编译器在编译时未能发现越界错误,那么程序在运行时会出现panic
- Panic会导致程序崩溃
package main
import "fmt"
func main() {
var planets [8]string
i := 8
planets[i] = "Pluto"
pluto := planets[i]
fmt.Println(pluto)
}
小测试
- 访问planets[11]会导致编译时错误还是运行时 panic?
使用复合字面值初始化数组
- 复合字面值(composite literal)是一种给复合类型初始化的紧凑语法。
- Go的复合字面值语法允许我们只用一步就完成数组声明和数组初始化两步操作:
dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
- 可以在复合字面值里使用 … 作为数组的长度,这样Go编译器会为你算出数组的元素数量
package main
import "fmt"
func main() {
planets := [...]string{
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
}
fmt.Println(planets)
}
- 无论哪种方式,数组的长度都是固定的。
遍历数组
- for循环
package main
import "fmt"
func main() {
dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
for i := 0; i < len(dwarfs); i++ {
dwarf := dwarfs[i]
fmt.Println(i, dwarf)
}
}
- 使用range
package main
import "fmt"
func main() {
dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
for i, dwarf := range dwarfs {
fmt.Println(i, dwarf)
}
}
小测试
- 使用range关键字遍历数组可避免哪些错误?
- 什么情况下,使用for循环比range更合适?
数组的复制
- 无论数组赋值给新的变量还是将它传递给函数,都会产生一个完整的数组副本。
package main
import "fmt"
func main() {
planets := [...]string{
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
}
planetsMarkII := planets
planets[2] = "whoops"
fmt.Println(planets)
fmt.Println(planetsMarkII)
}
- 数组也是一种值,函数通过值传递来接受参数。所以数组作为函数的参数就非常低效。
package main
import "fmt"
// terraform accomplishes nothing
func terraform(planets [8]string) {
for i := range planets {
planets[i] = "New " + planets[i]
}
}
func main() {
planets := [...]string{
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
}
terraform(planets)
fmt.Println(planets)
}
- 数组的长度也是数组类型的一部分。
- 尝试将长度不符的数组作为参数传递,将会报错。
- 函数一般使用 slice 而不是数组作为参数。
数组的数组
package main
import "fmt"
func main() {
var board [8][8]string
board[0][0] = "r"
board[0][7] = "r"
for column := range board[1] {
board[1][column] = "p"
}
fmt.Print(board)
}
小测试
- 如果我们要设计一个数独游戏程序,那么该如何声明9 x 9的整数网格?
作业题
package main
import "fmt"
func main() {
var board [8][8]string
board[0][0] = "r"
board[0][7] = "r"
for column := range board[1] {
board[1][column] = "p"
}
fmt.Print(board)
}
- 扩展上面例子的程序,使用字符kqrbnp表示上方的黑棋,字符KQRBNP表示下方的白棋,然后在棋子的起始位置是打印出所有的棋子。
- 编写一个能够美观的打印出整个棋盘的函数
- 使用
[8][8]rune
数组而不是字符串来表示棋
十七、Slice(切片)
Slice指向数组的窗口
- 假设planets是一个数组,那么planets[0:4] 就是一个切片,它切分出了数组里前4个元素。
- 切分数组不会导致数组被修改,它只是创建了指向数组的一个窗口或视图,这种视图就是slice类型。
切分数组
- Slice使用的是半开区间
- 例如planets[0:4],包含索引0、1、2、3对应的元素,不包括索引4对应的元素。
package main
import "fmt"
func main() {
planets := [...]string{
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
}
terrestrial := planets[0:4]
gasGiants := planets[4:6]
iceGiants := planets[6:8]
fmt.Println(terrestrizl, gasGiants, iceGiants)
}
小测试
- 切分数组会产生什么?
- 使用planets[4:6]切分数组时,结果包含多少个元素?
Slice的默认索引
- 忽略掉slice的起始索引,表示从数组的起始位置进行切分;
- 忽略掉slice的结束索引,相当于使用数组的长度作为结束索引。
- 注意:slice的索引不能是负数。
package main
import "fmt"
func main() {
planets := [...]string{
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
}
terrestrial := planets[:4]
gasGiants := planets[4:6]
iceGiants := planets[6:]
allPlenets := planets[:]
fmt.Println(terrestrizl, gasGiants, iceGiants, allPlanets)
}
- 如果同时省略掉起始和结束索引,那就是包含数组所有元素的一个slice。
- 切分数组的语法也可以用于切分字符串
package main
import "fmt"
func main() {
neptune := "Neptune"
tune := neptune[3:]
fmt.Println(tune)
neptune = "Poseidon"
fmt.Println(tune) // 将原来切片的值赋给一个新的值,并不会改变原来切片的值
}
- 切分字符串时,索引代表的是字节数而非 rune 的数。
package main
import "fmt"
func main() {
question := "¡Qué pasa!"
fmt.Println(question[:6])
}
Slice的复合字面值
- Go里面很多函数都倾向于使用slice而不是数组作为参数。
- 想要获得与底层数组相同元素的slice,那么可以使用 [:] 进行切分
- 切分数组并不是创建slice唯一的方法,可以直接声明slice:
- 例如[]string
package main
import "fmt"
func main() {
dwarfArray := [...]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
dwarfSlice := dwarfArray[:]
dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
fmt.Println(dwarfSlice, dwarfs)
}
小测试
- 使用格式化动词%T比较dwarfArray和dwarfs的类型。
切片的威力
package main
import "fmt"
// hyperspace removes the space surrounding worlds
func hyperspace(worlds []string) {
for i := range worlds {
worlds[i] = strings.TrimSpace(worlds[i])
}
}
func main() {
planets := []string{" Venus ", "Earth ", " Mars"}
hyperspace(planets)
fmt.Println(strings.Join(planets, ""))
}
小测试
- 访问Go标准库网站,查阅TrimSpace和Join函数的相关文档,说出他们的作用
带有方法的切片
- 在Go里,可以将slice或数组作为底层类型,然后绑定其它方法。
package main
import (
"fmt"
"sort"
)
type StringSlice []string
func (p StringSlice) Sort() {
}
func main() {
planets := []string{
"Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune",
}
sort.StringSlice(planets).Sort()
fmt.Println(planets)
}
小测试
- 执行代码sort.StringSlice(planets)会发生什么?
作业题
- 编写一个程序:
- 它通过给字符串slice中所有的行星加上“New ”前缀来完成行星的地球化处理,然后使用该程序对Mars、Uranus、Neptune实行地球化
- 必须使用planets类型,并为之实现相应的terraform方法。
十八、更大的slice
append函数
- append函数也是内置函数,它可以将元素添加到slice里面。
package main
import "fmt"
func main() {
dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
dwarfs = append(dwarfs, "Orcus")
dwarfs = append(dwarfs, "Salacia", "Quaoar", "Sedna")
fmt.Println(dwarfs)
}
小测试
- 执行上例的代码后,dwarfs切片总共包含多少个矮行星?用什么函数能够获知这一点?
长度和容量(length & capacity)
- Slice中元素的个数决定了slice的长度。
- 如果slice的底层数组比slice还大,那么就说该slice还有容量可供增长。
package main
import "fmt"
// dump slice length, capacity, and contents
func dump(label string, slice []string) {
fmt.Printf("%v: length %v, capacity %v %v\n", label, len(slice), cap(slice), slice)
}
func main() {
dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
dump("dwarfs", dwarfs)
dump("dwarfs[1:2]", dwarfs[1:2])
}
小测试
- 为什么dwarfs[1:2]这个slice的容量是4?
详解append
package main
import "fmt"
// dump slice length, capacity, and contents
func dump(label string, slice []string) {
fmt.Printf("%v: length %v, capacity %v %v\n", label, len(slice), cap(slice), slice)
}
func main() {
planets := []string{
"Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune",
}
terrestrial := planets[0:4:4]
worlds := append(terrestrial, "Ceres")
dump("planets", planets) // 8 8
dump("terrestrial", terrestrial) // 4 4
dump("worlds", worlds) // 5 8
}
小测试
- 对于上面例子中的dwarfs3这个slice,如果我们修改它的一个元素,那么dwarfs2和dwarfs1这两个切片会发生变化吗?dwarfs3[1] = “Pluto!”
三个索引的切分操作
- Go 1.2中引入了能够限制新建切片容量的三索引切分操作。
package main
import "fmt"
// dump slice length, capacity, and contents
func dump(label string, slice []string) {
fmt.Printf("%v: length %v, capacity %v %v\n", label, len(slice), cap(slice), slice)
}
func main() {
dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
dump("dwarfs", dwarfs)
dump("dwarfs[1:2]", dwarfs[1:2])
}
小测试
- 什么时候应该使用三索引的切片操作?
使用make函数对slice进行预分配
- 当slice的容量不足以执行append操作时,Go必须创建新数组并复制旧数组中的内容。
- 但通过内置的make函数,可以对slice进行预分配策略。
- 尽量避免额外的内存分配和数组复制操作。
package main
import "fmt"
// dump slice length, capacity, and contents
func dump(label string, slice []string) {
fmt.Printf("%v: length %v, capacity %v %v\n", label, len(slice), cap(slice), slice)
}
func main() {
dwarfs := make([]string, 0, 10)
dump("dwarfs", dwarfs)
dwarfs = append(dwarfs, "Ceres", "Pluto", "Haumea", "Makemake", "Eris")
dump("dwarfs", dwarfs)
}
小测试
- 使用make函数创建slice有什么好处?
声明可变参数的函数
- 声明Printf、append这样的可变参数函数,需要在函数的最后一个参数前面加上 … 符号。
package main
import "fmt"
func terraform(prefix string, worlds ...string) []string {
newWorlds := make([]string, len(worlds))
for i := range worlds {
newWorlds[i] = prefix + " " + worlds[i]
}
return newWorlds
}
func main() {
twoWorlds := terraform("New", "Venus", "Mars")
fmt.Println(twoWorlds)
planets := []string{"Venus", "Mars", "Jupiter"}
newPlanets := terraform("New", planets...)
fmt.Println(newPlanets)
}
小测试
- 关于 … 符号,到目前为止,我们见过的3种用法分别是什么?
作业
- 编写一个程序:
- 通过循环,持续的将元素追加到slice里
- 在slice容量发生变化的时候打印出它的容量
- 请判断append 函数在底层数组的空间被填满之后,是否会将数组的容量增加一倍?
十九、Map
map
- map是Go提供的另外一种集合:
- 它可以将key映射到value。
- 可快速通过key找到对应的value
- 它的key几乎可以是任何类型
声明map
- 声明map,必须指定key和value的类型:
map [string] int
package main
import "fmt"
func main() {
temperature := map[string]int{
"Earth": 15,
"Mars": -65,
}
temp := temperature["Earth"]
fmt.Printf("On average the Earth is %v° C.\n", temp)
temperature["Earth"] = 16
temperature["Venus"] = 464
fmt.Println(temperature)
moon := temperature["Moon"]
fmt.Println(moon) // 返回对应的零值
}
逗号与ok写法
package main
import "fmt"
func main() {
temperature := map[string]int{
"Earth": 15,
"Mars": -65,
}
temp := temperature["Earth"]
fmt.Printf("On average the Earth is %v° C.\n", temp)
temperature["Earth"] = 16
temperature["Venus"] = 464
fmt.Println(temperature)
moon := temperature["Moon"]
fmt.Println(moon) // 返回对应的零值
if moon, ok := temperature["Moon"]; ok {
fmt.Printf("On average the moon is %v° C.\n", moon)
} else {
fmt.Println("Where is the moon?")
}
}
小测试
- 如果你要声明一个key为64位浮点数,value为整数的map,那么应该使用什么类型?
- 如果你修改上例的代码,将key “Moon”的值设为0,那么使用 逗号与ok语法将产生什么结果?
map不会被复制
- 数组、int、float64等类型在赋值给新变量或传递至函数/方法的时候会创建相应的副本
- 但map不会
package main
import "fmt"
func main() {
planets := map[string]string {
"Earth": "Sector ZZ9",
"Mars": "Sector ZZ9",
}
planetsMarkII := planets
planets["Earth"] = "whoops"
fmt.Println(planets)
fmt.Println(planetsMarkII)
delete(planets, "Earth")
fmt.Println(planetsMarkII)
fmt.Println(planets)
}
小测试
- 在上例中,为什么对planets修改会反映在planetMarkII中?
- 内置的delete函数有什么用?
使用make函数对 map 进行预分配
- 除非你使用复合字面值来初始化map,否则必须使用内置的make函数来为map分配空间。
- 创建map时,make函数可接受一个或两个参数
- 第二个参数用于为指定数量的key预先分配空间
- 使用make函数创建的map的初始长度为0
package main
import "fmt"
func main() {
temperature := make(map[float64]int, 8)
fmt.Println(temperature)
fmt.Println(len(temperature))
}
使用map作计数器
package main
import "fmt"
func main() {
temperatures := []float64{
-28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}
frequency := make(map[float64]int)
for _, t := range temperatures {
frequency[t]++
}
for t, num := range frequency {
fmt.Printf("%+.2f occurs %d times\n", t, num)
}
}
小测试
- 使用range关键字遍历map的时候,它会为两个变量提供什么数据?
使用map和slice实现数据分组
package main
import (
"fmt"
"math"
)
func main() {
temperatures := []float64{
-28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}
groups := make(map[float64][]float64)
for _, t := range temperatures {
g := math.Trunc(t/10) * 10
groups[g] = append(groups[g], t)
}
for g, temperatures := range groups {
fmt.Printf("%v: %v\n", g, temperatures)
}
}
小测试
- 声明
var groups map[string][]int
中的key和value都是什么类型?
将map用作set
- Set这种集合与数组类似,但元素不会重复
- Go语言里没有提供set集合
package main
import "fmt"
func main() {
var temperatures = []float64{
-28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}
set := make(map[float64]bool)
for _, t := range temperatures {
set[t] = true
}
if set[-28.0] {
fmt.Println("set member")
}
fmt.Println(set)
unique := make([]float64, 0, len(set))
for t := range set {
unique = append(unique, t)
}
sort.Float64s(unique)
fmt.Println(unique)
}
小测试
- 对于上面例子中的set集合,我们如何判断32.0是否是它的成员?
作业题
- 编写一个函数:
- 它可以统计文本字符串中不同单词的出现频率,并返回一个词频map。
- 该函数需要将文本转换为小写字母并移除包含的标点符号
- strings包中的Fields、ToLower、Trim函数应该对此有所帮助
- 使用该函数统计下文中各单词出现的频率,打印出现不止一次的单词和词频:As far as eye could reach he saw nothing but the stems of the great plants about him receding inthe violet shade, and far overhead the multiple transparency of huge leaves filtering the sunshineto the solemn splendour of twilight in which he walked. Whenever he felt able he ran again; theground continued soft and springy, covered with the same resilient weed which was the firstthing his hands had touched in Malacandra. Once or twice a small red creature scuttled across hispath, but otherwise there seemed to be no life stirring in the wood; nothing to fear—except thefact of wandering unprovisioned and alone in a forest of unknown vegetation thousands ormillions of miles beyond the reach or knowledge of man.
二十、第四部分习题
- 开天辟地
- 观察世界
- 激活细胞
- 适者生存
- 存活还是死亡
- 统计邻近细胞
- 游戏逻辑
- 平行世界
二十一、结构
结构类型(struct)
- 为了将分散的零件组成一个完整的结构体,Go提供了struct类型。
- struct允许你将不同类型的东西组合在一起。
- 声明结构
package main
func distance(lat1, long1, lat2, long2 float64) float64 {
return 0.0
}
func main() {
var curiosity struct {
lat float64
long float64
}
curiosity.lat = -4.5895
curiosity.long = 137.4417
fmt.Println(curiosity.lat, curiosity.long)
fmt.Println(curiosity)
}
小测试
- 与独立的变量相比,使用struct的优势是什么?
- 上例中,如果curiosity有一个海拔(altitude)字段,那么你如何给它赋值为-4400?
通过类型复用结构体
package main
func distance(loc1, loc2 location) dis {
return dis{0.0, 0.0}
}
type location struct {
lat float64
long float64
}
// type dis location
type dis struct {
lat float64
long float64
}
func main() {
//var curiosity struct {
// lat float64
// long float64
//}
//type location struct {
//lat float64
// long float64
// }
var spirit location
spirit.lat = -14.5684
spirit.long = 175.472636
var opportunity location
opportunity.lat = -1.9462
opportunity.long = 354.4734
fmt.Println(spirit, opportunity)
}
小测试
- 修改上面例子的代码,改用location类型表示好奇号火星探测器的位置
通过复合字面值初始化struct
- 两种形式:
- 通过成对的字段和值进行初始化
- 按字段定义的顺序进行初始化
package main
import "fmt"
func main() {
type location struct {
lat, long float64
}
opportunity := location{lat: -1.9462, long: 354.4734}
fmt.Println(opportunity)
insight := location{lat: 4.5, long: 135.9}
fmt.Println(insight)
spirit := location{-14.5684, 175.472636}
fmt.Println(spirit)
}
- 打印struct
pakcage main
import "fmt"
func main() {
type location struct {
lat, long float64
}
curiosity := location{-4.5859, 137.4417}
fmt.Printf("%v\n", curiosity)
fmt.Printf("%+v\n", curiosity)
}
- %v,打印出{-4.5895 137.4417}
- %+v,打印出{lat:-4.5895 long:137.4417}
小测试
- 与只给定值的初始化方式相比,成对给定字段和值的初始化方式有哪些优势?
struct的复制
pakcage main
import "fmt"
func main() {
type location struct {
lat, long float64
}
bradbury := location{-4.5859, 137.4417}
curiosity := bradbury
curiosity.long += 0.0106
fmt.Println(bradbury, curiosity)
}
小测试
- 如果将curiosity变量传递给函数,并在函数中对lat和long字段进行修改,那么调用者是否会看到这些变化?
由结构体组成的slice
package main
import "fmt"
func main() {
type location struct {
name string
lat float64
long float64
}
lats := []float64{-4.5859, -14.5684, -1.9462}
longs := []float64{137.4417, 175.472636, 354.4734}
locations := []location{
{name: "Bradbury Landing", lat: -4.5859, long: 137.4417},
{name: "Columbia Memorial Station", lat: -14.5684, long: 175.472636},
{name: "Challenger Memorial Station", lat: -1.9462, long: 354.4734},
}
fmt.Println(lats, longs, locations)
}
小测试
- 使用多个相互关联的slice有什么坏处?
将struct编码为JSON
-
JSON(JavaScript Object Notation,JavaScript对象表示法)
-
常用于Web API
-
json包的Marshal函数可以将struct中的数据转化为JSON格式。
package main
import (
"encoding/json"
"fmt"
"os"
)
func main() {
type location struct {
Lat, Long, float64
}
curiosity := location{-4.5895, 137.4417}
bytes, err := json.Marshal(curiosity)
exitOnError(err)
fmt.Println(string(bytes))
}
// exitOnError prints any errors and exits.
func exitOnError(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
- Marshal函数只会对struct中被导出的字段进行编码。
小测试
- JSON代表什么意思?
使用struct标签来自定义JSON
-
Go语言中的json包要求struct中的字段必须以大写字母开头,类似CamelCase驼峰型命名规范。
-
但有时候我们需要snake_case蛇形命名规范,那么该怎么办?
-
可以为字段注明标签,使得json包在进行编码的时候能够按照标签里的样式修改字段名。
package main
import (
"encoding/json"
"fmt"
"os"
)
func main() {
type location struct {
Lat float64 `json: "latitude"`
Long, float64 `json: "longitude"`
}
curiosity := location{-4.5895, 137.4417}
bytes, err := json.Marshal(curiosity)
exitOnError(err)
fmt.Println(string(bytes))
}
// exitOnError prints any errors and exits.
func exitOnError(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
小测试
- 为什么在编码JSON数据的时候,Lat和Long字段必须以大写字母开头?
作业题
- 编写一个程序:
- 能够以JSON格式打印出 上面例子中3台 探测器的着陆点。
- 被打印的JSON数据必须包含每个着陆点的名称,并使用上面例子中展示的struct标签特性。
- 请使用json包中的MarshalIndent函数让打印输出变得更加美观和易读。
二十二、Go语言没有class
Go语言里没有class
- Go和其它经典语言不同,它没有class,没有对象,也没有继承。
- 但是Go提供了struct和方法。
将方法关联到struct
- 方法可以被关联到你声明的类型上
package main
import "fmt"
// coordinate in degrees, minutes, seconds in a N/S/E/W hemisphere.
type coordinate struct {
d, m, s float64
h rune
}
// decimal converts a d/m/s coordinate to decimal degrees.
func (c coordinate) decimal() float64 {
sign := 1.0
switch c.h {
case 'S', 'W', 's', 'w':
sign = -1
}
return sign * (c.d + c.m/60 + c.s/3600)
}
func main() {
// Bradbury Landing: 4°35'22.2" S, 137°26‘30.1“ E
lat := coordinate{4, 35, 22.2, 'S'}
long := coordinate{137, 26, 30.12, 'E'}
fmt.Println(lat.decimal(), long.decimal())
}
小测试
- 上例中,decimal方法的接收者是谁?
构造函数
- 可以使用struct复合字面值来初始化你所要的数据。
- 但如果struct初始化的时候还要做很多事情,那就可以考虑写一个构造用的函数。
package main
import "fmt"
// coordinate in degrees, minutes, seconds in a N/S/E/W hemisphere.
type coordinate struct {
d, m, s float64
h rune
}
// decimal converts a d/m/s coordinate to decimal degrees.
func (c coordinate) decimal() float64 {
sign := 1.0
switch c.h {
case 'S', 'W', 's', 'w':
sign = -1
}
return sign * (c.d + c.m/60 + c.s/3600)
}
type location struct {
lat, long float64
}
// newLocation from latitude, longitude d/m/s coordinates.
func newLocation(lat, long, coordinate) location {
return location{lat.decimal(), long.decimal()}
}
func main() {
// Bradbury Landing: 4°35'22.2" S, 137°26‘30.1“ E
lat := coordinate{4, 35, 22.2, 'S'}
long := coordinate{137, 26, 30.12, 'E'}
fmt.Println(lat.decimal(), long.decimal())
// curiosity := location{lat.decimal(), long.decimal()}
curiosity := newLocation(lat, long)
fmt.Println(curiosity)
}
- Go语言没有专用的构造函数,但以new或者New开头的函数,通常是用来构造数据的。例如newPerson(),NewPerson()
New函数
- 有一些用于构造的函数的名称就是New(例如errors包里面的New函数)。
- 这是因为函数调用时使用包名.函数名的形式。
- 如果该函数叫NewError,那么调用的时候就是errors.NewError(),这就不如errors.New()简洁
小测试
- 如果你想构建一个Universe类型的变量,那么你如何为该函数命名?
class的替代方案
- Go语言没有class,但使用struct并配备几个方法也可以达到同样的效果。
package main
import (
"fmt"
"math"
)
type location struct {
lat, long float64
}
type world struct {
radius float64
}
// distance calculation using the Spherical Law of Cosines.
func (w world) distance(p1, p2 location) float64 {
s1, c1 := math.Sincos(rad(p1.lat))
s2, c2 := math.Sincos(rad(p2.lat))
clong := math.Cos(rad(p1.long - p2.long))
return w.radius * math.Acos(s1*s2+c1*c2*clong)
}
// rad converts degrees to radians.
func rad(deg float64) float64 {
return deg * math.Pi / 180
}
func main() {
var mars = world{radius: 3389.5}
spirit := location{-14.5684, 175.472636}
opportunity := location{-1.9462, 354.4734}
dist := mars.distance(spirit, opportunity)
fmt.Printf("%.2f km\n", dist)
}
小测试
- 与不采用面向对象的方式相比,在world类型上声明一个distance方法的好处是什么?
作业题
- 使用例子中的代码,编写一个程序。并为下表中每个 位置都声明一个location,以十进制度数打印出每个位置。
- 使用例子中的distance方法,编写一个程序,来判定上题表中每对着陆点之间的距离。并回答:
- 哪两个着陆点之间最近?
- 哪两个着陆点之间最远?
- 计算伦敦到巴黎之间的距离(51°30’N 0°08’W),(48°51’N 2°21’E),地球半径为6371公里。
- 计算你的城市到北京距离
- 计算火星上Mount Sharp (5°4’ 48”S, 137°51’E)到Olympus Mons (18°39’N,226°12’E)之间的距离。火星的半径是3389.5公里。
二十三、组合与转发 Composition and forwarding
组合
- 在面向对象的世界中,对象由更小的对象组合而成。
- 术语:对象组合或组合
- Go通过结构体实现组合(composition)。
- Go提供了“嵌入”(embedding)特性,它可以实现方法的转发(forwarding)
- 组合是一种更简单、灵活的方式。
组合结构体
例子一
package main
type report struct {
sol int
high, low float64
lat, hong float64
}
func main() {
}
例子二
package main
type report struct {
sol int
temperature temperature
location location
}
type temperature struct {
hign, low celsius
}
type location struct {
lat, long float64
}
type celsius float64
func (t temperature) average() celsius {
return (t.high + t.low) / 2
}
func (r report) average() celsius {
return r.temperature.average()
}
func main() {
bradbury := location{-4.5895, 137.4417}
t := temperature{high: -1.0, low: -78.0}
fmt.Println(t.average())
report := report {
sol: 15,
temperature: t,
location: bradbury
}
fmt.Println(report.temperature.average())
fmt.Println(report.average())
fmt.Printf("%+v\n", report)
fmt.Printf("a balmy %v° C\n", report.temperature.high)
}
小测试
- 比较例子1和2的代码,你更喜欢哪一种?原因是什么?
转发方法
- Go可以通过struct嵌入 来实现方法的转发。
- 在struct中只给定字段类型,不给定字段名即可。
package main
import "fmt"
//type report struct {
// sol int
// temperature temperature
// location location
//}
type report struct {
sol int
temperature
location
}
type temperature struct {
high, low celsius
}
type location struct {
lat, long float64
}
type celsius float64
func (t temperature) average() celsius {
return (t.high + t.low) / 2
}
func main() {
bradbury := location{-4.5895, 137.4417}
t := temperature{high: -1.0, low: -78.0}
fmt.Println(t.average())
report := report {
sol: 15,
temperature: t,
location: bradbury
}
fmt.Println(report.average())
fmt.Println(report.high)
fmt.Printf("%+v\n", report)
fmt.Printf("a balmy %v° C\n", report.temperature.high)
}
- 在struct中,可以转发任意类型。
package main
import "fmt"
//type report struct {
// sol int
// temperature temperature
// location location
//}
type sol int
type report struct {
sol
temperature
location
}
type temperature struct {
high, low celsius
}
type location struct {
lat, long float64
}
type celsius float64
func (t temperature) average() celsius {
return (t.high + t.low) / 2
}
func (s sol) days(s2 sol) int {
days := int(s2 - s)
if days < 0 {
days = -days
}
return days
}
func main() {
report := report {
sol: 15,
}
fmt.Println(report.sol.days(1446))
fmt.Println(report.days(1446))
}
小测试
- 结构体可以嵌入什么类型?
- 访问report.lat字段是否合法?如果合法,那么上面例子中它指向哪个字段?
命名冲突
package main
import "fmt"
//type report struct {
// sol int
// temperature temperature
// location location
//}
type sol int
type report struct {
sol
temperature
location
}
type temperature struct {
high, low celsius
}
type location struct {
lat, long float64
}
type celsius float64
func (t temperature) average() celsius {
return (t.high + t.low) / 2
}
func (s sol) days(s2 sol) int {
days := int(s2 - s)
if days < 0 {
days = -days
}
return days
}
func (l location) days(l2 location) int {
// To-do: complicated distance calculation
return 5
}
func (r report) days(s2 sol) int {
return r.sol.days(s2)
}
func main() {
report := report {
sol: 15,
}
fmt.Println(report.sol.days(1446))
//fmt.Println(report.days(1446)) // 报错
fmt.Println(report.days(1446))
}
继承 还是 组合?
- Favor object composition over class inheritance.
- 优先使用对象组合而不是类的继承。
- Use of classical inheritance is always optional; every problem thatit solves can be solved another way.
- 对传统的继承不是必需的;所有使用继承解决的问题都可以通过其它方法解决。
小测试
- 如果多个嵌入的类型都实现了同名的方法,那么Go编译器会报错吗?
作业题
实验:gps.go
二十四、接口
接口
- 接口关注于类型可以做什么,而不是存储了什么。
- 接口通过列举类型必须满足的一组方法来进行声明。
- 在Go语言中,不需要显式声明接口。
例子一
package main
var t interface {
talk() string
}
func main() {
}
例子二
package main
import (
"fmt"
"strings"
)
var t interface {
talk() string
}
type martian struct {}
func (m martian) talk() string {
return "nack nack"
}
type laser int
func (l laser) talk() string {
return strings.Repeat("pew ", int(l))
}
func main() {
t = martian{}
fmt.Println(t.talk())
t = laser(3)
fmt.Println(t.talk())
}
接口类型
- 为了复用,通常会把接口声明为类型。
- 按约定,接口名称通常以er结尾。
例子一
package main
import (
"fmt"
)
type talker interface {
talk() string
}
type martian struct {}
func (m martian) talk() string {
return "nack nack"
}
type laser int
func (l laser) talk() string {
return strings.Repeat("pew ", int(l))
}
func shout(t talker) {
louder := strings.ToUpper(t.talk())
fmt.Println(louder)
}
func main() {
shout(martian{})
shout(laser(2))
}
例子二
package main
import (
"fmt"
)
type talker interface {
talk() string
}
type martian struct {}
func (m martian) talk() string {
return "nack nack"
}
type laser int
func (l laser) talk() string {
return strings.Repeat("pew ", int(l))
}
func shout(t talker) {
louder := strings.ToUpper(t.talk())
fmt.Println(louder)
}
type starship struct {
laser
}
func main() {
s := starship{laser(3)}
fmt.Println(s.talk())
shout(s)
}
- 接口可以与struct嵌入 特性一同使用。
- 同时使用组合和接口将构成非常强大的设计工具。
小测试
- 修改例子中laser类型的talk方法,阻止火星的激光枪发射,拯救人类免遭入侵。
- 扩展例子的代码,声明一个带有talk方法的rover类型,并使方法返回“whir whir”,最后再使用shout函数处理这个新类型。
探索接口
- Go语言的接口都是隐式满足的。
例子一
package main
import (
"fmt"
"time"
)
// stardate returns a fictional measure of time for a given date.
func stardate(t time.Time) float64 {
doy := float64(t.YearDay())
h := float64(t.Hour()) / 24.0
return 1000 + doy + h
}
func main() {
day := time.Date(2012, 8, 6, 5, 17, 0, 0, time.UTC)
fmt.Printf("%.1f Curiosity has landed\n", stardate(day))
}
例子二
package main
import (
"fmt"
"time"
)
type stardater interface {
YearDay() int
Hour() int
}
type sol int
func (s sol) YearDay() int {
return int(s % 668)
}
func (s sol) Hour() int {
return 0
}
// stardate returns a fictional measure of time for a given date.
func stardate(t stardater) float64 {
doy := float64(t.YearDay())
h := float64(t.Hour()) / 24.0
return 1000 + doy + h
}
func main() {
day := time.Date(2012, 8, 6, 5, 17, 0, 0, time.UTC)
fmt.Printf("%.1f Curiosity has landed\n", stardate(day))
s := sol(1422)
fmt.Printf("%.1f Happy birthday\n", stardate(s))
}
小测试
- 隐式满足接口有什么好处?
满足接口
- Go标准库导出了很多只有单个方法的接口。
- Go通过简单的、通常只有单个方法的接口……来鼓励组合而不是继承,这些接口在各个组件之间形成了简明易懂的界限。–– Rob Pike
- 例如fmt包声明的Stringer接口:
type Stringer interface {String() string}
package main
import "fmt"
// location with a latitude, longitude in decimal degrees.
type location struct {
lat, long float64
}
// String formats a location with latitude, longitude.
func (l location) String() string {
return fmt.Sprintf("%v, %v", l.lat, l.long)
}
func main() {
curiosity := location{-4.5859, 137.4417}
fmt.Println(curiosity)
}
- 标准库中常用接口还包括:io.Reader,io.Writer,json.Marshaler…
小测试
- 请为coordinate类型编写String方法,并使用该方法以更为可读的方式打印出坐标数据。
type coordinate struct {
d, m, s float64
h rune
}
- 请使你的程序打印出:Elysium Planitia is at 4°30’0.0“ N, 135°54’0.0“ E
作业题
- 基于刚才小测试的答案,扩展并编写一个用JSON格式输出坐标的程序。这个程序的JSON输出应该分别用十进制(DD)和度/分/秒(DMS)两种格式提供坐标:
- 通过满足定制JSON数据的json.Marshaler接口,你应该无需修改坐标结构就能够达到上述目的。在编写MarshalJSON方法的时候可以考虑使用json.Marshal函数。
- 可以使用22章介绍过的decimal方法
二十五、第五部分习题
单元实验:火星上的动物避难所
二十六、指针
什么是指针
- 指针是指向另一个变量地址的变量。
- Go语言的指针同时也强调安全性,不会出现迷途指针(danglingpointers)
&和*
符号
- 变量会将它们的值存储在计算机的RAM里,存储位置就是该变量的内存地址。
- & 表示地址操作符,通过&可以获得变量的内存地址。
package main
import "fmt"
func main() {
answer := 42
fmt.Println(&answer)
}
- &操作符无法获得字符串/数值/布尔字面值的地址。
- &42,&“hello”这些都会导致编译器报错
- *操作符与&的作用相反,它用来解引用,提供内存地址指向的值。
package main
import "fmt"
func main() {
answer := 42
fmt.Println(&answer)
address := &answer
fmt.Println(*address)
}
注意
- C语言中的内存地址可以通过例如address++这样的指针运算进行操作,但是在Go里面不允许这种不安全操作。
小测试
- 在例子中执行
fmt.Println(*&answer)
会打印出什么结果? - 乘法运算和解引用都需要用到星号
*
,Go编译器将如何区分呢?
指针类型
- 指针存储的是内存地址。
package main
import "fmt"
func main() {
answer := 42
address := &answer // *int
fmt.Printf("address is a %T\n", address)
}
- 指针类型和其它普通类型一样,出现在所有需要用到类型的地方,如变量声明、函数形参、返回值类型、结构体字段等。
package main
import "fmt"
func main() {
canada := "Canada"
var home *string
fmt.Printf("home is a %T\n", home)
home = &canada
fmt.Printf("canada is a %v\n", *canada)
}
- 将
*
放在类型前面表示声明指针类型 - 将
*
放在变量前面表示解引用操作
小测试
- 你会使用什么代码来声明一个指向整数的名为address的变量?
- 你是如何区分 例子中声明指针变量和解引用指针这两个操作的?
指针就是用来指向的
package main
import "fmt"
func main() {
var administrator *string
scolese := "Christopher J. Scolese"
administrator = &scolese
fmt.Println(*administrator)
bolden := "Charies F. Bolden"
administrator = &bolden
fmt.Println(*administrator)
bolden = "Charles Frank Bolden Jr."
fmt.Printf(*administrator)
*administrator = "Maj. Gen. Charles Frank Bolden Jr."
fmt.Println(bolden)
major := administrator
*major = "Major General Charles Frank Bolden Jr."
fmt.Println(bolden)
fmt.Println(administrator == major)
lightfoot := "Robert M. Lightfoot Jr."
administrator = &lightfoot
fmt.Println(administrator == major)
charles := *major
*major = "Charles Bolden"
fmt.Println(charles)
fmt.Println(bolden)
charlse = "Charles Bolden"
fmt.Println(charles == bolden)
fmt.Println(&charles == &bolden)
}
- 两个指针变量持有相同的内存地址,那么它们就是相等。
小测试
- 例子中使用指针的好处是什么?
- 请说明语句major := administrator和Charles := *major的作用。
指向结构的指针
- 与字符串和数值不一样,复合字面量的前面可以放置&。
package main
import "fmt"
func main() {
type person struct {
name, superpower string
age int
}
timmy := &person {
name: "Timothy",
age: 10,
}
(*timmy).superpower = "flying"
timmy.superpower = "flying"
fmt.Printf("%+v\n", timmy)
}
- 访问字段时,对结构体进行解引用并不是必须的。
小测试
- 以下哪些是&操作符的合法使用?
- A.放置在字符串字面值的前面,例如&“Tim”
- B.放置在整数字面值的前面,例如&10
- C.放置在复合字面值的前面,例如&person{name: “Tim”}
- D.以上全部都是
- 语句timmy.superpower和(*timmy).superpower有何区别?
指向数组的指针
- 和结构体一样,可以把&放在数组的复合字面值前面来创建指向数组的指针。
package main
import "fmt"
func main() {
superpowers := &[3]string{"flight", "invisibility", "super strength"}
fmt.Println(superpowers[0])
fmt.Println(superpowers[1:2])
}
- 数组在执行索引或切片操作时会自动解引用。没有必要写
(*superpower)[0]
这种形式。 - 与C语言不一样,Go里面数组和指针是两种完全独立的类型。
- Slice和map的复合字面值前面也可以放置&操作符,但是Go并没有为它们提供自动解引用的功能。
小测试
- 当superpower是一个指针或者数组时,有什么语句可以和*superpower具有同样的执行效果呢?
实现修改
- Go语言的函数和方法都是按值传递参数的,这意味着函数总是操作于被传递参数的副本。
- 当指针被传递到函数时,函数将接收传入的内存地址的副本。之后函数可以通过解引用内存地址来修改指针指向的值。
例子一个
package main
type person struct {
name, superpower string
age int
}
func birthday(p *person) {
p.age++
}
func main() {
}
例子二
package main
type person struct {
name, superpower string
age int
}
func birthday(p *person) {
p.age++
}
func main() {
rebecca := person{
name: "Rebecca",
superpower: "imagination",
age: 14,
}
birthday(&rebecca)
fmt.Printf("%+v\n", rebecca)
}
小测试
- 对例子26.6来说,下列哪行代码会返回Timothy 11?
- A.birthday(&timmy
- )B.birthday(timmy)
- C.birthday(*timmy)
- 对于例子26.9和26.10来说,如果birthday(p person)函数不使用指针,那么Rebecca的岁数(age)将是多少?
指针接收者
- 方法的接收者和方法的参数在处理指针方面是很相似的。
package main
type person struct {
name string
age int
}
func (p *person) birthday() {
p.age++
}
func main() {
terry := &person{
name: "Terry",
age: 15,
}
terry.birthday()
fmt.Printf("%+v\n", terry)
nathan := person{
name: "Nathan",
age: 17,
}
nathan.birthday()
fmt.Printf("%+v\n", nathan)
}
- Go语言在变量通过点标记法进行调用的时候,自动使用 & 取得变量的内存地址。
- 所以不用写(&nathan).birthday()这种形式也可以正常运行。
package main
type person struct {
name string
age int
}
func (p *person) birthday() {
p.age++
}
func main() {
terry := &person{
name: "Terry",
age: 15,
}
terry.birthday()
fmt.Printf("%+v\n", terry)
nathan := person{
name: "Nathan",
age: 17,
}
nathan.birthday()
(&nathan).birthday()
fmt.Printf("%+v\n", nathan)
const layout = "Mon, Jan 2, 2006"
day := time.Now()
tomorrow := day.Add(24 * time.Hour)
fmt.Println(day.Format(layout))
fmt.Println(tomorrow.Format(layout))
}
注意
- 使用指针作为接收者的策略应该始终如一:
- 如果一种类型的某些方法需要用到指针作为接收者,就应该为这种类型的所有方法都是用指针作为接收者。
小测试
- 怎样才能判断time.Time类型所有的方法是否都没有使用指针作为接收者。
内部指针
- Go语言提供了 内部指针 这种特性。
- 它用于确定结构体中指定字段的内存地址。
package main
type stats struct {
level int
endurance, health int
}
func levelUp(s *stats) {
s.level ++
s.endurance = 42 + (14 * s.level)
s.health = 5 * s.endurance
}
type character struct {
name string
stats stats
}
func main() {
player := character(name: "Matthias")
levelUp(&player.stats)
fmt.Printf("%+v\n", player.stats)
}
- &操作符不仅可以获得结构体的内存地址,还可以获得结构体中指定字段的内存地址。
小测试
- 什么是内部指针?
修改数组
- 函数通过指针对数组的元素进行修改。
package main
import "fmt"
func reset(board *[8][8]rune) {
board[0][0] = 'r'
// ...
}
func main() {
var board [8][8]rune
reset(&board)
fmt.Printf("%c", board[0][0])
}
小测试
- 什么情况下应该使用指向数组的指针?
隐式的指针
- Go语言里一些内置的集合类型就在暗中使用指针。
- map在被赋值或者被作为参数传递的时候不会被复制。
- map就是一种隐式指针。
- 这种写法就是多此一举:
func demolish(planets *map[string]string)
- map的键和值都可以是指针类型
- 需要将指针指向map的情况并不多见
小测试
- map是指针吗?
slice指向数组
-
之前说过slice是指向数组的窗口,实际上slice在指向数组元素的时候也使用了指针。
-
每个slice内部都会被表示为一个包含3个元素的结构,它们分别指向:
- 数组的指针
- slice的容量
- slice的长度
-
当slice被直接传递至函数或方法时,slice的内部指针就可以对底层数据进行修改。
-
指向slice的显式指针的唯一作用就是修改slice本身:slice的长度、容量以及起始偏移量。
package main
import "fmt"
func reclassify(planets *[]string) {
*planets = (*planets)[0:8]
}
func main() {
planets := []string{
"Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune",
"Pluto",
}
reclassify(&planets)
fmt.Println(planets)
}
小测试
- 如果函数和方法想要修改它们接收到的数据,那么它们应该使用指向哪两种数据类型的指针?
指针和接口
package main
import (
"fmt"
"strings"
)
type talker interface {
talk() string
}
func shout(t talker) {
louder := strings.ToUper(t.talk())
fmt.Println(louder)
}
type martian struct {}
func (m martian) talk() string {
return "nack nakc"
}
fun main() {
shout(martian{})
shout(&martian{})
}
- 本例中,无论martian还是指向martian的指针,都可以满足talker接口。
- 如果方法使用的是指针接收者,那么情况会有所不同。
package main
import (
"fmt"
"strings"
)
type talker interface {
talk() string
}
func shout(t talker) {
louder := strings.ToUper(t.talk())
fmt.Println(louder)
}
type martian struct {}
func (m martian) talk() string {
return "nack nakc"
}
type laser int
func (l *laser) talk() string {
return strings.Repeat("pew ", int(*l))
}
fun main() {
pew := laser(2)
shout(&pew)
shout(pew) // 报错
}
小测试
- 指针在什么情况下才能满足接口?
明智的使用指针
- 应合理使用指针,不要过度使用指针。
作业题
- 编写一个可以让海龟上下左右移动的程序:
- 程序中的海龟需要存储一个位置(x,y)
- 正数坐标表示向下或向右
- 通过使用方法对相应的变量实施自增和自减来实现移动
- 请使用main函数测试这些方法并打印出海龟的最终位置
二十七、nil
nil
- Nil是一个名词,表示“无”或者“零”。
- 在Go里,nil是一个零值。
- 如果一个指针没有明确的指向,那么它的值就是nil
- 除了指针,nil还是slice、map和接口的零值。
- Go语言的nil,比以往语言中的null更为友好,并且用的没那么频繁,但是仍需谨慎使用。
nil会导致panic
- 如果指针没有明确的指向,那么程序将无法对其实施的解引用。
- 尝试解引用一个nil指针将导致程序崩溃。
package main
import "fmt"
func main() {
var nowhere *int
if nowhere != nil {
fmt.Println(*nowhere)
}
fmt.Println(nowhere)
fmt.Println(*nowhere) // panic
}
小测试
- 类型*string的零值是什么?
保护你的方法
- 避免nil引发panic
package main
import "fmt"
type person struct {
age int
}
func (p *person) birthday() {
p.age++ // 报错
}
func main() {
var nobody *person
fmt.Println(nobody)
nobody.birthday() // panic
}
- 因为值为nil的接收者和值为nil的参数在行为上并没有区别,所以Go语言即使在接收者为nil的情况下,也会继续调用方法。
package main
import "fmt"
type person struct {
age int
}
func (p *person) birthday() {
if p == nil {
return
}
p.age++
}
func main() {
var nobody *person
fmt.Println(nobody)
nobody.birthday()
}
小测试
- 如果p为nil,那么访问字段p.age会产生什么结果?
nil函数值
- 当变量被声明为函数类型时,它的默认值是nil。
package main
import "fmt"
func main() {
var fn func(a, b int) int
fmt.Println(fn == nil)
}
- 检查函数值是否为nil,并在有需要时提供默认行为。
package main
import (
"fmt"
"sort"
)
func sortStrings(s []string, less func(i, j int) bool) {
if less == nil {
less = func(i, j int) bool {return s[i] < s[j]}
}
sort.Slice(s, less)
}
func main() {
food := []string{"onion", "carrot", "celery"}
sortStrings(food, nil)
fmt.Println(food)
}
小测试
- 请为例子编写一个单行函数,它可以按照字符串从短到长的顺序对food进行排序
nil slice
- 如果slice在声明之后没有使用复合字面值或内置的make函数进行初始化,那么它的值就是nil。
- 幸运的是,range、len、append等内置函数都可以正常处理值为nil的slice。
package main
import "fmt"
func main() {
var soup []string
fmt.Println(soup == nil)
for _, ingredient := range soup {
fmt.Println(ingredient)
}
fmt.Println(len(soup))
soup = append(soup, "onion", "carrot", "celery")
fmt.Println(soup)
}
- 虽然空slice和值为nil的slice并不相等,但它们通常可以替换使用。
package main
import "fmt"
func mirepoix(ingredients []string) []string {
return append(ingredients, "onion", "carrot", "celery")
}
func main() {
soup := mirepoix(nil)
fmt.Println(soup)
}
小测试
- 我们可以安全的对nil slice执行哪些操作?
nil map
- 和slice一样,如果map在声明后没有使用复合字面值或内置的make函数进行初始化,那么它的值将会是默认的nil
package main
import "fmt"
func main() {
var soup map[string]int
fmt.Println(soup == nil)
measurement, ok := soup["onion"] // 读值为 nil 的读取不会报错
if ok {
fmt.Println(measurement)
}
for ingredient, measurement := range soup { // 不会报错 {} 中的代码不会走
fmt.Println(ingredient, measurement)
}
}
小测试
- 对值为nil的map执行什么操作会引发panic?
nil接口
- 声明为接口类型的变量在未被赋值时,它的零值是nil。
- 对于一个未被赋值的接口变量来说,它的接口类型和值都是nil,并且变量本身也等于nil。
package main
import "fmt"
func main() {
var v interface{}
fmt.Printf("%T %v %v\n", v, v, v == nil)
}
- 当接口类型的变量被赋值后,接口就会在内部指向该变量的类型和值。
package main
import "fmt"
func main() {
var v interface{}
fmt.Printf("%T %v %v\n", v, v, v == nil)
var p *int
v = p
fmt.Printf("%T %v %v\n", v, v, v == nil)
}
- 在Go中,接口类型的变量只有在类型和值都为nil时才等于nil。
- 即使接口变量的值仍为nil,但只要它的类型不是nil,那么该变量就不等于nil。
- 检验接口变量的内部表示
package main
import "fmt"
func main() {
var v interface{}
fmt.Printf("%T %v %v\n", v, v, v == nil)
var p *int
v = p
fmt.Printf("%T %v %v\n", v, v, v == nil)
fmt.Printf("%#v\n", v)
}
小测试
- 在执行声明var s fmt.Stringer之后,变量s的值是什么?
nil之外的另一个选择
package main
import "fmt"
type number struct {
value int
valid bool
}
func newNumber(v int) number {
return number{value: v, valid: true}
}
func (n number) String() string {
if !n.valid {
return "not set"
}
return fmt.Sprintf("%d", n.value)
}
func main() {
n := newNumber(42)
fmt.Println(n)
e := number{}
fmt.Println(e)
}
小测试
- 例子中采用的策略有何优势?
作业题
- 亚瑟被一位骑士挡住了去路。正如leftHand *item变量的值为nil所示,这位英雄手上正空无一物。
- 请实现一个拥有pickup(i item)和give(tocharacter)等方法的character结构,然后使用你在本节学到的知识编写一个脚本,使得亚瑟可以拿起一件物品并将其交给骑士,与此同时为每个动作打印出适当的描述。
二十八、错误
处理错误
- Go语言允许函数和方法同时返回多个值
- 按照惯例,函数在返回错误时,最后边的返回值应用来表示错误。
- 调用函数后,应立即检查是否发生错误。
- 如果没有错误发生,那么返回的错误值为nil。
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
files, err := ioutil.ReadDir(".")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, file := range files {
fmt.Println(file.Name())
}
}
注意
- 当错误发生时,函数返回的其它值通常就不再可信。
小测试
- 修改例子,让它读取一个虚构的文件夹,如“unicorns”,看看会打印出什么错误信息?
- 如果我们使用ReadDir来读取诸如“/etc/hosts”这样的文件而不是文件夹的时候,程序会打印出什么错误信息?
优雅的错误处理
- 减少错误处理代码的一种策略是:将程序中不会出错的部分和包含潜在错误隐患的部分隔离开来。
- 对于不得不返回错误的代码,应尽力简化相应的错误处理代码。
- Errors are values.
- Don’t just check errors, handle them gracefully.
- Don’t panic.
- Make the zero value useful.
- The bigger the interface, the weaker the abstraction.
- interface{} says nothing.
- Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.
- Documentation is for users.
- A little copying is better than a little dependency.
- Clear is better than clever.
- Concurrency is not parallelism.
- Don’t communicate by sharing memory, share memory bycommunicating.
- Channels orchestrate; mutexes serialize.
文件写入
- 写入文件的时候可能出错:
- 路径不正确
- 权限不够
- 磁盘空间不足
- …
- 文件写入完毕后,必须被关闭,确保文件被刷到磁盘上,避免资源的泄露。
package main
import (
"fmt"
"os"
)
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
_, err = fmt.Fprintln(f, "Errors are values.")
if err != nil {
f.Close()
return err
}
_, err = fmt.Fprintln(f, "Don't just check errors, handle them gracefully.")
f.Close()
return err
}
func main() {
err := proverbs("proverbs.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
内置类型error
- 内置类型error用来表示错误。
小测试
- 为什么函数应返回错误而不是退出程序?
defer关键字
- 使用defer关键字,Go可以确保所有deferred的动作可以在函数返回前执行。
package main
import (
"fmt"
"os"
)
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
defer f.Close()
_, err = fmt.Fprintln(f, "Errors are values.")
if err != nil {
return err
}
_, err = fmt.Fprintln(f, "Don't just check errors, handle them gracefully.")
return err
}
func main() {
err := proverbs("proverbs.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
- 可以defer任意的函数和方法。
- defer并不是专门做错误处理的。
- defer可以消除必须时刻惦记执行资源释放的负担
小测试
- defer的动作什么时候会被执行?
有创意的错误处理
package main
import (
"fmt"
"io"
)
type safeWriter struct {
w io.Writer
err error
}
func (sw *safeWriter) writeln(s string) {
if sw.err != nil {
return
}
_, sw.err = fmt.Fprintln(sw.w, s)
}
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
defer f.Close()
sw := safeWriter{w: f}
sw.writeln("Errors are values.")
sw.writeln("Don’t just check errors, handle them gracefully.")
sw.writeln("Don’t panic.")
sw.writeln("Make the zero value useful.")
return sw.err
}
func main() {
}
小测试
- 在例子中,如果在将 “Clear is better than clever.”写入到文件的过程中出错了,那么接下来会发生哪些事件?
New error
- errors包里有一个构造用New函数,它接收string作为参数用来表示错误信息。该函数返回error类型。
package main
import (
"errors"
"fmt"
"os"
)
const rows, columns = 9, 9
// Grid is a Sudoku grid
type Grid [rows][columns]int8
// Set ...
func (g *Grid) Set(row, column int, digit int8) error {
if !inBounds(row, column) {
return errors.New("out of bounds")
}
g[row][column] = digit
return nil
}
func inBounds(row, column int) bool {
if row < 0 || row >= rows {
return false
}
if column < 0 || column >= columns {
return false
}
return true
}
func main() {
var g Grid
err := g.Set(10, 0, 5)
if err != nil {
fmt.Printf("An error occurred: %v.\n", err)
os.Exit(1)
}
}
提示
- 错误信息应具有信息性
- 可以把错误信息当作用户界面的一部分,无论对最终用户还是开发者。
小测试
- 在函数里,首先编写对输入参数的防卫性代码有什么好处?
按需返回错误
- 按照惯例,包含错误信息的变量名应以Err开头。
package main
import (
"errors"
"fmt"
"os"
)
const rows, columns = 9, 9
// Grid is a Sudoku grid
type Grid [rows][columns]int8
var (
// ErrBounds ...
ErrBounds = errors.New("out of bounds")
// ErrDigit ...
ErrDigit = errors.New("invalid digit")
)
// Set ...
func (g *Grid) Set(row, column int, digit int8) error {
if !inBounds(row, column) {
return ErrBounds
}
g[row][column] = digit
return nil
}
func inBounds(row, column int) bool {
if row < 0 || row >= rows {
return false
}
if column < 0 || column >= columns {
return false
}
return true
}
func main() {
var g Grid
err := g.Set(10, 0, 5)
if err != nil {
switch err { // 指针 内存地址
case ErrBounds, ErrDigit:
fmt.Println("Les erreurs de parametres hors limites.")
default:
fmt.Println(err)
}
os.Exit(1)
}
}
- errors.New这个构造函数是使用指针实现的,所以上例中的switch语句比较的是内存地址,而不是错误包含的文字信息。
小测试
- 编写一个validDigit函数,用它来保证Set函数只接受1到9之间的数值。
自定义错误类型
- error类型是一个内置的接口:任何类型只要实现了返回string的Error()方法就满足了该接口。
- 可以创建新的错误类型。
package main
import (
"errors"
"fmt"
"os"
)
const rows, columns = 9, 9
// Grid is a Sudoku grid
type Grid [rows][columns]int8
var (
// ErrBounds ...
ErrBounds = errors.New("out of bounds")
// ErrDigit ...
ErrDigit = errors.New("invalid digit")
)
// SudokuError ...
type SudokuError []error
// Error returns one or more errors separated by commas.
func (se SudokuError) Error() string {
var s []string
for _, err := range se {
s = append(s, err.Error())
}
return strings.Join(s, ", ")
}
// Set ...
func (g *Grid) Set(row, column int, digit int8) error {
var errs SudokuError
if !inBounds(row, column) {
errs = append(errs, ErrBounds)
}
if !validDight(digit) {
errs = append(errs, ErrDigit)
}
if len(errs) > 0 {
return errs
}
g[row][column] = digit
return nil
}
func inBounds(row, column int) bool {
if row < 0 || row >= rows {
return false
}
if column < 0 || column >= columns {
return false
}
return true
}
func main() {
var g Grid
err := g.Set(12, 0, 15)
if err != nil {
switch err { // 指针 内存地址
case ErrBounds, ErrDigit:
fmt.Println("Les erreurs de parametres hors limites.")
default:
fmt.Println(err)
}
os.Exit(1)
}
}
- 按照惯例,自定义错误类型的名字应以Error结尾。
- 有时候名字就是Error,例如url.Error
小测试
- 上例中,如果操作成功时,Set方法返回空的error slice,那么会发生什么?
类型断言
- 上例中,我们可以使用类型断言来访问每一种错误。
- 使用类型断言,你可以把接口类型转化成底层的具体类型。
- 例如:err.(SudokuError)
package main
import (
"errors"
"fmt"
"os"
)
const rows, columns = 9, 9
// Grid is a Sudoku grid
type Grid [rows][columns]int8
var (
// ErrBounds ...
ErrBounds = errors.New("out of bounds")
// ErrDigit ...
ErrDigit = errors.New("invalid digit")
)
// SudokuError ...
type SudokuError []error
// Error returns one or more errors separated by commas.
func (se SudokuError) Error() string {
var s []string
for _, err := range se {
s = append(s, err.Error())
}
return strings.Join(s, ", ")
}
// Set ...
func (g *Grid) Set(row, column int, digit int8) error {
var errs SudokuError
if !inBounds(row, column) {
errs = append(errs, ErrBounds)
}
if !validDight(digit) {
errs = append(errs, ErrDigit)
}
if len(errs) > 0 {
return errs
}
g[row][column] = digit
return nil
}
func inBounds(row, column int) bool {
if row < 0 || row >= rows {
return false
}
if column < 0 || column >= columns {
return false
}
return true
}
func main() {
var g Grid
err := g.Set(12, 0, 15)
if err != nil {
if errs, ok := err.(SudokuError); ok {
fmt.Printf("%d error(s) occurred:\n", len(errs))
for _, e := range errs {
fmt.Printf("- %v\n", e)
}
}
os.Exit(1)
}
}
- 如果类型满足多个接口,那么类型断言可使它从一个接口类型转化为另一个接口类型。
小测试
- 这个类型断言err.(SudokuError)做了什么?
不要恐慌(don’t panic)
- Go没有异常,它有个类似机制panic。
- 当panic发生,那么程序就会崩溃。
其它语言的异常vs Go的错误值
- 其它语言的异常在行为和实现上与Go语言的错误值有很大的不同:
- 如果函数抛出异常,并且附近没人捕获它,那么它就会“冒泡”到函数的调用者那里,如果还没有人进行捕获,那么就继续“冒泡”到更上层的调用者…直到达到栈(Stack)的顶部(例如main函数)。
- 异常这种错误处理方式可被看作是可选的:
- 不处理异常,就不需要加入其它代码。
- 想要处理异常,就需要加入相当数量的专用代码。
- Go语言中的错误值更简单灵活:
- 忽略错误是有意识的决定,从代码上看也是显而易见的。
小测试
- 和异常相比,Go的错误值有哪两个好处?
如何panic
- Go里有一个和其他语言异常类似的机制:panic。
- 实际上,panic很少出现。
- 创建panic:
- panic(“I forgot my towel”)
- panic的参数可以是任意类型
- panic(“I forgot my towel”)
错误值、panic、os.Exit ?
- 通常,更推荐使用错误值,其次才是panic。
- panic比os.Exit更好:panic后会执行所有defer的动作,而os.Exit则不会。
- 有时候Go程序会panic而不是返回错误值
package main
import "fmt"
func main() {
var zero int
_ = 42 / zero // panic
}
小测试
- 你的程序应该在什么时候panic?
保持冷静并继续
- 为了防止panic导致程序崩溃,Go提供了 recover 函数。
- defer的动作会在函数返回前执行,即使发生了panic。
- 但如果defer的函数调用了recover,panic就会停止,程序将继续运行。
package main
import "fmt"
func main() {
defer func() {
if e := recover(); e != nil {
fmt.Println(e)
}
}()
panic("I forgot my towel")
}
小测试
- 在哪里可以使用内置的recover函数?
作业题
- 编写一个程序:
- 在Go标准库里,有个函数可以解析网址(golang.org/pkg/net/url/#Parse):
- 使用一个非法网址传递到url.Parse函数,把发生的错误显示出来。
- 使用%#v和Printf打印错误,看看都显示什么。
- 然后执行*url.Error类型断言,来访问和打印底层结构体的字段和内容。
二十九、第六部分习题
单元实验:数独规则
三十、goroutine和并发(concurrent)
一个地鼠(gopher)工厂
goroutine
- 在Go中,独立的任务叫做goroutine
- 虽然goroutine与其它语言中的协程、进程、线程都有相似之处,但goroutine和它们并不完全相
- Goroutine创建效率非常高
- Go能直截了当的协同多个并发(concurrent)操作
- 在某些语言中,将顺序式代码转化为并发式代码需要做大量修改
- 在Go里,无需修改现有顺序式的代码,就可以通过goroutine以并发的方式运行任意数量的任务。
启动goroutine
- 只需在调用前面加一个go关键字。
package main
import (
"fmt"
"time"
)
func main() {
go sleepyGopher()
time.Sleep(4 * time.Second)
}
func sleepyGopher() {
time.Sleep(3 * time.Second)
fmt.Println("...snore...")
}
小测试
- 怎样才能在Go语言里同时做不止一件事情?
- 使用什么关键字可以启动一个新的、独立运行的任务?
不止一个goroutine
- 每次使用go关键字都会产生一个新的goroutine。
- 表面上看,goroutine似乎在同时运行,但由于计算机处理单元有限,其实技术上来说,这些goroutine不是真的在同时运行。
- 计算机处理器会使用“分时”技术,在多个goroutine上轮流花费一些时间。
- 在使用goroutine时,各个goroutine的执行顺序无法确定。
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
go sleepyGopher()
}
time.Sleep(4 * time.Second)
}
func sleepyGopher() {
time.Sleep(3 * time.Second)
fmt.Println("...snore...")
}
goroutine的参数
- 向goroutine传递参数就跟向函数传递参数一样,参数都是按值传递的(传入的是副本)
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
go sleepyGopher(i)
}
time.Sleep(4 * time.Second)
}
func sleepyGopher(id int) {
time.Sleep(3 * time.Second)
fmt.Println("...snore...", id)
}
小测试
- 多个不同的goroutine将以何种顺序执行?
通道channel
- 通道(channel)可以在多个goroutine之间安全的传值。
- 通道可以用作变量、函数参数、结构体字段…
- 创建通道用make函数,并指定其传输数据的类型
- c := make(chan int)
通道channel发送、接收
- 使用左箭头操作符 <- 向通道发送值 或 从通道接收值
- 向通道发送值:c <- 99
- 从通道接收值:r := <- c
- 发送操作会等待直到另一个goroutine尝试对该通道进行接收操作为止。
- 执行发送操作的goroutine在等待期间将无法执行其它操作
- 未在等待通道操作的goroutine可以继续自由的运行
- 执行接收操作的goroutine将等待直到另一个goroutine尝试向该通道进行发送操作为止。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
go sleepyGopher(i, c)
}
for i := 0; i < 5; i++ {
gopherID := <- c
fmt.Println("gopher ", gopherID, " has finished sleeping")
}
}
func sleepyGopher(id int, c chan int) {
time.Sleep(3 * time.Second)
fmt.Println("...snore...", id)
c <- id
}
通道 例子
小测试
- 应该使用什么语句,才能将字符串“hello world”发送至名为c的通道?
- 如何才能从通道中接收值并将其赋给变量?
使用select处理多个通道
- 等待不同类型的值。
- time.After函数,返回一个通道,该通道在指定时间后会接收到一个值(发送该值的goroutine是Go运行时的一部分)。
- select和switch有点像。
- 该语句包含的每个case都持有一个通道,用来发送或接收数据。
- select会等待直到某个case分支的操作就绪,然后就会执行该case分支。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
go sleepyGopher(i, c)
}
timeout := time.After(2 * time.Second)
for i := 0; i < 5; i++ {
select {
case gopherID := <- c:
fmt.Println("gopher ", gopherID, " has finished sleeping")
case <- timeout:
fmt.Println("my patience ran out")
return
}
}
}
func sleepyGopher(id int, c chan int) {
time.Sleep(time.Duration(rand.Intn(4000)) * time.Millisecond)
c <- id
}
- 注意:即使已经停止等待goroutine,但只要main函数还没返回,仍在运行的goroutine将会继续占用内存。
select语句
- select语句在不包含任何case的情况下将永远等下去。
nil通道
- 如果不使用make初始化通道,那么通道变量的值就是nil(零值)
- 对nil通道进行发送或接收不会引起panic,但会导致永久阻塞。
- 对nil通道执行close函数,那么会引起panic
- nil通道的用处:
- 对于包含select语句的循环,如果不希望每次循环都等待select所涉及的所有通道,那么可以先将某些通道设为nil,等到发送值准备就绪之后,再将通道变成一个非nil值并执行发送操作。
小测试
- time.After返回什么类型的值?
- 对值为nil的通道执行发送或接收操作会产生什么后果?
- select语句的每个case可以包含什么?
阻塞和死锁
- 当goroutine在等待通道的发送或接收时,我们就说它被阻塞了。
- 除了goroutine本身占用少量的内存外,被阻塞的goroutine并不消耗任何其它资源。
- goroutine静静的停在那里,等待导致其阻塞的事情来解除阻塞。
- 当一个或多个goroutine因为某些永远无法发生的事情被阻塞时,我们称这种情况为死锁。而出现死锁的程序通常会崩溃或挂起。
- (引发死锁的例子)
package main
func main() {
c := make(chan int)
// go func() {c <- 2} ()
<- c // deadlock 死锁
}
小测试
- 被阻塞的goroutine会做什么?
地鼠装配线
package main
func sourceGopher(downstream chan string) {
for _, v := range []string{"hello world", "a bad apple", "goodbye all"}
{
downstream <- v
}
downstream <- ""
}
func filterGopher(upstream, downstream chan string) {
for {
item := <-upstream
if item == "" {
downstream <- ""
return
}
if !strings.Contains(item, "bad") {
downstream <- item
}
}
}
func printGopher(upstream chan string) {
for {
v := <-upstream
if v == "" {
return
}
fmt.Println(v)
}
}
func main() {
c0 := make(chan string)
c1 := make(chan string)
go sourceGopher(c0)
go filterGopher(c0, c1)
printGopher(c1)
}
- Go允许在没有值可供发送的情况下通过close函数关闭通道
- 例如close(c)
- 通道被关闭后无法写入任何值,如果尝试写入将引发panic。
- 尝试读取被关闭的通道会获得与通道类型对应的零值。
- 注意:如果循环里读取一个已关闭的通道,并没检查通道是否关闭,那么该循环可能会一直运转下去,耗费大量CPU时间
- 执行以下代码可得知通道是否被关闭:
- v, ok := <- c
package main
import (
"fmt"
"strings"
)
func sourceGopher(downstream chan string) {
for _, v := range []string{"hello world", "a bad apple", "goodbye all"}
{
downstream <- v
}
close(downstream)
}
func filterGopher(upstream, downstream chan string) {
for {
item, ok := <-upstream
if !ok {
close(downstream)
return
}
if !strings.Contains(item, "bad") {
downstream <- item
}
}
}
func printGopher(upstream chan string) {
for {
v := <-upstream
if v == "" {
return
}
fmt.Println(v)
}
}
func main() {
c0 := make(chan string)
c1 := make(chan string)
go sourceGopher(c0)
go filterGopher(c0, c1)
printGopher(c1)
}
常用模式
- 从通道里面读取值,直到它关闭为止。
- 可以使用range关键字达到该目的
package main
import (
"fmt"
"strings"
)
func sourceGopher(downstream chan string) {
for _, v := range []string{"hello world", "a bad apple", "goodbye all"}
{
downstream <- v
}
close(downstream)
}
//func filterGopher(upstream, downstream chan string) {
// for {
// item, ok := <-upstream
// if !ok {
// close(downstream)
// return
// }
// if !strings.Contains(item, "bad") {
// downstream <- item
// }
// }
//}
func filterGopher(upstream, downstream chan string) {
for item := range upstream {
if !strings.Contains(item, "bad") {
downstream <- item
}
}
close(downstream)
}
//func printGopher(upstream chan string) {
// for {
// v := <-upstream
// if v == "" {
// return
// }
// fmt.Println(v)
// }
//}
func printGopher(upstream chan string) {
for v := range upstream {
fmt.Println(v)
}
}
func main() {
c0 := make(chan string)
c1 := make(chan string)
go sourceGopher(c0)
go filterGopher(c0, c1)
printGopher(c1)
}
小测试
- 尝试读取一个已经关闭的通道会得到什么值?
- 如何才能检测通道是否已经关闭?
作业题
- 编写一个流水线部件(一个goroutine),他需要记住前面出现的所有值,并且只有在值之前从未出现过的情况下才会将值传递至流水线的下一个阶段。假定第一个值永远不是空字符串。
- 编写一个流水线部件,它接收字符串并将它们拆分成单词,然后向流水线的下一阶段一个接一个的发送这些单词。可以使用strings.Fields函数
三十一、并发状态
并发状态
- 共享值
- 竞争条件(race condition)
Go的互斥锁(mutex)
- mutex = mutual exclusive
- Lock(),Unlock()
- sync包
package main
import "sync"
var mu sync.Mutex
func main() {
mu.Lock()
defer mu.Unlock()
// The lock is held until we return from the function.
}
- 互斥锁定义在被保护的变量之上
package main
import "sync"
// Visited tracks whether web pages have been bisited.
// Its methods mya be used concurrently from multiple goroutines.
type Visited struct {
// mu guards the visited map.
mu sync.Mutex
visited map[string]int
}
// VisitLink tracks that the page with the given URL has
// been visited, and returns the updated link count.
func (v *Visited) VisitLink(url string) int {
v.mu.Lokc()
defer v.mu.Unlock()
count := v.visited[url]
count++
v.visited[url] = count
return count
}
func main() {
}
小测试
- 当两个goroutine同时修改一个值的时候,会发生什么?
- 尝试对一个已被锁定的互斥锁执行锁定操作,会发生什么?
- 尝试对一个未被锁定的互斥锁执行解锁操作,会发生什么?
- 同时在多个不同的goroutine里面调用相同类型的方法是安全的吗?
互斥锁的隐患
- 死锁
- 为保证互斥锁的安全使用,我们须遵守以下规则:
- 尽可能的简化互斥锁保护的代码
- 对每一份共享状态只使用一个互斥锁
小测试
- 尝试锁定一个互斥锁可能会引起哪两个问题?
长时间运行的工作进程
- 工作进程(worker)
- 通常会被写成包含select语句的for循环。
package main
import "fmt"
func worker() {
for {
select {
// Wait for channels here.
}
}
}
func main() {
go worker()
}
事件循环和goroutine
- 事件循环(event loop)
- 中心循环(central loop)
- Go通过提供goroutine作为核心概念,消除了对中心循环的需求。
例子一
package main
import (
"fmt"
"time"
)
func worker() {
n := 0
next := time.After(time.Second)
for {
select {
case <- next:
n++
fmt.Println(n)
next = time.After(time.Second)
}
}
}
func main() {
go worker()
}
例子二
package main
import (
"fmt"
"image"
"time"
)
func worker() {
pos := image.Point{X: 10, Y: 10}
direction := image.Point{X: 1, Y: 0}
next := time.After(time.Second)
for {
select {
case <- next:
pos = pos.Add(direction)
fmt.Println("current position is ", pos)
next = time.After(time.Second)
}
}
}
type command int
const (
right = command(0)
left = command(1)
)
// RoverDriver drives a rover around the surface of Mars.
type RoverDriver struct {
commandc chan command
}
// NewRoverDriver ...
func NewRoverDriver() *RoverDriver {
r := &RoverDriver {
commandc: make(chan command),
}
go r.driver()
return r
}
// drive is responsible for driving the rover. It
// is expected to be started in a goroutine.
func (r *RoverDriver) drive() {
pos := image.Point{X: 0, Y: 0}
direction := image.Point{X: 1, Y: 0}
updateInterval := 250 * time.Millisecond
nextMove := time.After(updateInterval)
for {
select {
case c := <-r.commandc:
switch c {
case right:
direction = image.Point {
X: -direction.Y,
Y: direction.X,
}
case left:
direction = image.Point {
X: direction.Y,
Y: -direction.X,
}
}
log.Printf("new direction %v", direction)
case <- nextMove:
pos = pos.Add(direction)
log.Printf("moved to %v", pos)
nextMove = time.After(updateInterval)
}
}
}
// Left turns the rover left (90° counterclockwise).
func (r *RoverDriver) Left() {
r.commandc <- left
}
// Right turns the rover fight (90° clockwise).
func (r *RoverDriver) Right() {
r.commandc <- right
}
func main() {
r := NewRoverDriver()
time.Sleep(3 * time.Second)
r.Left()
time.Sleep(3 * time.Second)
r.Right()
time.Sleep(3 * time.Second)
}
小测试
- Go提供了什么来替代事件循环?
- Go标准库中的哪个包提供了Point数据类型?
- 在实现长时间运行的工作进程goroutine时,你会使用Go中的哪些语句?
- 如何隐藏使用通道时的内部细节?
- Go的通道可以发送哪些值?
作业题
- 以例子为基础,修改代码使得每次移动之间的间隔增加半秒。
- 以RoverDriver类型为基础,定义Start方法、Stop方法和对应的命令,然后修改代码使得探测器可以接受这两个新命令。
三十二、第七部分习题
单元实验:寻找火星生命