概述
1. 变量
声明单个变量
func main() {
// 1. 指定变量类型,若不赋值,则默认值。(int => 0)
var a int
a = 10
fmt.Println("a =",a)
// var a int = 10 声明变量并初始化
// 2. 类型推导,系统自动推导变量类型
var b = 10
fmt.Println("b =",b)
// 3. 省略var, 用 : 替代
c := 10
fmt.Println("c =",c)
var n1 float32 = -123.0000901
var n2 float64 = -123.0000901
fmt.Println("n1=",n1,"n2=",n2)
}
声明多个变量
func main() {
var width, height int = 100, 50
fmt.Println("width = ", width, "height = ", height)
// 在一个语句中声明不同类型的变量
var (
name = "golang"
age = 10
sex = "man"
)
fmt.Println(name, age, sex)
// 简短声明
width, h := 500, 100 //错误,要求 := 操作符的左边至少有一个变量是尚未声明的
fmt.Println(width, h)
// 变量也可以在运行时进行赋值
a, b := 145.8, 543.8
c := math.Min(a, b)
fmt.Println("minimum value is ", c)
}
2. 类型
Go支持的基本类型:
- bool
- 数字类型
- 有符号整型 : int8, int16, int32, int64, int
- 无符号整型 : uint8, uint16, unint32, unint64, uint
- 浮点型 : float32, float64
- 复数 : complex64, complex128
- int8的别名 : byte
- int32的别名 : rune
- string
类型转换
把 v 转换为 T 类型的语法是 T(v)
func main() {
i := 55
j := 67.8
//sum = i + j 错误
sum := i + int(j)
fmt.Println(sum) // 122
}
3. 常量
定义
var a int = 50
var b string = "I love Go"
在上面的代码中,变量 a
和 b
分别被赋值为常量 50
和 I love GO
。
关键字 const
被用于表示常量,比如 50
和 I love Go
。
即使在上面的代码中我们没有明确的使用关键字 const
,但是在 Go 的内部,它们是常量。
package main
func main() {
const a = 55 // 允许
a = 89 // 不允许重新赋值
}
常量的值会在编译的时候确定。
因为函数调用发生在运行时,所以不能将函数的返回值赋值给常量。
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println("Hello, playground")
var a = math.Sqrt(4) // 允许
const b = math.Sqrt(4) // 不允许
}
查看常量类型
func main() {
var name = "Sam"
fmt.Printf("type %T value %v", name, name)
// type string value Sam
}
4. 函数
函数声明
func functionname(parameter1 type, parameter2 type) returntype {
// 函数体
}
[可选] 参数,返回值
例如:计算 商品总价 = 商品单价 * 商品数量
func totalPrice(price int, number int) int {
var res = price * number
return res
}
func main() {
price, number := 2, 3
ans := totalprice(price, number)
fmt.Println(ans)
}
如果有连续若干个参数,它们的类型一致,那么我们无须一一罗列,只需在最后一个参数后添加该类型。 例如,price int, no int
可以简写为 price, no int
多个返回值
Go 语言支持一个函数可以有多个返回值。
func rectProps(length, width float64) (float64, float64) {
var area = length * width
var perimeter = (length + width) * 2
return area, perimeter
}
func main() {
area, perimeter := rectProps(10.8, 5.6)
fmt.Printf("Area %f Perimeter %f", area, perimeter)
}
命名返回值
从函数中可以返回一个命名值。
一旦命名了返回值,可以认为这些值在函数第一行就被声明为变量了。
上面的 rectProps 函数也可用这个方式写成:
func rectProps(length, width float64)(area, perimeter float64) {
area = length * width
perimeter = (length + width) * 2
return // 不需要明确指定返回值,默认返回 area, perimeter 的值
}
请注意, 函数中的 return 语句没有显式返回任何值。由于 area 和 perimeter 在函数声明中指定为返回值, 因此当遇到 return 语句时, 它们将自动从函数返回。
空白符
_ 在 Go 中被用作空白符,可以用作表示任何类型的任何值。
我们继续以 rectProps
函数为例,该函数计算的是面积和周长。假使我们只需要计算面积,而并不关心周长的计算结果,该怎么调用这个函数呢?这时,空白符 _ 就上场了。
下面的程序我们只用到了函数 rectProps
的一个返回值 area
func rectProps(length, width float64) (float64, float64) {
var area = length * width
var perimeter = (length + width) * 2
return area, perimeter
}
func main() {
area, _ := rectProps(10.8, 5.6)
fmt.Printf("Area %f", area)
}
5. 包
所有可执行的 Go 程序都必须包含一个 main 函数。
这个函数是程序运行的入口。
main 函数应该放置于 main 包中。
创建自定义的包
package rectangle
import "math"
// 计算矩形面积
func Area(len, wid float64) float64 {
area := len * wid
return area
}
// 计算矩形对角线的长度
func Diagonal(len, wid float64) float64 {
diagonal := math.Sqrt((len * len) + (wid * wid)) // 平方根
return diagonal
}
在 Go 中,任何以大写字母开头的变量或者函数都是被导出的名字。
其它包只能访问被导出的函数和变量。
因此,Area 和 Diagonal 函数的首字母大写。
导入自定义包
package main // 指定该文件属于main包
import "fmt" // 导入其他包
import "5包/geometry/rectangle"
func main() {
var rectLen, rectWidth float64 = 6, 7
fmt.Println("几何参数")
fmt.Printf("area %.2f n", rectangle.Area(rectLen, rectWidth))
fmt.Printf("rectangle %.2f", rectangle.Diagonal(rectLen, rectWidth))
}
init 函数
init 函数可用于执行初始化任务,也可用于在开始执行之前验证程序的正确性。
包的初始化顺序如下:
- 首先初始化包级别(Package Level)的变量
- 紧接着调用 init 函数。包可以有多个 init 函数(在一个文件或分布于多个文件中),它们按照编译器解析它们的顺序进行调用。
package main // 指定该文件属于main包
// 导入其他包
import (
"5包/geometry/rectangle"
"fmt"
"log"
)
// 包级别变量
var rectLen, rectWidth float64 = 6, 7
// init 函数
func init() {
println("main package initialized")
if rectLen < 0{
log.Fatal("length is less than zero")
}
if rectWidth < 0{
log.Fatal("width is less than zero")
}
}
func main() {
fmt.Println("几何参数")
fmt.Printf("area %.2f n", rectangle.Area(rectLen, rectWidth))
fmt.Printf("rectangle %.2f", rectangle.Diagonal(rectLen, rectWidth))
}
main 包的初始化顺序为:
- 首先初始化被导入的包。因此,首先初始化了 rectangle 包。
- 接着初始化了包级别的变量 rectLen 和 rectWidth。
- 调用 init 函数。
- 最后调用 main 函数。
空白标识符
导入了包,却不在代码中使用它,这在 Go 中是非法的。
然而,在程序开发的活跃阶段,又常常会先导入包,而暂不使用它。遇到这种情况就可以使用空白标识符 _
。
package main
import (
"geometry/rectangle"
_ "fmt" // 错误屏蔽器
)
var _ = rectangle.Area // 错误屏蔽器
func main() {
}
6. if else
语法
if condition {
// 代码
} else if condition {
// 代码
} else { // }和else 必须在同一行,避免插入分号
// 代码
}
在 Go 语言规则中,它指定在 }
之后插入一个分号,如果这是该行的最终标记。因此,在if语句后面的 }
会自动插入一个分号。
实际上我们的程序变成了
if num%2 == 0 {
fmt.Println("the number is even")
}; //semicolon inserted by Go
else {
fmt.Println("the number is odd")
}
分号插入之后。从上面代码片段可以看出第三行插入了分号。因此会报错。
举例:
package main
import "fmt"
func main() {
num := 99
if num <= 50 {
fmt.Println("number is less than or equal to 50")
} else if num >= 51 && num <= 100 {
fmt.Println("number is between 51 and 100")
} else {
fmt.Println("number is greater than 100")
}
}
7. 循环
for
是 Go 语言唯一的循环语句。
Go 语言中并没有其他语言比如 C 语言中的 while
和 do while
循环。
for 循环语法
for
循环的三部分,初始化语句、条件语句、post 语句都是可选的。
for initialisation; condition; post {
}
eg.
func main() {
for i := 1; i <= 10; i++ {
fmt.Printf(" %d",i)
}
}
or.
func main() {
i := 0
for i <= 10 {
fmt.Printf("%d ", i)
i += 2
}
}
多个变量
在 for
循环中可以声明和操作多个变量。
func main() {
for no, i := 10, 1; i <= 10 && no <= 19; i, no = i+1, no+1{
fmt.Printf("%d * %d = %d n", no, i ,no*i)
}
}
break
break
语句用于在完成正常执行之前突然终止 for 循环,之后程序将会在 for 循环下一行代码开始执行。
continue
continue
语句用来跳出 for
循环中当前循环。在 continue
语句后的所有的 for
循环语句都不会在本次循环中执行。循环体会在一下次循环中继续执行。
无限循环
无限循环的语法是:
for {
}
8. switch
switch
是一个条件语句,用于将表达式的值与可能匹配的选项列表进行比较,并根据匹配情况执行相应的代码块。
func main(){
num := 3
switch num {
case 1:
fmt.Println(1)
case 2:
fmt.Println(2)
case 3:
fmt.Println(3)
default:
fmt.Println("5")
}
}
当匹配不成功时,执行default.
多表达式判断
通过用逗号分隔,可以在一个 case 中包含多个表达式。
func main() {
letter := "i"
switch letter {
case "a", "e", "i", "o", "u": // 一个选项多个表达式
fmt.Println("vowel")
default:
fmt.Println("not a vowel")
}
}
无表达式
在 switch 语句中,表达式是可选的,可以被省略。
如果省略表达式,则表示这个 switch 语句等同于 switch true
,并且每个 case
表达式都被认定为有效,相应的代码块也会被执行。
func main() {
num := 75
switch { // 表达式被省略了
case num >= 0 && num <= 50:
fmt.Println("num is greater than 0 and less than 50")
case num >= 51 && num <= 100:
fmt.Println("num is greater than 51 and less than 100")
case num >= 101:
fmt.Println("num is greater than 100")
}
}
Fallthrough 语句
在 Go 中,每执行完一个 case 后,会从 switch 语句中跳出来,不再做后续 case 的判断和执行。
使用 fallthrough
语句可以在已经执行完成的 case 之后,把控制权转移到下一个 case 的执行代码中。
func main() {
switch num := number(); { // num is not a constant
case num < 50:
fmt.Printf("%d is lesser than 50n", num)
fallthrough
case num < 100:
fmt.Printf("%d is lesser than 100n", num)
fallthrough
case num < 200:
fmt.Printf("%d is lesser than 200", num)
}
}
fallthrough
语句应该是 case 子句的最后一个语句。
9. 数组和切片
数组
数组是同一类型元素的集合。Go 语言中不允许混合不同类型的元素。
如果是 interface{} 类型数组,可以包含任意类型
数组的声明
一个数组的表示形式为 [n]T
。n
表示数组中元素的数量,T
代表每个元素的类型。
func main() {
var a [3]int
fmt.Println(a) // [0 0 0]
a[0] = 1
a[1] = 2
a[2] = 3
fmt.Println(a) // [1 2 3]
}
简略声明:
func main() {
a := [3]int{1,2,3}
fmt.Println(a) // [1 2 3]
b := [3]int{1}
fmt.Println(b) // [1 0 0]
}
省略数组长度,用 ...
代替:
func main() {
a := [...]int{1,2,3}
fmt.Println(a) // [1 2 3]
}
数组的大小是类型的一部分。因此 [5]int
和 [25]int
是不同类型。数组不能调整大小。
Go 中的数组是值类型而不是引用类型。这意味着当数组赋值给一个新的变量时,该变量会得到一个原始数组的副本。如果对新变量进行更改,则不会影响原始数组。
func main() {
a := [...]int{1,2,3,4,5}
b := a
b[0] = 6
fmt.Println(a) // [1 2 3 4 5]
fmt.Println(b) // [6 2 3 4 5]
}
同样,当数组作为参数传递给函数时,它们是按值传递,而原始数组保持不变。
func changeLocal(num [5]int) {
num[0] = 55
fmt.Println("inside function ", num) // [55 6 7 8 8]
}
func main() {
num := [...]int{5, 6, 7, 8, 8}
fmt.Println("before passing to function ", num) // [5 6 7 8 8]
changeLocal(num)
fmt.Println("after passing to function ", num) // [5 6 7 8 8]
}
数组的长度
通过将数组作为参数传递给 len()
函数, len(Array)
可以得到数组的长度。
range
通过使用 for
循环的 range 方法来遍历数组,返回索引和该索引处的值
func main() {
a := [...]float64{12.3, 23.4, 34.5, 45.6, 56.7}
sum := float64(0)
for i,v := range a{
fmt.Printf("数组a中第%d位元素是%.2f n", i, v)
sum += v
}
fmt.Println("数组a的元素和为:", sum)
}
如果只需要值并希望忽略索引,则可以通过用 _
空白标识符替换索引来执行。
for _, v := range a { // ignores index
}
多维数组
func main() {
a := [3][2]string{
{"lion", "tiger"},
{"cat", "dog"},
{"pigeon", "peacock"}, //逗号必须
}
}
切片
切片是由数组建立的一种方便、灵活且功能强大的包装(Wrapper)。切片本身不拥有任何数据。它们只是对现有数组的引用。
创建一个切片
带有 T 类型元素的切片由 []T
表示。
使用语法 a[start:end]
创建一个从 a
数组索引 start
开始到 end - 1
结束的切片:
func main() {
a := [5]int{1,2,3,4,5}
var b []int = a[1:4]
fmt.Println(b)
}
or
创建一个有 3 个整型元素的数组,并返回一个存储在 c 中的切片引用:
func main() {
c := []int{6, 7, 8}
fmt.Println(c)
}
切片的修改
切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。
func main() {
darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
dslice := darr[2:5]
fmt.Println("array before", darr) // [57 89 90 82 100 78 67 69 59]
for i := range dslice {
dslice[i]++
}
fmt.Println("array after", darr) // [57 89 91 83 101 78 67 69 59]
}
当多个切片共享同一个数组时,每个切片所做的修改都会反映在数组中。
切片的长度和容量
切片的长度是切片中的元素数。切片的容量是从创建切片索引开始的底层数组中元素数。
func main() {
fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
fruitslice := fruitarray[1:3]
fmt.Println(len(fruitslice)) // 2
fmt.Println(cap(fruitslice)) // 6
}
使用 make 创建一个切片
make 函数创建一个数组,并返回引用该数组的切片:
func make([]类型,长度,容量)
容量是可选参数, 默认值为切片长度。
func main() {
i := make([]int, 5, 5)
fmt.Println(i) // [0 0 0 0 0]
}
追加切片元素
数组的长度是固定的,不能增加。
切片是动态的,使用 append
可以将新元素追加到切片上。append 函数的定义是
func append(s[]T,x ... T)
。
如果切片由数组支持,并且数组本身的长度是固定的,那么切片如何具有动态长度。以及内部发生了什么?
当新的元素被添加到切片时,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回这个新数组的新切片引用。现在新切片的容量是旧切片的两倍(不同版本扩容方式不同)。
func main() {
cars := []string{"Ferrari", "Honda", "Ford"}
fmt.Println("长度是:", len(cars), ", 容量是:", cap(cars)) // 长度是: 3 , 容量是: 3
cars = append(cars, "Toyota")
fmt.Println("长度是:", len(cars), ", 容量是:", cap(cars)) // 长度是: 4 , 容量是: 6
}
切片类型的零值为 nil
。一个 nil
切片的长度和容量为 0。
也可以使用 ...
运算符将一个切片添加到另一个切片。
func main() {
veggies := []string{"potatoes", "tomatoes", "brinjal"}
fruits := []string{"oranges", "apples"}
food := append(veggies, fruits...) //将fruits添加到veggies中
fmt.Println("food:",food) // food: [potatoes tomatoes brinjal oranges apples]
切片的函数传递
我们可以认为,切片在内部可由一个结构体类型表示。这是它的表现形式,
type slice struct {
Length int
Capacity int
ZerothElement *byte
}
切片包含长度、容量和指向数组第零个元素的指针。
当切片传递给函数时,即使它通过值传递,指针变量也将引用相同的底层数组。
因此,当切片作为参数传递给函数时,函数内所做的更改也会在函数外可见,区别于数组。
func subtactOne(numbers []int) {
for i := range numbers {
numbers[i] -= 2
}
}
func main() {
nos := []int{8, 7, 6}
fmt.Println("slice before function call", nos) // [8 7 6]
subtactOne(nos)
fmt.Println("slice after function call", nos) // [6 5 4]
}
多维切片
类似于数组,切片可以有多个维度。
func main() {
pls := [][]string {
{"C", "C++"},
{"JavaScript"},
{"Go", "Rust"},
}
for _, v1 := range pls {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("n")
}
}
内存优化
切片持有对底层数组的引用。只要切片在内存中,数组就不能被垃圾回收。
假设有一个非常大的数组,我们只想处理它的一小部分。由这个数组创建一个切片,并开始处理切片,在切片引用时数组仍然存在内存中。
一种解决方法是使用 copy函数 func copy(dst,src[]T)int
来生成一个切片的副本。这样我们可以使用新的切片,原始数组可以被垃圾回收。
func countries() []string {
countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
neededCountries := countries[:len(countries)-2]
countriesCpy := make([]string, len(neededCountries))
copy(countriesCpy, neededCountries) // 将 neededCountries 复制到 countriesCpy
return countriesCpy
}
func main() {
countriesNeeded := countries()
fmt.Println(countriesNeeded)
}
现在 countries
数组可以被垃圾回收, 因为 neededCountries
不再被引用。
10. 可变参数函数
可变参数函数是一种参数个数可变的函数。
语法
如果函数最后一个参数被记作 ...T
,这时函数可以接受任意个 T
类型参数作为最后一个参数。
请注意只有函数的最后一个参数才允许是可变的。
拿append
函数举例:
func append(slice []Type, elems ...Type)
上面是 append 函数的定义。在定义中 elems 是可变参数。这样 append 函数可以接受可变化的参数。
可变参数函数的工作原理是把可变参数转换为一个新的切片。
func find(num int, nums ...int) {
fmt.Printf("type of nums is %Tn", nums)
found := false
for i, v := range nums {
if v == num {
fmt.Println(num, "found at index", i, "in", nums)
found = true
}
}
if !found {
fmt.Println(num, "not found in ", nums)
}
fmt.Printf("n")
}
func main() {
find(89, 89, 90, 95)
find(45, 56, 67, 45, 90, 109)
find(78, 38, 56, 98)
find(87)
}
以上面程序中为例,find
函数中的可变参数是 89,90,95 。 find 函数接受一个 int
类型的可变参数。因此这三个参数被编译器转换为一个 int 类型切片 int []int{89, 90, 95}
然后被传入 find
函数。
给可变参数函数传入切片
在切片后加上 ...
后缀,切片将直接传入函数,不再创建新的切片
func find(num int, nums ...int) {
fmt.Printf("type of nums is %Tn", nums)
found := false
for i, v := range nums {
if v == num {
fmt.Println(num, "found at index", i, "in", nums)
found = true
}
}
if !found {
fmt.Println(num, "not found in ", nums)
}
fmt.Printf("n")
}
func main() {
nums := []int{89, 90, 95}
find(89, nums...)
}
11. map
map 是在 Go 中将值(value)与键(key)关联的内置类型。通过相应的键可以获取到值。
创建map
语法:
make(map[type of key]type of value)
personSalary := make(map[string]int)
上面的代码创建了一个名为 personSalary
的 map,其中键是 string 类型,值是 int 类型。
map 必须使用 make
函数初始化,未初始化的 map 的值是 nil。
给 map 添加元素
func main() {
personSalary := make(map[string]int)
personSalary["steve"] = 12000
personSalary["jamie"] = 15000
personSalary["mike"] = 9000
fmt.Println(personSalary) //map[jamie:15000 mike:9000 steve:12000]
}
or:
func main() {
personSalary := map[string]int {
"steve": 12000,
"jamie": 15000,
}
personSalary["mike"] = 9000
fmt.Println(personSalary) // map[jamie:15000 mike:9000 steve:12000]
}
获取 map 中的元素
获取值
获取 map 元素的语法是 map[key]
func main() {
personSalary := map[string]int{
"steve": 12000,
"jamie": 15000,
}
personSalary["mike"] = 9000
employee := "jamie"
fmt.Println(employee, "is", personSalary[employee]) // jamie is 15000
fmt.Println("joe is", personSalary["joe"]) //joe is 0
}
如果我们获取一个不存在的元素,会返回
int
类型的零值0
map 中 key 是否存在?
如果想知道 map 中到底是不是存在这个 key
,该怎么做 ?
value, ok := map[key]
上面就是获取 map 中某个 key 是否存在的语法。如果 ok
是 true,表示 key 存在,key 对应的值就是 value
,反之表示 key 不存在。
func main() {
personSalary := map[string]int{
"steve": 12000,
"jamie": 15000,
}
personSalary["mike"] = 9000
newEmp := "joe"
value, ok := personSalary[newEmp]
if ok == true {
fmt.Println("Salary of", newEmp, "is", value)
} else {
fmt.Println(newEmp,"not found")
}
}
遍历 map 中所有的元素
遍历 map 中所有的元素需要用 for range
循环。
func main() {
personSalary := map[string]int{
"steve": 12000,
"jamie": 15000,
}
personSalary["mike"] = 9000
fmt.Println("All items of a map")
for key, value := range personSalary {
fmt.Printf("personSalary[%s] = %dn", key, value)
}
}
删除 map 中的元素
删除 map
中 key
的语法是 delete(map, key)
没有返回值。
func main() {
personSalary := map[string]int{
"steve": 12000,
"jamie": 15000,
}
personSalary["mike"] = 9000
fmt.Println(personSalary) // map[jamie:15000 mike:9000 steve:12000]
delete(personSalary, "steve")
fmt.Println(personSalary) // map[jamie:15000 mike:9000]
}
获取 map 的长度
获取 map 的长度使用 len
函数
func main() {
personSalary := map[string]int{
"steve": 12000,
"jamie": 15000,
}
personSalary["mike"] = 9000
fmt.Println(len(personSalary)) // 3
}
Map 是引用类型
和 slices
类似,map 也是引用类型。
当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。
func main() {
personSalary := map[string]int{
"steve": 12000,
"jamie": 15000,
}
personSalary["mike"] = 9000
fmt.Println(personSalary) // map[jamie:15000 mike:9000 steve:12000]
newPersonSalary := personSalary
newPersonSalary["mike"] = 18000
fmt.Println(newPersonSalary) // map[jamie:15000 mike:18000 steve:12000]
fmt.Println(personSalary) // map[jamie:15000 mike:18000 steve:12000]
}
Map 的相等性
map 之间不能使用 ==
操作符判断,==
只能用来检查 map 是否为 nil
。
判断两个 map 是否相等的方法是遍历比较两个 map 中的每个元素。
12. 字符串
Go 语言中的字符串是一个字节切片
Go 中的字符串是兼容 Unicode 编码的,并且使用 UTF-8 进行编码。
获取字符串的每一个字节
func printBytes(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func printChars(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%c ",s[i]) // %c: 字符
}
}
func main() {
name := "Hello World"
printBytes(name) // 48 65 6c 6c 6f 20 57 6f 72 6c 64
fmt.Printf("n")
printChars(name) // H e l l o W o r l d
fmt.Printf("n")
name = "Señor"
printBytes(name) // 53 65 c3 b1 6f 72
fmt.Printf("n")
printChars(name) // S e à ± o r
}
上面的程序获取字符串的每一个字符,虽然看起来是合法的,但却有一个严重的 bug。
在上面程序,我们尝试输出 Señor 的字符,但却输出了错误的 S e à ± o r。 为什么程序分割 Hello World
时表现完美,但分割 Señor
就出现了错误呢?这是因为 ñ
的 Unicode 代码点(Code Point)是 U+00F1
。它的 UTF-8 编码占用了 c3 和 b1 两个字节。它的 UTF-8 编码占用了两个字节 c3 和 b1。而我们打印字符时,却假定每个字符的编码只会占用一个字节,这是错误的。在 UTF-8 编码中,一个代码点可能会占用超过一个字节的空间。那么我们该怎么办呢?rune 能帮我们解决这个难题。
rune
rune 是 Go 语言的内建类型,它也是 int32 的别称。在 Go 语言中,rune 表示一个代码点。代码点无论占用多少个字节,都可以用一个 rune 来表示。
func printBytes(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func printChars(s string) {
runes := []rune(s)
for i:= 0; i < len(runes); i++ {
fmt.Printf("%c ",runes[i])
}
}
func main() {
name := "Hello World"
printBytes(name) // 48 65 6c 6c 6f 20 57 6f 72 6c 64
fmt.Printf("n")
printChars(name) // H e l l o W o r l d
fmt.Printf("nn")
name = "Señor"
printBytes(name) // 53 65 c3 b1 6f 72
fmt.Printf("n")
printChars(name) // S e ñ o r
}
字符串的 for range
func printCharsAndBytes(s string) {
for index, rune := range s {
fmt.Printf("%c starts at byte %dn", rune, index)
}
}
func main() {
name := "Señor"
printCharsAndBytes(name)
}
// S starts at byte 0
// e starts at byte 1
// ñ starts at byte 2
// o starts at byte 4
// r starts at byte 5
从上面的输出中可以清晰的看到 ñ
占了两个字节。
用字节切片构造字符串
func main() {
byteSlice := []byte{0x43, 0x61, 0x66, 0xC3, 0xA9}
str := string(byteSlice)
fmt.Println(str) // Café
}
用 rune 切片构造字符串
func main() {
runeSlice := []rune{0x0053, 0x0065, 0x00f1, 0x006f, 0x0072}
str := string(runeSlice)
fmt.Println(str) // Señor
}
字符串的长度
utf8 package 包中的 func RuneCountInString(s string) (n int)
方法用来获取字符串的长度。这个方法传入一个字符串参数然后返回字符串中的 rune 的数量。
func length(s string) {
fmt.Printf("length of %s is %dn", s, utf8.RuneCountInString(s))
}
func main() {
word1 := "Señor"
length(word1) // 5
word2 := "Pets"
length(word2) // 4
}
字符串是不可变的
Go 中的字符串是不可变的。一旦一个字符串被创建,那么它将无法被修改。
为了修改字符串,可以把字符串转化为一个 rune 切片。然后这个切片可以进行任何想要的改变,然后再转化为一个字符串。
func mutate(s []rune) string {
s[0] = 'a'
return string(s)
}
func main() {
h := "hello"
fmt.Println(mutate([]rune(h))) // aello
}
13. 指针
什么是指针?
指针是一种存储变量内存地址(Memory Address)的变量。
如上图所示,变量 b
的值为 156
,而 b
的内存地址为 0x1040a124
。变量 a
存储了 b
的地址。我们就称 a
指向了 b
。
指针的声明
指针变量的类型为 *T
,该指针指向一个 T 类型的变量。
func main() {
b := 255
var a *int = &b
fmt.Printf("Type of a is %Tn", a) // *int
fmt.Println("address of b is", a) // 0xc00000a0b0
}
& 操作符用于获取变量的地址。上面程序把 b
的地址赋值给 *int
类型的 a
。称 a
指向了 b
。
a
的值就是 b
的地址
指针的零值
指针的零值是 nil
func main() {
a := 25
var b *int
if b == nil {
fmt.Println("b is", b) // <nil>
b = &a
fmt.Println("b after initialization is", b) // 0xc00000a0b0
}
}
指针的解引用
指针的解引用可以获取指针所指向的变量的值。将 a
解引用的语法是 *a
。
func main() {
b := 255
a := &b
fmt.Println("address of b is", a) // 0xc00000a0b0
fmt.Println("value of b is", *a) // 255
*a++
fmt.Println(a) // 0xc00000a0b0
fmt.Println(*a) // 256
fmt.Println(b) // 256
}
把 a
指向的值加 1,由于 a
指向了 b
,因此 b
的值也发生了同样的改变。
向函数传递指针参数
func change(val *int) {
*val = 55
}
func main() {
a := 58
fmt.Println(a) // 58
b := &a
change(b)
fmt.Println(a) // 55
}
向函数 change
传递了指针变量 b
,而 b
存储了 a
的地址。 change
函数解引用,修改了 a 的值。
Go 并不支持其他语言(例如 C)中的指针运算。
14. 结构体
什么是结构体?
结构体是用户定义的类型,表示若干个字段(Field)的集合。
例如,一个职员有 firstName
、lastName
和 age
三个属性,而把这些属性组合在一个结构体 employee
中就很合理。
结构体的声明
关键字:type
type Employee struct {
firstName string
lastName string
age int
}
在上面的代码片段里,声明了一个结构体类型 Employee
,它有 firstName
、lastName
和 age
三个字段。
通过把相同类型的字段声明在同一行,结构体可以变得更加紧凑。上面的结构体可以重写为:
type Employee struct {
firstName, lastName string
age, salary int
}
上面的结构体 Employee
称为 命名结构体(Named Structure)。我们创建了名为 Employee
的新类型,而它可以用于创建 Employee
类型的结构体变量。
声明结构体时也可以不用声明一个新类型(不用type
),这样的结构体类型称为 匿名结构体(Anonymous Structure)。
var employee struct {
firstName, lastName string
age int
}
上述代码片段创建一个匿名结构体 employee
。
创建命名结构体
type Employee struct {
firstName, lastName string
age, salary int
}
func main() {
// 带有字段名时,顺序可以打乱
emp1 := Employee{
firstName: "Sam",
age: 25,
salary: 500,
lastName: "Anderson",
}
// 省略字段名时,顺序必须相同
emp2 := Employee{"Thomas", "Paul", 29, 800}
fmt.Println(emp1) // {Sam Anderson 25 500}
fmt.Println(emp2) // {Thomas Paul 29 800}
}
创建匿名结构体
func main() {
emp3 := struct {
firstName, lastName string
age, salary int
}{
firstName: "Andreah",
lastName: "Nikola",
age: 31,
salary: 5000,
}
fmt.Println(emp3) // {Andreah Nikola 31 5000}
}
结构体的零值
当定义好的结构体并没有被显式地初始化时,该结构体的字段将默认赋为零值。
当为某些字段指定初始值,而忽略其他字段时,忽略的字段会被赋值为零值。
访问结构体的字段
点号操作符 .
用于访问结构体的字段。
type Employee struct {
firstName, lastName string
age, salary int
}
func main() {
emp6 := Employee{"Sam", "Anderson", 55, 6000}
fmt.Println(emp6.firstName) // Sam
fmt.Println(emp6.age) // 55
}
还可以创建零值的 struct
,以后再给各个字段赋值。
type Employee struct {
firstName, lastName string
age, salary int
}
func main() {
var emp7 Employee
emp7.firstName = "Jack"
emp7.lastName = "Adams"
fmt.Println("Employee 7:", emp7)
}
结构体的指针
type Employee struct {
firstName, lastName string
age, salary int
}
func main() {
emp8 := &Employee{"Sam", "Anderson", 55, 6000}
fmt.Println("First Name:", (*emp8).firstName) // Sam
fmt.Println("Age:", (*emp8).age) // 55
}
Go 语言允许在访问 firstName
字段时,使用 emp8.firstName
来代替显式的解引用 (*emp8).firstName
。
匿名字段
当我们创建结构体时,字段可以只有类型,而没有字段名。这样的字段称为匿名字段。
以下代码创建一个 Person
结构体,它含有两个匿名字段 string
和 int
。
type Person struct {
string
int
}
func main() {
p := Person{"Naveen", 50}
fmt.Println(p) // {"Naveen", 50}
}
虽然匿名字段没有名称,但其实匿名字段的名称就默认为它的类型。比如在上面的 Person
结构体里,虽说字段是匿名的,但 Go 默认这些字段名是它们各自的类型。所以 Person
结构体有两个名为 string
和 int
的字段。
type Person struct {
string
int
}
func main() {
var p1 Person
p1.string = "naveen"
p1.int = 50
fmt.Println(p1)
}
嵌套结构体
结构体的字段有可能也是一个结构体。这样的结构体称为嵌套结构体。
type Address struct {
city, state string
}
type Person struct {
name string
age int
address Address
}
func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.address = Address {
city: "Chicago",
state: "Illinois",
}
fmt.Println(p.name) // Naveen
fmt.Println(p.age) // 50
fmt.Println(p.address.city) // Chicago
fmt.Println(p.address.state) // Illinois
}
提升字段
如果是结构体中有匿名的结构体类型的字段,则该匿名结构体里的字段就称为提升字段。
这是因为提升字段就像是属于外部结构体一样,可以用外部结构体直接访问。
type Address struct {
city, state string
}
type Person struct {
name string
age int
Address
}
在上面的代码片段中,Person
结构体有一个匿名字段 Address
,而 Address
是一个结构体。现在结构体 Address
有 city
和 state
两个字段,访问这两个字段就像在 Person
里直接声明的一样,因此我们称之为提升字段。
type Address struct {
city, state string
}
type Person struct {
name string
age int
Address
}
func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.Address = Address{
city: "Chicago",
state: "Illinois",
}
fmt.Println("Name:", p.name)
fmt.Println("Age:", p.age)
fmt.Println("City:", p.city) // city is promoted field
fmt.Println("State:", p.state) // state is promoted field
}
上面代码使用了语法 p.city
和 p.state
,访问提升字段 city
和 state
就像它们是在结构体 p
中声明的一样。
导出结构体和字段
如果结构体名称以大写字母开头,则它是其他包可以访问的导出类型。同样,如果结构体里的字段首字母大写,它也能被其他包访问到。
(理解为大写字母开头的对象可以在其他包中使用。)
结构体相等性(Structs Equality)
结构体是值类型。
如果它的每一个字段都是可比较的,则该结构体也是可比较的。
如果结构体包含不可比较的字段(map),则结构体变量也不可比较。
如果两个结构体变量的对应字段相等,则这两个变量也是相等的。
15. 方法
什么是方法?
方法其实就是一个函数,在 func
这个关键字和方法名中间加入了一个特殊的接收器类型。
- 接收器可以是结构体类型或者是非结构体类型。
- 接收器是可以在方法的内部访问的。
创建一个方法的语法:
func (t Type) methodName(parameter list) {
}
上面的代码 创建了一个接收器类型为 Type
的方法 methodName
。
type Employee struct {
name string
salary int
currency string
}
/* displaySalary() 方法将 Employee 做为接收器类型 */
func (e Employee) displaySalary() {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
// Salary of Sam is $5000
}
func main() {
emp1 := Employee {
name: "Sam",
salary: 5000,
currency: "$",
}
emp1.displaySalary() // 调用 Employee 类型的 displaySalary() 方法
}
为什么已经有函数了还需要方法呢?
既然可以使用函数写出相同的程序,那么为什么我们需要方法?这有着几个原因:
- Go 不是纯粹的面向对象编程语言,而且Go不支持类。因此,基于类型的方法是一种实现和类相似行为的途径。
- 相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。假设我们有一个
Square
和Circle
结构体。可以在Square
和Circle
上分别定义一个Area
方法。见下面的程序。
type Rectangle struct {
length int
width int
}
type Circle struct {
radius float64
}
func (r Rectangle) Area() int {
return r.length * r.width
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func main() {
r := Rectangle{
length: 10,
width: 5,
}
fmt.Printf("Area of rectangle %dn", r.Area()) // Area of rectangle 50
c := Circle{
radius: 12,
}
fmt.Printf("Area of circle %f", c.Area()) // Area of circle 452.389342
}
指针接收器与值接收器
值接收器和指针接收器之间的区别在于:
- 在值接收器的方法内部的改变对于调用者是不可见的
- 在指针接收器的方法内部的改变对于调用者是可见的
type Employee struct {
name string
age int
}
/* 使用值接收器的方法 */
func (e Employee) changeName(newName string) {
e.name = newName
}
/* 使用指针接收器的方法 */
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}
func main() {
e := Employee{
name: "Mark Andrew",
age: 50,
}
fmt.Printf(e.name) // Mark Andrew
e.changeName("Michael Andrew")
fmt.Printf( e.name) // Mark Andrew
fmt.Println(e.age) // 50
(&e).changeAge(51)
fmt.Println(e.age) // 51
}
Go中可以使用 e.changeAge(51)
来代替 (&e).changeAge(51)
那么什么时候使用指针接收器,什么时候使用值接收器?
一般来说,指针接收器可以使用在:对方法内部的接收器所做的改变应该对调用者可见时。
指针接收器也可以被使用在如下场景:当拷贝一个结构体的代价过于昂贵时。考虑下一个结构体有很多的字段。在方法内使用这个结构体做为值接收器需要拷贝整个结构体,这是很昂贵的。在这种情况下使用指针接收器,结构体不会被拷贝,只会传递一个指针到方法内部使用。
在其他的所有情况,值接收器都可以被使用。
匿名字段的方法
属于结构体的匿名字段的方法可以被直接调用,就好像这些方法是属于定义了匿名字段的结构体一样。
type address struct {
city string
state string
}
func (a address) fullAddress() {
fmt.Printf("Full address: %s, %s", a.city, a.state)
}
type person struct {
firstName string
lastName string
address // 匿名字段
}
func main() {
p := person{
firstName: "Elon",
lastName: "Musk",
address: address {
city: "Los Angeles",
state: "California",
},
}
p.fullAddress() //访问 address 结构体的 fullAddress 方法
}
在方法中使用值接收器 与 在函数中使用值参数
-
方法使用值接收器,可以接受值接收器和指针接收器
-
函数使用值参数,只能接受值参数
type rectangle struct {
length int
width int
}
// 函数,值参数
func area(r rectangle) {
fmt.Printf("Area Function result: %dn", (r.length * r.width))
}
// 方法,值接收器
func (r rectangle) area() {
fmt.Printf("Area Method result: %dn", (r.length * r.width))
}
func main() {
r := rectangle{
length: 10,
width: 5,
}
area(r) // 函数接收值
r.area() // 方法接收值
p := &r
// 错误使用: area(p)
p.area() // 方法接收指针,等同于(*p).area()
}
在方法中使用指针接收器 与 在函数中使用指针参数
- 方法使用指针接收器,可以接受值接收器和指针接收器
- 函数使用指针参数,只能接受指针
type rectangle struct {
length int
width int
}
func perimeter(r *rectangle) {
fmt.Println("perimeter function output:", 2*(r.length+r.width))
}
func (r *rectangle) perimeter() {
fmt.Println("perimeter method output:", 2*(r.length+r.width))
}
func main() {
r := rectangle{
length: 10,
width: 5,
}
p := &r
perimeter(p) // 函数接收 指针
p.perimeter() // 方法接收 指针
// 错误使用: perimeter(r)
r.perimeter() // 方法接收 值
在非结构体上的方法
为了在一个类型上定义一个方法,方法的接收器类型定义和方法的定义应该在同一个包中。
package main
func (a int) add(b int) {
}
func main() {
}
在上面程序的第 3 行,我们尝试把一个 add
方法添加到内置的类型 int
。这是不允许的,因为 add
方法的定义和 int
类型的定义不在同一个包中。该程序会抛出编译错误 cannot define new methods on non-local type int
。
让该程序工作的方法是为内置类型 int 创建一个类型别名,然后创建一个以该类型别名为接收器的方法。
package main
import "fmt"
type myInt int
func (a myInt) add(b myInt) myInt {
return a + b
}
func main() {
num1 := myInt(5)
num2 := myInt(10)
sum := num1.add(num2)
fmt.Println("Sum is", sum)
}
在上面程序的第5行,为 int
创建了一个类型别名 myInt
。
在第7行,定义了一个以 myInt
为接收器的的方法 add
。
该程序将会打印出 Sum is 15
。
16. 接口
什么是接口?
在面向对象的领域里,接口定义一个对象的行为。
接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。
在 Go 语言中,接口就是方法签名的集合。当一个类型定义了接口中的所有方法,我们称它实现了该接口。
例如,WashingMachine
是一个含有 Cleaning()
和 Drying()
两个方法的接口。任何定义了 Cleaning()
和 Drying()
的类型,都称它实现了 WashingMachine
接口。
接口的声明与实现
编写代码,创建一个接口并且实现它。
// 创建了一个名为 VowelsFinder 的接口,该接口有一个 FindVowels() []rune 的方法。
type VowelsFinder interface {
FindVowels() []rune
}
// 创建了一个 MyString 类型
type MyString string
// 给接受者类型 MyString 添加了 FindVowels() []rune 方法
// 称 MyString 实现了 VowelsFinder 接口
func (ms MyString) FindVowels() []rune {
var vowels []rune
for _, rune := range ms {
if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
vowels = append(vowels, rune)
}
}
return vowels
}
func main() {
name := MyString("Sam Anderson") // name 的类型为 MyString
var v VowelsFinder // v 的类型为 VowelsFinder
v = name // 由于 MyString 实现了 VowelFinder,因此赋值合法
fmt.Printf("Vowels are %c", v.FindVowels())
// v.FindVowels() 调用了 MyString 类型的 FindVowels 方法
}
在上面的程序里,如果使用 name.FindVowels()
,而不是 v.FindVowels()
,程序依然能够照常运行,因此接口并没有体现出实际价值。
接口的实际用途
编写一个简单程序,根据公司员工的个人薪资,计算公司的总支出。为了简单起见,我们假定支出的单位都是美元。
// 声明一个 SalaryCalculator 接口类型,它只有一个方法 CalculateSalary() int
type SalaryCalculator interface {
CalculateSalary() int
}
// 长期员工
type Permanent struct {
empId int
basicpay int
pf int
}
// 合同员工
type Contract struct {
empId int
basicpay int
}
// 长期员工(Permanent)的薪资是 basicpay 与 pf 相加之和
func (p Permanent) CalculateSalary() int {
return p.basicpay + p.pf
}
// 合同员工(Contract)只有基本工资 basicpay
func (c Contract) CalculateSalary() int {
return c.basicpay
}
// 由于 Permanent 和 Contract 都声明了 CalculateSalary 方法,因此它们都实现了 SalaryCalculator 接口。
// 总费用是通过对SalalCalculator切片进行迭代并求和各个员工的薪水而得出的
// totalExpense 方法接收一个 SalaryCalculator 接口的切片([]SalaryCalculator)作为参数
func totalExpense(s []SalaryCalculator) {
expense := 0
for _, v := range s {
expense = expense + v.CalculateSalary()
}
fmt.Printf("Total Expense Per Month $%d", expense) // $14050
}
func main() {
pemp1 := Permanent{1, 5000, 20}
pemp2 := Permanent{2, 6000, 30}
cemp1 := Contract{3, 3000}
employees := []SalaryCalculator{pemp1, pemp2, cemp1}
fmt.Println(employees) // [{1 5000 20} {2 6000 30} {3 3000}]
totalExpense(employees)
}
向 totalExpense
方法传递了一个包含 Permanent
和 Contact
类型的切片。通过调用不同类型对应的 CalculateSalary
方法,totalExpense
可以计算得到支出。
这样做最大的优点是:totalExpense
可以扩展新的员工类型,而不需要修改任何代码。假如公司增加了一种新的员工类型 Freelancer
,它有着不同的薪资结构。Freelancer
只需传递到 totalExpense
的切片参数中,无需 totalExpense
方法本身进行修改。只要 Freelancer
也实现了 SalaryCalculator
接口,totalExpense
就能够实现其功能。
接口的内部表示
可以把接口看作内部的一个元组 (type, value)
。 type
是接口底层的具体类型,而 value
是具体类型的值。
// Test 接口只有一个方法 Tester()
type Test interface {
Tester()
}
type MyFloat float64
// MyFloat 类型实现了 Test 接口
func (m MyFloat) Tester() {
fmt.Println(m)
}
func describe(t Test) {
fmt.Printf("Interface type %T value %vn", t, t)
}
func main() {
var t Test
f := MyFloat(89.7)
t = f // 现在 t 的具体类型为 MyFloat, 而 t 的值为 89.7
describe(t)
t.Tester()
}
空接口
没有包含方法的接口称为空接口。空接口表示为 interface{}
。由于空接口没有方法,因此所有类型都实现了空接口。
func describe(i interface{}) {
fmt.Printf("Type = %T, value = %vn", i, i)
}
func main() {
s := "Hello World"
describe(s) // Type = string, value = Hello World
i := 55
describe(i) // Type = int, value = 55
strt := struct {
name string
}{
name: "Naveen R",
}
describe(strt) // Type = struct { name string }, value = {Naveen R}
}
describe(i interface{})
函数接收空接口作为参数,因此,可以给这个函数传递任何类型。
类型断言
类型断言用于提取接口的底层值。
在语法 i.(T)
中,接口 i
的具体类型是 T
,该语法用于获得接口的底层值。
func assert(i interface{}) {
s := i.(int) // 使用语法 i.(int) 来提取 i 的底层 int 值
fmt.Println(s)
}
func main() {
var s interface{} = 56 // s 的具体类型是 int
assert(s) // 56
}
在上面程序中,如果把类型为 string
的 s
传递给了 assert
函数,试图从它提取出 int 值,则该程序会报错。
要解决该问题,可以使用以下语法:
v, ok := i.(T)
如果 i
的具体类型是 T
,那么 v
赋值为 i
的底层值,而 ok
赋值为 true
。
如果 i
的具体类型不是 T
,那么 ok
赋值为 false
,v
赋值为 T
类型的零值,此时程序不会报错。
func assert(i interface{}) {
v, ok := i.(int)
fmt.Println(v, ok)
}
func main() {
var s interface{} = 56
assert(s) // 56 true
var i interface{} = "Steven Paul"
assert(i) // 0 false
}
类型选择
类型选择用于将接口的具体类型与很多 case 语句所指定的类型进行比较。它与一般的 switch 语句类似。唯一的区别在于类型选择指定的是类型,而一般的 switch 指定的是值。
类型选择的语法类似于类型断言。类型断言的语法是 i.(T)
,而对于类型选择,类型 T
由关键字 type
代替。
func findType(i interface{}) {
switch i.(type) {
case string:
fmt.Printf("I am a string and my value is %sn", i.(string))
case int:
fmt.Printf("I am an int and my value is %dn", i.(int))
default:
fmt.Printf("Unknown typen")
}
}
func main() {
findType("Naveen") // I am a string and my value is Naveen
findType(77) // I am an int and my value is 77
findType(89.98) // Unknown type
}
switch i.(type)
表示一个类型选择
还可以将一个类型和接口相比较。如果一个类型实现了接口,那么该类型与其实现的接口就可以互相比较:
type Describer interface {
Describe()
}
type Person struct {
name string
age int
}
func (p Person) Describe() {
fmt.Printf("%s is %d years old", p.name, p.age)
}
func findType(i interface{}) {
switch v := i.(type) {
case Describer:
v.Describe()
default:
fmt.Printf("unknown typen")
}
}
func main() {
findType("Naveen") // unknown type
p := Person{
name: "Naveen R",
age: 25,
}
findType(p) // Naveen R is 25 years old
}
实现接口:指针接受者
上面做列举的例子都是值接收,本节讨论指针接收。
- 使用值接受者声明的方法,既可以用值来调用,也能用指针调用。
- 使用指针接受者声明的方法,只能用指针调用。
type Describer interface {
Describe()
}
type Person struct {
name string
age int
}
func (p Person) Describe() { // 使用值接受者实现
fmt.Printf("%s is %d years oldn", p.name, p.age)
}
type Address struct {
state string
country string
}
func (a *Address) Describe() { // 使用指针接受者实现
fmt.Printf("State %s Country %s", a.state, a.country)
}
func main() {
var d1 Describer
p1 := Person{"Sam", 25}
d1 = p1
d1.Describe() // Sam is 25 years old
p2 := Person{"James", 32}
d1 = &p2
d1.Describe() // James is 32 years old
var d2 Describer
a := Address{"Washington", "USA"}
// 错误用法:d2 = a, 因为 a 属于值类型,它并没有实现 Describer 接口
d2 = &a // 合法, 因为在第 22 行,Address 类型的指针实现了 Describer 接口
d2.Describe() // State Washington Country USA
}
我们曾经学习过,使用指针接受者的方法,无论指针还是值都可以调用它。那么为什么d2 = a
就不管用呢?
其原因是:对于使用指针接受者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。但接口中存储的具体值并不能取到地址,因此在第 45 行,对于编译器无法自动获取 a
的地址,于是程序报错。
实现多个接口
类型可以实现多个接口。
type SalaryCalculator interface {
DisplaySalary()
}
type LeaveCalculator interface {
CalculateLeavesLeft() int
}
type Employee struct {
firstName string
lastName string
basicPay int
pf int
totalLeaves int
leavesTaken int
}
func (e Employee) DisplaySalary() {
fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}
func (e Employee) CalculateLeavesLeft() int {
return e.totalLeaves - e.leavesTaken
}
func main() {
e := Employee {
firstName: "Naveen",
lastName: "Ramanathan",
basicPay: 5000,
pf: 200,
totalLeaves: 30,
leavesTaken: 5,
}
var s SalaryCalculator = e
s.DisplaySalary() // Naveen Ramanathan has salary $5200
var l LeaveCalculator = e
fmt.Println("nLeaves left =", l.CalculateLeavesLeft()) // Leaves left = 25
}
接口的嵌套
尽管 Go 语言没有提供继承机制,但可以通过嵌套其他的接口,创建一个新接口。
type SalaryCalculator interface {
DisplaySalary()
}
type LeaveCalculator interface {
CalculateLeavesLeft() int
}
type EmployeeOperations interface { // 嵌套
SalaryCalculator
LeaveCalculator
}
type Employee struct {
firstName string
lastName string
basicPay int
pf int
totalLeaves int
leavesTaken int
}
func (e Employee) DisplaySalary() {
fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}
func (e Employee) CalculateLeavesLeft() int {
return e.totalLeaves - e.leavesTaken
}
func main() {
e := Employee {
firstName: "Naveen",
lastName: "Ramanathan",
basicPay: 5000,
pf: 200,
totalLeaves: 30,
leavesTaken: 5,
}
var empOp EmployeeOperations = e
empOp.DisplaySalary() // Naveen Ramanathan has salary $5200
fmt.Println("nLeaves left =", empOp.CalculateLeavesLeft()) // Leaves left = 25
}
接口的零值
接口的零值是 nil
。对于值为 nil
的接口,其底层值和具体类型都为 nil
。
对于值为 nil
的接口,由于没有底层值和具体类型,当我们试图调用它的方法时,程序会产生 panic
异常。
17. 并发
并发和并行的区别
- 并发是指立即处理多个任务的能力。
- 并行是指同时处理多个任务。
假如有一个 web 浏览器,它有各种组件。其中两个分别是 web 页面的渲染区和从网上下载文件的下载器。
当浏览器在单核处理器中运行时,处理器会在浏览器的两个组件间进行上下文切换。它可能一会儿下载文件,一会儿对web 页面进行渲染。这就是并发。并发的进程从不同的时间点开始,分别交替运行。
当浏览器在一个多核处理器上运行,此时下载文件的组件和渲染 HTML 的组件可能会在不同的核上同时运行。这称之为并行。
并行不一定会加快运行速度,因为并行运行的组件之间可能需要相互通信。在浏览器的例子里,当文件下载完成后,应当对用户进行提醒,比如弹出一个窗口。于是,在负责下载的组件和负责渲染用户界面的组件之间,就产生了通信。在并发系统上,这种通信开销很小。但在多核的并行系统上,组件间的通信开销就很高了。所以,并行不一定会加快运行速度!
Go 编程语言原生支持并发。
Go 使用 Go 协程(Goroutine) 和信道(Channel)来实现并发。
18. Go协程
Go 协程是什么?
Go 协程是与其他函数或方法一起并发运行的函数或方法。
Go 协程可以看作是轻量级线程。与线程相比,创建一个 Go 协程的成本很小。因此在 Go 应用中,常常会看到有数以千计的 Go 协程并发运行。
Go 协程相比于线程的优势
- 相比线程而言,Go 协程的成本极低。堆栈大小只有若干 kb,并且可以根据应用的需求进行增减。而线程必须指定堆栈的大小,其堆栈是固定不变的。
- Go 协程会复用(Multiplex)数量更少的 OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。所有这一切都在运行时进行,作为程序员,我们没有直接面临这些复杂的细节,而是有一个简洁的 API 来处理并发。
- Go 协程使用信道(Channel)来进行通信。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。信道可以看作是 Go 协程之间通信的管道。
如何启动一个 Go 协程?
调用函数或者方法时,在前面加上关键字 go
,可以让一个新的 Go 协程并发地运行。
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
fmt.Println("main function")
}
go hello()
启动了一个新的 Go 协程。
现在 hello()
函数与 main()
函数会并发地执行。
主函数会运行在一个特有的 Go 协程上,它称为 Go 主协程(Main Goroutine)。
然而:
该程序只会输出文本 main function
。我们启动的 Go 协程究竟出现了什么问题?要理解这一切,我们需要理解两个 Go 协程的主要性质。
- 启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。
- 如果希望运行其他 Go 协程,Go 主协程必须继续运行着。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也不会继续运行。
因此,在调用了 go hello()
之后,程序控制没有等待 hello()
协程结束,立即返回到了代码下一行,打印 main function
。接着由于没有其他可执行的代码,Go 主协程终止,于是 hello()
协程就没有机会运行了。
解决方法:信道可用于在其他协程结束执行之前,阻塞 Go 主协程。
启动多个 Go 协程
func numbers() {
for i := 1; i <= 5; i++ {
time.Sleep(250 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func alphabets() {
for i := 'a'; i <= 'e'; i++ {
time.Sleep(400 * time.Millisecond)
fmt.Printf("%c ", i)
}
}
func main() {
go numbers()
go alphabets()
time.Sleep(3000 * time.Millisecond)
fmt.Println("nmain terminated")
}
打印结果:
1 a 2 3 b 4 c 5 d e
main terminated
程序的运作如下图所示。为了更好地观看图片,请在新标签页中打开。
19. 信道
什么是信道?
信道可以想像成 Go 协程之间通信的管道。
通过使用信道,数据也可以从一端发送,在另一端接收。
信道的声明
所有信道都关联了一个类型。信道只能运输这种类型的数据,而运输其他类型的数据都是非法的。
chan T
表示 T
类型的信道。
信道的零值为 nil
。信道的零值没有什么用,应该像对 map 和切片所做的那样,用 make
来定义信道。
func main() {
var a chan int
if a == nil {
fmt.Println("channel a is nil") // channel a is nil
a = make(chan int)
fmt.Printf("Type of a is %T", a) // Type of a is chan int
}
}
Or:
a := make(chan int)
通过信道进行发送和接收
如下所示,该语法通过信道发送和接收数据。
data := <- a // 读取信道 a 的数据赋值给 data
a <- data // 将 data 写入信道 a
发送与接收默认是阻塞的。
- 当把数据发送到信道时,程序控制会在发送数据的语句处发生阻塞,直到有其它 Go 协程从信道读取到数据,才会解除阻塞。
- 当读取信道的数据时,如果没有其它的协程把数据写入到这个信道,那么读取过程就会一直阻塞着。
信道的这种特性能够帮助 Go 协程之间进行高效的通信,不需要用到其他编程语言常见的显式锁或条件变量。
信道的代码示例
示例一:
func hello(done chan bool) {
fmt.Println("Hello world goroutine")
done <- true
}
func main() {
done := make(chan bool) // 创建了一个 bool 类型的信道 done
go hello(done) // 把 done 作为参数传递给了 hello() 协程
<- done // 在 hello 向 done 写入数据之前阻塞
fmt.Println("main function")
}
<- done
这行代码通过信道done
接收数据,但并没有使用数据或者把数据存储到变量中,是合法的。
示例二:
计算一个数中每一位的平方和与立方和,然后把平方和与立方和相加并打印出来。
例如,如果输出是 123,该程序会如下计算输出:
squares = (1 * 1) + (2 * 2) + (3 * 3)
cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3)
output = squares + cubes = 50
func calcSquares(number int, squareop chan int) {
sum := 0
for number != 0 {
digit := number % 10
sum += digit * digit
number /= 10
}
squareop <- sum
}
func calcCubes(number int, cubeop chan int) {
sum := 0
for number != 0 {
digit := number % 10
sum += digit * digit * digit
number /= 10
}
cubeop <- sum
}
func main() {
number := 123
sqrch := make(chan int)
cubech := make(chan int)
go calcSquares(number, sqrch)
go calcCubes(number, cubech)
squares, cubes := <-sqrch, <-cubech
fmt.Println("Final output", squares + cubes) // 50
}
死锁
使用信道需要考虑的一个重点是死锁。
当 Go 协程给一个信道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic,形成死锁。
同理,当有 Go 协程等着从一个信道接收数据时,我们期望其他的 Go 协程会向该信道写入数据,要不然程序就会触发 panic。
单向信道
目前讨论的信道都是双向信道,即通过信道既能发送数据,又能接收数据。其实也可以创建单向信道,这种信道只能发送或者接收数据。
关闭信道
语法:
close(chnl)
数据发送方可以关闭信道,通知接收方这个信道不再有数据发送过来。
当从信道接收数据时,接收方可以多用一个变量来检查信道是否已经关闭。
v, ok := <- ch
-
如果
ok
等于 true,那么可成功接收信道所发送的数据 -
如果
ok
等于 false,说明该信道关闭,接收值是该信道类型的零值
使用 for range 遍历信道
func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
// 不使用 for range
for {
v, ok := <-ch
if ok == false {
break
}
fmt.Println("Received ", v, ok)
}
// 使用 for range
for v := range ch {
fmt.Println("Received ",v)
}
}
20. 缓冲信道和工作池
在《19. 信道》里详细讨论了无缓冲信道,无缓冲信道的发送和接收过程是阻塞的。
什么是缓冲信道?
只在缓冲已满的情况,才会阻塞向缓冲信道发送数据。同样,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据。
通过向 make
函数再传递一个表示容量的参数(指定缓冲的大小),可以创建缓冲信道。
ch := make(chan type, capacity)
func write(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
fmt.Println("successfully wrote", i, "to ch")
}
close(ch)
}
func main() {
ch := make(chan int, 2)
go write(ch)
time.Sleep(2 * time.Second)
for v := range ch {
fmt.Println("read value", v,"from ch")
time.Sleep(2 * time.Second)
}
}
运行结果
successfully wrote 1 to ch
read value 0 from ch
successfully wrote 2 to ch
read value 1 from ch
successfully wrote 3 to ch
read value 2 from ch
successfully wrote 4 to ch
read value 3 from ch
read value 4 from ch
长度 vs 容量
缓冲信道的容量是指信道可以存储的值的数量。在使用 make
函数创建缓冲信道的时候会指定容量大小。语法:cap(ch)
缓冲信道的长度是指信道中当前排队的元素个数。语法:len(ch)
WaitGroup
WaitGroup
用于等待一批 Go 协程执行结束。程序控制会一直阻塞,直到这些协程全部执行完毕。
假设有 3 个并发执行的 Go 协程(由 Go 主协程生成)。Go 主协程需要等待这 3 个协程执行结束后,才会终止。这就可以用 WaitGroup
来实现。
import (
"fmt"
"sync"
"time"
)
func process(i int, wg *sync.WaitGroup) {
fmt.Println("started Goroutine ", i)
time.Sleep(2 * time.Second)
fmt.Printf("Goroutine %d endedn", i)
wg.Done()
}
func main() {
no := 3
var wg sync.WaitGroup
for i := 0; i < no; i++ {
wg.Add(1)
go process(i, &wg)
}
wg.Wait()
fmt.Println("All go routines finished executing")
}
打印结果
started Goroutine 2
started Goroutine 1
started Goroutine 0
Goroutine 0 ended
Goroutine 2 ended
Goroutine 1 ended
All go routines finished executing
WaitGroup 是一个结构体类型,使用计数器工作:
var wg sync.WaitGrou
,创建了WaitGroup
类型的变量,其初始值为零值。wg.Add(x)
:计数加 xwg.Done()
:计数减 1wg.Wait()
:阻塞,直到计数器变为 0 后才会停止阻塞。
工作池的实现
缓冲信道的重要应用之一就是实现工作池。
一般而言,工作池就是一组等待任务分配的线程。一旦完成了所分配的任务,这些线程可继续等待任务的分配。
接下来,使用缓冲信道来实现工作池。工作池的任务是计算所输入数字的每一位的和。例如,如果输入 234,结果会是 9(即 2 + 3 + 4)。向工作池输入的是一列伪随机数。
工作池的核心功能如下:
- 创建一个 Go 协程池,监听一个等待作业分配的输入型缓冲信道。
- 将作业添加到该输入型缓冲信道中。
- 作业完成后,再将结果写入一个输出型缓冲信道。
- 从输出型缓冲信道读取并打印结果。
-
创建一个结构体,表示作业和结果。
type Job struct { id int randomno int } type Result struct { job Job sumofdigits int }
-
分别创建用于接收作业和写入结果的缓冲信道。
var jobs = make(chan Job, 10) var results = make(chan Result, 10)
工作协程(Worker Goroutine)会监听缓冲信道
jobs
里更新的作业。一旦工作协程完成了作业,其结果会写入缓冲信道results
。 -
digits
函数的任务是计算整数的每一位之和func digits(number int) int { sum := 0 no := number for no != 0 { digit := no % 10 sum += digit no /= 10 } time.Sleep(2 * time.Second) return sum }
-
woker()
函数创建工作协程。func worker(wg *sync.WaitGroup) { for job := range jobs { output := Result{job, digits(job.randomno)} results <- output } wg.Done() }
上面的函数创建了一个工作者(Worker),读取
jobs
信道的数据,根据当前的job
和digits
函数的返回值,创建Result
结构体变量,然后将结果写入results
缓冲信道。worker
函数接收了一个WaitGroup
类型的wg
作为参数,当所有的jobs
完成的时候,调用了Done()
方法。 -
createWorkerPool
函数创建了一个 Go 协程的工作池。func createWorkerPool(noOfWorkers int) { var wg sync.WaitGroup for i := 0; i < noOfWorkers; i++ { wg.Add(1) go worker(&wg) } wg.Wait() close(results) }
上面函数的参数是需要创建的工作协程的数量。在创建 Go 协程之前,它调用了
wg.Add(1)
方法,于是WaitGroup
计数器递增。接下来,创建工作协程,并向worker
函数传递wg
的地址。创建了需要的工作协程后,函数调用wg.Wait()
,等待所有的 Go 协程执行完毕。所有协程完成执行之后,函数会关闭results
信道。因为所有协程都已经执行完毕,于是不再需要向results
信道写入数据了。 -
函数
allocate
把作业分配给工作者func allocate(noOfJobs int) { for i := 0; i < noOfJobs; i++ { randomno := rand.Intn(999) job := Job{i, randomno} jobs <- job } close(jobs) }
上面的
allocate
函数的接收参数是所需创建的作业数量,生成了最大值为 998 的伪随机数,并使用该随机数创建了Job
结构体变量。这个函数把 for 循环的计数器i
作为 id,最后把创建的结构体变量写入jobs
信道。当写入所有的job
之后关闭jobs
信道。 -
创建一个读取
results
信道和打印输出的函数。func result(done chan bool) { for result := range results { fmt.Printf("Job id %d, input random no %d , sum of digits %dn", result.job.id, result.job.randomno, result.sumofdigits) } done <- true }
result
函数读取results
信道,并打印出job
的id
、输入的随机数、该随机数的每位数之和。result
函数也接受done
信道作为参数,当打印所有结果时,done
会被写入 true。 -
在
main()
函数中调用上面所有的函数。func main() { startTime := time.Now() noOfJobs := 100 go allocate(noOfJobs) done := make(chan bool) go result(done) noOfWorkers := 10 createWorkerPool(noOfWorkers) <-done endTime := time.Now() diff := endTime.Sub(startTime) fmt.Println("total time taken ", diff.Seconds(), "seconds") }
首先把
noOfJobs
设置为 100,接下来调用了allocate
,向jobs
信道添加作业。接着创建
done
信道,并将其传递给result
协程。于是该协程会开始打印结果,并在完成打印时发出通知。通过调用
createWorkerPool
函数创建了一个有 10 个协程的工作池。main
函数会监听done
信道的通知,等待所有结果打印结束。
Totol:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type Job struct {
id int
randomno int
}
type Result struct {
job Job
sumofdigits int
}
var jobs = make(chan Job, 10)
var results = make(chan Result, 10)
func digits(number int) int {
sum := 0
no := number
for no != 0 {
digit := no % 10
sum += digit
no /= 10
}
time.Sleep(2 * time.Second)
return sum
}
/* 创建 工作协程 */
func worker(wg *sync.WaitGroup) {
for job := range jobs { // 监听 jobs 作业缓冲信道
output := Result{job, digits(job.randomno)}
results <- output // 把计算结果写入 results 信道
}
wg.Done()
}
/* 创建 工作池 */
func createWorkerPool(noOfWorkers int) {
var wg sync.WaitGroup // wg 的作用是确保工作池中的协程运行完
for i := 0; i < noOfWorkers; i++ { // 每个工作池中并行 noOfWorkers 个协程
wg.Add(1)
go worker(&wg)
}
wg.Wait()
close(results)
}
/* 把作业分配给工作者 */
func allocate(noOfJobs int) {
for i := 0; i < noOfJobs; i++ {
randomno := rand.Intn(999)
job := Job{i, randomno}
jobs <- job // 把作业添加到 jobs 缓冲信道中
}
close(jobs)
}
/* 读取 results 信道和打印输出 */
func result(done chan bool) {
for result := range results { // 监听 results 信道, 打印结果
fmt.Printf("Job id %d, input random no %d , sum of digits %dn", result.job.id, result.job.randomno, result.sumofdigits)
}
done <- true
}
func main() {
startTime := time.Now()
noOfJobs := 100 // 作业数量
go allocate(noOfJobs) // 把100个作业分配给100个工作者
done := make(chan bool)
go result(done)
noOfWorkers := 20
createWorkerPool(noOfWorkers)
<- done
endTime := time.Now()
diff := endTime.Sub(startTime)
fmt.Println("total time taken ", diff.Seconds(), "seconds")
}
21. select
什么是 select?
select
语句用于在多个发送/接收信道操作中进行选择。在发送/接收操作准备就绪之前,select
语句会一直阻塞。
如果有信道操作准备完毕,select
会随机地选取其中之一执行。
该语法与 switch
类似,只是这里的每个 case
语句都是信道操作。
func server1(ch chan string) {
time.Sleep(6 * time.Second)
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
select { // select 会一直发生阻塞,直到有 case 准备就绪。
case s1 := <- output1:
fmt.Println(s1)
case s2 := <- output2:
fmt.Println(s2)
}
}
运行结果:
from server2
由于server2()
只休眠3秒就往信道中写入了消息,即信道output2
中先有了消息,因此出现了上述结果。
select 的应用
在上面程序中,函数之所以取名为 server1
和 server2
,是为了展示 select
的实际应用。
假设有一个关键性应用,需要尽快地把输出返回给用户。这个应用的数据库复制并且存储在世界各地的服务器上。假设函数 server1
和 server2
与这样不同区域的两台服务器进行通信。每台服务器的负载和网络时延决定了它的响应时间。我们向两台服务器发送请求,并使用 select
语句等待相应的信道返回响应。select
会选择首先响应的服务器,而忽略其它的响应。
使用这种方法,可以向多个服务器发送请求,并给用户返回最快的响应了。:)
默认情况 default
- 在没有 case 准备就绪时,将执行
select
语句中的默认情况 ch
等于nil
时,将执行select
语句中的默认情况
这通常用于防止 select
语句一直阻塞。
func process(ch chan string) {
time.Sleep(5500 * time.Millisecond)
ch <- "process successful"
}
func main() {
ch := make(chan string)
go process(ch)
for {
time.Sleep(1000 * time.Millisecond)
select {
case v := <-ch:
fmt.Println("received value: ", v)
return
default:
fmt.Println("no value received")
}
}
}
打印结果:
no value received
no value received
no value received
no value received
no value received
received value: process successful
随机选取
当 select
由多个 case 准备就绪时,将会随机地选取其中之一去执行。
func server1(ch chan string) {
ch <- "from server1"
}
func server2(ch chan string) {
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
time.Sleep(1 * time.Second)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
空 select
package main
func main() {
select {}
}
除非有 case 执行,否则 select 语句会一直阻塞。在这里,select
语句没有任何 case,因此它会一直阻塞,导致死锁。
22. mutex(互斥)
临界区
在学习 Mutex 之前,我们需要理解并发编程中临界区(Critical Section)
的概念。当程序并发地运行时,多个 Go 协程不应该同时访问那些修改共享资源的代码。这些修改共享资源的代码称为临界区。
例如,假设有一段代码,将一个变量 x
自增 1。
x = x + 1
如果只有一个 Go 协程访问上面的代码段,那没有任何问题。
但当有多个协程并发运行时,代码却会出错。
在代码的内部,系统执行程序时分为如下几个步骤:
- 获得 x 的当前值
- 计算 x + 1
- 将步骤 2 计算得到的值赋值给 x
如果只有一个协程执行上面的三个步骤,不会有问题。
我们讨论一下当有两个并发的协程执行该代码时,会发生什么。下图描述了当两个协程并发地访问代码行 x = x + 1
时,可能出现的一种情况。
我们假设 x
的初始值为 0。而协程 1 获取 x
的初始值,并计算 x + 1
。而在协程 1 将计算值赋值给 x
之前,系统上下文切换到了协程 2。于是,协程 2 获取了 x
的初始值(依然为 0),并计算 x + 1
。接着系统上下文又切换回了协程 1。现在,协程 1 将计算值 1 赋值给 x
,因此 x
等于 1。然后,协程 2 继续开始执行,把计算值(依然是 1)复制给了 x
,因此在所有协程执行完毕之后,x
都等于 1。
现在我们考虑另外一种可能发生的情况。
在上面的情形里,协程 1 开始执行,完成了三个步骤后结束,因此 x
的值等于 1。接着,开始执行协程 2。目前 x
的值等于 1。而当协程 2 执行完毕时,x
的值等于 2。
所以,从这两个例子你可以发现,根据上下文切换的不同情形,x
的最终值是 1 或者 2。这种不太理想的情况称为竞态条件(Race Condition),其程序的输出是由协程的执行顺序决定的。
在上例中,如果在任意时刻只允许一个 Go 协程访问临界区,那么就可以避免竞态条件。
Mutex
Mutex 用于提供一种加锁机制(Locking Mechanism),可确保在某时刻只有一个协程在临界区运行,以防止出现竞态条件。
Mutex 可以在 sync 包内找到。Mutex 定义了两个方法:Lock 和 Unlock。所有在 Lock
和 Unlock
之间的代码,都只能由一个 Go 协程执行,于是就可以避免竞态条件。
mutex.Lock()
x = x + 1
mutex.Unlock()
在上面的代码中,x = x + 1
只能由一个 Go 协程执行,因此避免了竞态条件。
如果有一个 Go 协程已经持有了锁(Lock),当其他协程试图获得该锁时,这些协程会被阻塞,直到 Mutex 解除锁定为止。
含有竞态条件的程序
编写一个含有竞态条件的程序:
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup) {
x = x + 1
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w)
}
w.Wait()
fmt.Println("final value of x", x)
}
这段程序由于竞态条件,每一次输出都不同。
使用 Mutex 处理竞态条件
在上一个程序中创建了 1000 个 Go 协程。如果每个协程对 x
加 1,最终 x
期望的值应该是 1000。
需要在程序里使用 Mutex,修复竞态条件的问题。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x + 1
m.Unlock()
wg.Done()
}
func main() {
var w sync.WaitGroup
var m sync.Mutex
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, &m)
}
w.Wait()
fmt.Println("final value of x", x)
}
Mutex 是一个结构体类型,increment(&w, &m)
传递 Mutex 的地址很重要。如果传递的是 Mutex 的值,而非地址,那么每个协程都会得到 Mutex 的一份拷贝,竞态条件还是会发生。
使用 信道 处理竞态条件
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
ch <- true
x = x + 1
<- ch
wg.Done()
}
func main() {
var w sync.WaitGroup
ch := make(chan bool, 1)
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, ch)
}
w.Wait()
fmt.Println("final value of x", x)
}
由于缓冲信道的容量为 1,所以任何其他协程试图写入该信道时,都会发生阻塞。
Mutex vs 信道
Mutex 和信道,都可以解决竞态条件的问题。
当 Go 协程需要与其他协程通信时,可以使用信道。而当只允许一个协程访问临界区时,可以使用 Mutex。
23. 结构体取代类
Go 支持面向对象吗?
Go 并不是完全面向对象的编程语言。虽然 Go 有类型和方法,支持面向对象的编程风格,但却没有类型的层次结构。
结构体 & 类
Go 不支持类,而是提供了结构体。结构体中可以添加方法。这样可以将数据和操作数据的方法绑定在一起,实现与类相似的效果。
现在目录结构如下所示:
workspacepath -> oop -> employee -> employee.go
workspacepath -> oop -> main.go
employee.go
的代码:
package employee
import (
"fmt"
)
type Employee struct {
FirstName string
LastName string
TotalLeaves int
LeavesTaken int
}
func (e Employee) LeavesRemaining() {
fmt.Printf("%s %s has %d leaves remaining", e.FirstName, e.LastName, (e.TotalLeaves - e.LeavesTaken))
}
main.go
的代码:
package main
import "oop/employee"
func main() {
e := employee.Employee {
FirstName: "Sam",
LastName: "Adolf",
TotalLeaves: 30,
LeavesTaken: 20,
}
e.LeavesRemaining()
}
打印结果
Sam Adolf has 10 leaves remaining
New() 函数 & 构造器
上面的程序看起来没什么问题,但还是有一些细节问题需要注意。
当定义一个零值的 employee
结构体变量时,
package main
import "oop/employee"
func main() {
var e employee.Employee
e.LeavesRemaining()
}
打印结果:
has 0 leaves remaining
NewT(parameters)
函数,初始化 T
类型的变量
employee.go
的代码:
package employee
import (
"fmt"
)
type employee struct { // Employee 结构体的首字母改为小写 e, 变成不可引用, 防止其他包对它的访问。
firstName string
lastName string
totalLeaves int
leavesTaken int
}
func New(firstName string, lastName string, totalLeave int, leavesTaken int) employee {
e := employee {firstName, lastName, totalLeave, leavesTaken}
return e
}
func (e employee) LeavesRemaining() {
fmt.Printf("%s %s has %d leaves remaining", e.firstName, e.lastName, (e.totalLeaves - e.leavesTaken))
}
提供了一个可引用的 New
函数,该函数接收必要的参数,返回一个新创建的 employee
结构体变量。
main.go
的代码:
package main
import "oop/employee"
func main() {
e := employee.New("Sam", "Adolf", 30, 20)
e.LeavesRemaining()
}
由此,虽然 Go 不支持类,但结构体能够很好地取代类,而以 New(parameters)
签名的方法可以替代构造器。
24. 组合取代继承
Go 不支持继承,但它支持组合(Composition)
通过嵌套结构体实现组合
在 Go 中,通过在结构体内嵌套结构体,可以实现组合。
组合的典型例子就是博客帖子,每一个博客的帖子都有标题、内容和作者信息。
-
首先创建一个
author
结构体。type author struct { firstName string lastName string bio string } func (a author) fullName() string { return fmt.Sprintf("%s %s", a.firstName, a.lastName) }
-
创建
post
结构体。type post struct { title string content string author // 匿名字段 } func (p post) details() { fmt.Println("Title: ", p.title) fmt.Println("Content: ", p.content) fmt.Println("Author: ", p.author.fullName()) // 可替换 fmt.Println("Bio: ", p.author.bio) }
post
结构体的字段有title
和content
。它还有一个嵌套的匿名字段author
。post
可以访问author
结构体的所有字段和方法。因此p.author.fullName()
可以替换为p.fullName()
。 -
在
main
函数中创建一个博客帖子。package main import ( "fmt" ) type author struct { firstName string lastName string bio string } func (a author) fullName() string { return fmt.Sprintf("%s %s", a.firstName, a.lastName) } type post struct { title string content string author } func (p post) details() { fmt.Println("Title: ", p.title) fmt.Println("Content: ", p.content) fmt.Println("Author: ", p.fullName()) fmt.Println("Bio: ", p.bio) } func main() { author1 := author{ "Naveen", "Ramanathan", "Golang Enthusiast", } post1 := post{ "Inheritance in Go", "Go supports composition instead of inheritance", author1, } post1.details()
打印结果:
Title: Inheritance in Go
Content: Go supports composition instead of inheritance
Author: Naveen Ramanathan
Bio: Golang Enthusiast
结构体切片的嵌套
进一步处理上面的示例,使用博客帖子的切片来创建一个网站
首先定义 website
结构体。
type website struct { // 结构体不能嵌套一个匿名切片
posts []post
}
func (w website) contents() {
fmt.Println("Contents of Websiten")
for _, v := range w.posts {
v.details()
fmt.Println()
}
}
package main
import (
"fmt"
)
type author struct {
firstName string
lastName string
bio string
}
func (a author) fullName() string {
return fmt.Sprintf("%s %s", a.firstName, a.lastName)
}
type post struct {
title string
content string
author
}
func (p post) details() {
fmt.Println("Title: ", p.title)
fmt.Println("Content: ", p.content)
fmt.Println("Author: ", p.fullName())
fmt.Println("Bio: ", p.bio)
}
type website struct {
posts []post
}
func (w website) contents() {
fmt.Println("Contents of Websiten")
for _, v := range w.posts {
v.details()
fmt.Println()
}
}
func main() {
author1 := author{
"Naveen",
"Ramanathan",
"Golang Enthusiast",
}
post1 := post{
"Inheritance in Go",
"Go supports composition instead of inheritance",
author1,
}
post2 := post{
"Struct instead of Classes in Go",
"Go does not support classes but methods can be added to structs",
author1,
}
post3 := post{
"Concurrency",
"Go is a concurrent language and not a parallel one",
author1,
}
w := website{
posts: []post{post1, post2, post3},
}
w.contents()
}
打印结果:
Contents of Website Title: Inheritance in Go Content: Go supports composition instead of inheritance Author: Naveen Ramanathan Bio: Golang Enthusiast Title: Struct instead of Classes in Go Content: Go does not support classes but methods can be added to structs Author: Naveen Ramanathan Bio: Golang Enthusiast Title: Concurrency Content: Go is a concurrent language and not a parallel one Author: Naveen Ramanathan Bio: Golang Enthusiast
25. 多态
Go 通过接口来实现多态。 Go 语言是隐式地实现接口,一个类型如果定义了接口所声明的全部方法,那它就实现了该接口。
使用接口实现多态
所有实现了接口的类型,都可以把它的值保存在一个接口类型的变量中。因此可以使用接口的这种特性来实现多态。
package main
import (
"fmt"
)
type Income interface { // 接口
calculate() int
source() string
}
type FixedBilling struct {
projectName string
biddedAmount int
}
type TimeAndMaterial struct {
projectName string
noOfHours int
hourlyRate int
}
func (fb FixedBilling) calculate() int {
return fb.biddedAmount
}
func (fb FixedBilling) source() string {
return fb.projectName
}
func (tm TimeAndMaterial) calculate() int {
return tm.noOfHours * tm.hourlyRate
}
func (tm TimeAndMaterial) source() string {
return tm.projectName
}
func calculateNetIncome(ic []Income) {
var netincome int = 0
for _, income := range ic {
fmt.Printf("Income From %s = $%dn", income.source(), income.calculate())
netincome += income.calculate()
}
fmt.Printf("Net income of organisation = $%d", netincome)
}
func main() {
project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
// 所有实现了接口的类型,都可以把它的值保存在一个接口类型的变量中
incomeStreams := []Income{project1, project2, project3}
calculateNetIncome(incomeStreams)
}
新增收益流
假设前面的组织通过广告业务,建立了一个新的收益流(Income Stream)
我们首先定义 Advertisement
类型,并在 Advertisement
类型中定义 calculate()
和 source()
方法。
type Advertisement struct {
adName string
CPC int // 每次点击成本
noOfClicks int // 点击次数
}
func (a Advertisement) calculate() int {
return a.CPC * a.noOfClicks
}
func (a Advertisement) source() string {
return a.adName
}
稍微修改一下 main
函数,把新的收益流添加进来。
func main() {
project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
bannerAd := Advertisement{adName: "Banner Ad", CPC: 2, noOfClicks: 500}
popupAd := Advertisement{adName: "Popup Ad", CPC: 5, noOfClicks: 750}
incomeStreams := []Income{project1, project2, project3, bannerAd, popupAd}
calculateNetIncome(incomeStreams)
}
尽管新增了收益流,但却完全没有修改 calculateNetIncome
函数。这就是多态的好处。
由于新的 Advertisement
同样实现了 Income
接口,所以能够向 incomeStreams
切片添加 Advertisement
。
calculateNetIncome
无需修改,因为它能够调用 Advertisement
类型的 calculate()
和 source()
方法。
26. defer(延迟)
延迟函数
defer
语句的用途:含有 defer
语句的函数,会在该函数返回之前,调用另一个函数。
func finished() {
fmt.Println("Finished finding largest")
}
func largest(nums []int) {
defer finished()
fmt.Println("Started finding largest")
max := nums[0]
for _, v := range nums {
if v > max {
max = v
}
}
fmt.Println("Largest number in", nums, "is", max)
}
func main() {
nums := []int{78, 109, 2, 563, 300}
largest(nums)
}
打印结果
Started finding largest Largest number in [78 109 2 563 300] is 563 Finished finding largest
largest()
函数的第一行的语句为 defer finished()
,表示在largest()
函数返回之前会调用 finished()
函数。
延迟方法
defer
不仅可以调用函数,也可以调用方法。
type person struct {
firstName string
lastName string
}
func (p person) fullName() {
fmt.Printf("%s %s",p.firstName,p.lastName)
}
func main() {
p := person {
firstName: "John",
lastName: "Smith",
}
defer p.fullName()
fmt.Printf("Welcome ")
}
打印结果
Welcome John Smith
实参取值
在 Go 语言中,不是在调用延迟函数时才确定实参,而是当执行 defer
语句的时候,就确定实参。
func printA(a int) {
fmt.Println("value of a in deferred function", a)
}
func main() {
a := 5
defer printA(a)
a = 10
fmt.Println("value of a before deferred function call", a)
}
打印结果
value of a before deferred function call 10
value of a in deferred function 5
defer 栈
当一个函数内多次调用 defer
时,Go 会把 defer
调用放入到一个栈中,随后按照后进先出的顺序执行。
func main() {
name := "Naveen"
for _, v := range []rune(name) {
defer fmt.Printf("%c", v)
}
}
打印结果
neevaN
defer 的实际应用
当一个函数应该在与当前代码流(Code Flow)无关的环境下调用时,可以使用 defer
。
首先写一个没有使用 defer
的程序,然后用 defer
来修改,看到 defer
的好处:
type rect struct {
length int
width int
}
func (r rect) area(wg *sync.WaitGroup) {
if r.length < 0 {
fmt.Printf("rect %v's length should be greater than zeron", r)
wg.Done()
return
}
if r.width < 0 {
fmt.Printf("rect %v's width should be greater than zeron", r)
wg.Done()
return
}
area := r.length * r.width
fmt.Printf("rect %v's area %dn", r, area)
wg.Done()
}
func main() {
var wg sync.WaitGroup
r1 := rect{-67, 89}
r2 := rect{5, -67}
r3 := rect{8, 9}
rects := []rect{r1, r2, r3}
for _, v := range rects {
wg.Add(1)
go v.area(&wg)
}
wg.Wait()
fmt.Println("All finished executing")
}
执行结果
rect {8 9}'s area 72
rect {5 -67}'s width should be greater than zero
rect {-67 89}'s length should be greater than zero
All finished executing
仔细观察,会发现 wg.Done()
只要在 area()
函数返回的时候就会调用。
因此可以只调用一次 defer
,来替换掉 wg.Done()
的多次调用。
func (r rect) area(wg *sync.WaitGroup) {
defer wg.Done()
if r.length < 0 {
fmt.Printf("rect %v's length should be greater than zeron", r)
return
}
if r.width < 0 {
fmt.Printf("rect %v's width should be greater than zeron", r)
return
}
area := r.length * r.width
fmt.Printf("rect %v's area %dn", r, area)
}
27. 错误处理
什么是错误?
错误表示程序中出现了异常情况。比如当试图打开一个文件时,文件系统里却并没有这个文件。
在 Go 中,错误用内建的 error
类型来表示。
就像其他的内建类型(如 int
、float64
等),错误值可以存储在变量里、作为函数的返回值。
示例
编写一个示例,试图打开一个并不存在的文件。
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("/test.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(f.Name(), "opened successfully")
}
运行结果
open /test.txt: No such file or directory
os
包里的 Open
函数有如下签名:
func Open(name string) (file *File, err error)
- 如果成功打开文件,
Open
函数会返回一个文件句柄(File Handler)和一个值为nil
的错误。 - 如果打开文件时发生了错误,会返回一个不等于
nil
的错误。
如果一个函数 或方法返回了错误,按照惯例,错误会作为最后一个值返回。( Open
函数也是将 err
作为最后一个返回值)
按照 Go 的惯例,在处理错误时,通常都是将返回的错误与 nil
比较:
-
nil
值表示了没有错误 -
非
nil
值表示出现了错误
错误类型的表示
error
是一个接口类型,定义如下:
type error interface {
Error() string
}
error
有了一个签名为 Error() string
的方法。所有实现该接口的类型都可以当作一个错误类型。Error()
方法给出了错误的描述。
fmt.Println
在打印错误时,会在内部调用 Error() string
方法来得到该错误的描述。
自定义错误
创建自定义错误最简单的方法是使用 errors
包中的 New
函数。
在使用 New
函数创建自定义错误之前,我们先来看看 New
是如何实现的。如下所示,是 errors
包中的 New
函数的实现。
// errors 包实现了处理错误的功能
package errors
// 创建了 errorString 类型的变量,并返回了它的地址。
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
创建一个计算圆半径的简单程序,如果半径为负,它会返回一个错误:
package main
import (
"errors"
"fmt"
"math"
)
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, errors.New("Area calculation failed, radius is less than zero")
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Area of circle %0.2f", area)
}
给错误添加更多信息
方法一:使用Errorf
如何让上面的程序能够打印出当前圆的半径?
这就要用到 fmt
包中的 Errorf
函数了。Errorf
函数会根据格式说明符,规定错误的格式,并返回一个符合该错误的字符串。
import (
"fmt"
"math"
)
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Area of circle %0.2f", area)
}
方法二:使用结构体类型和字段
-
创建一个表示错误的结构体类型,保存错误有关信息,错误类型的命名约定是名称以
Error
结尾。type areaError struct { err string // 存储实际的错误信息 radius float64 // 存储与错误有关的半径 }
-
实现
error
接口。func (e *areaError) Error() string { return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err) }
使用指针接收者
*areaError
,实现了error
接口的Error() string
方法。该方法打印出半径和关于错误的描述。 -
完整程序
package main import ( "fmt" "math" ) type areaError struct { err string radius float64 } func (e *areaError) Error() string { return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err) } func circleArea(radius float64) (float64, error) { if radius < 0 { return 0, &areaError{"radius is negative", radius} } return math.Pi * radius * radius, nil } func main() { radius := -20.0 area, err := circleArea(radius) // 使用结构体类型和字段 if err != nil { if err, ok := err.(*areaError); ok { fmt.Printf("Radius %0.2f is less than zero", err.radius) return } fmt.Println(err) return } fmt.Printf("Area of rectangle1 %0.2f", area) }
方法三:使用结构体类型的方法
编写一个计算矩形面积的程序。如果长或宽小于零,程序就会打印出错误。
-
创建一个表示错误的结构体。
type areaError struct { err string length float64 width float64 }
-
实现
error
接口,并给该错误类型添加两个方法,使它提供了更多的错误信息。func (e *areaError) Error() string { return e.err } func (e *areaError) lengthNegative() bool { return e.length < 0 } func (e *areaError) widthNegative() bool { return e.width < 0 }
-
Error() string
方法返回关于错误的描述。 -
当
length
小于零时,lengthNegative() bool
方法返回true
-
当
width
小于零时,widthNegative() bool
方法返回true
-
-
计算面积
func rectArea(length, width float64) (float64, error) { err := "" if length < 0 { err += "length is less than zero" } if width < 0 { if err == "" { err = "width is less than zero" } else { err += ", width is less than zero" } } if err != "" { return 0, &areaError{err, length, width} } return length * width, nil }
-
main()
函数func main() { length, width := -5.0, -9.0 area, err := rectArea(length, width) if err != nil { if err, ok := err.(*areaError); ok { if err.lengthNegative() { fmt.Printf("error: length %0.2f is less than zeron", err.length) } if err.widthNegative() { fmt.Printf("error: width %0.2f is less than zeron", err.width) } return } fmt.Println(err) return } fmt.Println("area of rect", area) }
28. panic
panic
和 recover
与python语言中的 try-except-finally
语句类似。
什么时候应该使用 panic?
需要注意的是,应该尽可能地使用error
错误,而不是使用 panic
和 recover
。只有当程序不能继续运行的时候,才应该使用 panic 和 recover 机制
。
panic 有两个合理的用例。
- 发生了一个不能恢复的错误,此时程序不能继续运行。 一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic,因为如果不能绑定端口,啥也做不了。
- 发生了一个编程上的错误。 假如我们有一个接收指针参数的方法,而其他人使用
nil
作为参数调用了它。在这种情况下,我们可以使用 panic,因为这是一个编程错误:用nil
参数调用了一个只能接收合法指针的方法。
panic 示例
func fullName(firstName *string, lastName *string) {
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %sn", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}
func main() {
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}
打印结果
panic: runtime error: last name cannot be nil
goroutine 1 [running]:
main.fullName(0xc00007df58, 0x0)
D:/code/goproject/src/28. panic 和 recover/panic 示例/main.go:12 +0x19d
main.main()
D:/code/goproject/src/28. panic 和 recover/panic 示例/main.go:20 +0x54
当执行程序出现了 panic
时,程序就会终止运行,打印出传入 panic 的参数,接着打印出堆栈跟踪信息。
发生 panic 时的 defer
重新总结一下 panic 做了什么。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。
在上面 panic 示例 中,没有延迟调用任何函数。如果有延迟函数,会先调用它,然后程序控制返回到函数调用方。
func fullName(firstName *string, lastName *string) {
defer fmt.Println("deferred call in fullName") // **
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %sn", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}
func main() {
defer fmt.Println("deferred call in main") // **
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}
打印结果
deferred call in fullName
deferred call in main
panic: runtime error: last name cannot be nilgoroutine 1 [running]:
main.fullName(0xc00007df28, 0x0)
D:/code/goproject/src/28. panic 和 recover/发生 panic 时的 defer/main.go:13 +0x24a
main.main()
D:/code/goproject/src/28. panic 和 recover/发生 panic 时的 defer/main.go:22 +0xcd
发生 panic 时,首先执行延迟函数,接着控制返回到函数调用方,调用方的延迟函数继续运行,直到到达顶层调用函数。
29. recover
recover
是一个内建函数,用于重新获得 panic 协程的控制。
只有在延迟函数的内部,调用 recover
才有用。在延迟函数内调用 recover
,可以取到 panic
的错误信息,并且停止 panic 续发事件,程序运行恢复正常。
如果在延迟函数的外部调用 recover
,就不能停止 panic 续发事件。
func recoverName() {
if r := recover(); r != nil {
fmt.Println("recovered from ==> ", r)
}
}
func fullName(firstName *string, lastName *string) {
defer recoverName()
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %sn", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}
func main() {
defer fmt.Println("deferred call in main")
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}
打印结果
recovered from ==> runtime error: last name cannot be nil
returned normally from main
deferred call in main
当程序在 fullName()
发生 panic 时,会调用延迟函数 recoverName
,它反过来会调用 recover()
来重新获得 panic 协程的控制。
if r := recover()
调用了 recover
,返回了 panic
的传参,因此会打印:
recovered from runtime error: last name cannot be nil
在执行完 recover()
之后,panic 会停止,程序控制返回到调用方(在这里就是 main
函数),程序在发生 panic 之后,从fmt.Println("returned normally from main")
开始会继续正常地运行。程序会打印 returned normally from main
,之后是 deferred call in main
。
panic & recover 和 协程
只有在相同的 Go 协程中调用 recover 才管用。recover
不能恢复一个不同协程的 panic。
func recovery() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}
func a() {
defer recovery()
fmt.Println("Inside A")
go b()
time.Sleep(1 * time.Second)
}
func b() {
fmt.Println("Inside B")
panic("oh! B panicked")
}
func main() {
a()
fmt.Println("normally returned from main")
}
打印结果:
Inside A
Inside B
panic: oh! B panickedgoroutine 6 [running]:
main.b()
D:/code/goproject/src/28. panic 和 recover/panic,recover 和 Go 协程/main.go:23 +0xa5
created by main.a
D:/code/goproject/src/28. panic 和 recover/panic,recover 和 Go 协程/main.go:17 +0xbd
在上面的程序中,函数 b()
发生 panic。函数 a()
调用了一个延迟函数 recovery()
,用于恢复 panic。
然而 panic 能够恢复吗?答案是否定的,panic 并不会恢复。因为调用 recovery
的协程和 b()
中发生 panic 的协程并不相同,因此不可能恢复 panic。
如果函数 b()
在相同的协程里调用,panic 就可以恢复。
程序由 go b()
修改为 b()
,就可以恢复 panic 了,因为 panic 发生在与 recover 相同的协程里。
打印结果:
Inside A
Inside B
recovered: oh! B panicked
normally returned from main
运行时 panic
运行时错误(如数组越界)也会导致 panic。这等价于调用了内置函数 panic
,其参数由接口类型 runtime.Error 给出。runtime.Error
接口的定义如下:
type Error interface {
error
// RuntimeError is a no-op function but
// serves to distinguish types that are run time
// errors from ordinary errors: a type is a
// run time error if it has a RuntimeError method.
RuntimeError()
}
而 runtime.Error
接口满足内建接口类型 error
。
func r() {
if r := recover(); r != nil {
fmt.Println("Recovered", r)
}
}
func a() {
defer r()
n := []int{5, 7, 4}
fmt.Println(n[3])
fmt.Println("normally returned from a")
}
func main() {
a()
fmt.Println("normally returned from main")
}
打印结果:
Recovered runtime error: index out of range [3] with length 3
normally returned from main
恢复后获得堆栈跟踪
当恢复 panic 时,就释放了它的堆栈跟踪。
恢复 panic 之后,可以使用 Debug
包中的 PrintStack
函数打印出堆栈跟踪。
func r() {
if r := recover(); r != nil {
fmt.Println("Recovered", r)
debug.PrintStack() // 打印堆栈跟踪
}
}
func a() {
defer r()
n := []int{5, 7, 4}
fmt.Println(n[3])
fmt.Println("normally returned from a")
}
func main() {
a()
fmt.Println("normally returned from main")
}
打印结果
Recovered runtime error: index out of range [3] with length 3
goroutine 1 [running]:
runtime/debug.Stack(0x3e, 0x0, 0x0)
E:/golang/src/runtime/debug/stack.go:24 +0xa5
runtime/debug.PrintStack()
E:/golang/src/runtime/debug/stack.go:16 +0x29
main.r()
D:/code/goproject/src/28. panic 和 recover/恢复后获得堆栈跟踪/main.go:11 +0xbb
panic(0xe80ce0, 0xc0000103a0)
E:/golang/src/runtime/panic.go:969 +0x1c7
main.a()
D:/code/goproject/src/28. panic 和 recover/恢复后获得堆栈跟踪/main.go:18 +0x53
main.main()
D:/code/goproject/src/28. panic 和 recover/恢复后获得堆栈跟踪/main.go:23 +0x29normally returned from main
30. 头等函数
什么是头等函数?
头等函数(First Class Function)可以把函数赋值给变量,也可以把函数作为其它函数的参数或者返回值。
匿名函数
编写一个简单的示例,把函数赋值给一个变量。
package main
import (
"fmt"
)
func main() {
a := func() {
fmt.Println("hello world first class function")
}
a()
fmt.Printf("%T", a)
}
打印结果
hello world first class function
func()
在上面的程序中,将一个函数赋值给了变量 a
。
赋值给 a
的函数没有名称。称为匿名函数。
调用该函数的方法就是:
-
使用变量
a
。即a()
,打印出hello world first class function
。 -
func main() { func() { fmt.Println("hello world first class function") }() }
就像其它函数一样,还可以向匿名函数传递参数。
package main
import (
"fmt"
)
func main() {
func(n string) {
fmt.Println("Welcome", n)
}("Gophers")
}
用户自定义的函数类型
语法:
type add func(a int, b int) int
以上代码片段创建了一个新的函数类型 add
,它接收两个整型参数,并返回一个整型。
编写一个程序,定义一个 add
类型的变量。
type add func(a int, b int) int
func main() {
var a add = func(a int, b int) int {
return a + b
}
s := a(5, 6)
fmt.Println("Sum", s)
}
打印结果
Sum 11
高阶函数
满足下列条件之一的函数:
- 接收一个或多个函数作为参数
- 返回值是一个函数
把函数作为参数,传递给其它函数
func simple(a func(a, b int) int) {
fmt.Println(a(60, 7))
}
func main() {
f := func(a, b int) int {
return a + b
}
simple(f)
}
打印结果
67
在其它函数中返回函数
func simple() func(a, b int) int {
f := func(a, b int) int {
return a + b
}
return f
}
func main() {
s := simple() // 把 simple 的返回值赋值给 s , 现在 s 包含了 simple 函数返回的函数
fmt.Println(s(60, 7)) // 调用 s
}
打印结果
67
闭包
闭包(Closure)是匿名函数的一个特例。当一个匿名函数所访问的变量定义在函数体的外部时,就称这样的匿名函数为闭包。
func main() {
a := 5
func() {
fmt.Println("a =", a)
}()
}
打印结果:
a = 5
在上面的程序中,匿名函数访问了变量 a
,而 a
存在于该函数体的外部。因此这个匿名函数就是闭包。
每一个闭包都会绑定一个它自己的外围变量:
func appendStr() func(string) string {
t := "Hello"
c := func(b string) string {
t = t + " " + b
return t
}
return c
}
func main() {
a := appendStr()
b := appendStr()
fmt.Println(a("World"))
fmt.Println(b("Everyone"))
fmt.Println(a("Gopher"))
fmt.Println(b("!"))
}
打印结果
Hello World
Hello Everyone
Hello World Gopher
Hello Everyone !
在上面程序中,函数 appendStr
返回了一个闭包。这个闭包绑定了变量 t
。
main
函数中声明的变量 a
和 b
都是闭包,它们绑定了各自的 t
值。
- 首先用参数
World
调用了a
。现在a
中t
值变为了Hello World
。 - 又用参数
Everyone
调用了b
。由于b
绑定了自己的变量t
,因此b
中的t
还是等于初始值Hello
。于是该函数调用之后,b
中的t
变为了Hello Everyone
。
头等函数的实际用途
创建一个程序,基于一些条件,来过滤一个 students
切片。
type student struct {
firstName string
lastName string
grade string
country string
}
func filter(s []student, f func(student) bool) []student {
var r []student
for _, v := range s {
if f(v) == true {
r = append(r, v)
}
}
return r
}
func main() {
s1 := student{
firstName: "Naveen",
lastName: "Ramanathan",
grade: "A",
country: "India",
}
s2 := student{
firstName: "Samuel",
lastName: "Johnson",
grade: "B",
country: "USA",
}
s := []student{s1, s2}
f := filter(s, func(s student) bool {
if s.grade == "B" {
return true
}
return false
})
fmt.Println(f)
}
打印结果:
[{Samuel Johnson B USA}]
31. 反射
什么是反射?
反射就是程序能够在运行时检查变量和值,求出它们的类型。
反射是 Go 语言中非常高级的概念,应该小心谨慎地使用它。使用反射编写清晰和可维护的代码是十分困难的,应该尽可能避免使用。
reflect 包
在 Go 语言中,reflect
实现了运行时反射。reflect
包会帮助识别 interface{}
变量的底层的具体类型和具体值。
reflect
包中的几种类型和方法:
-
reflect.Type 和 reflect.Value
reflect.Type
表示interface{}
的具体类型,而reflect.Value
表示它的具体值。reflect.TypeOf()
和reflect.ValueOf()
两个函数可以分别返回reflect.Type
和reflect.Value
。type order struct { ordId int customerId int } func createQuery(q interface{}) { t := reflect.TypeOf(q) v := reflect.ValueOf(q) fmt.Println("Type ", t) fmt.Println("Value ", v) } func main() { o := order{ ordId: 456, customerId: 56, } createQuery(o) }
-
reflect.Kind
Type
表示interface{}
的实际类型(上面程序:main.Order
),而Kind
表示该类型的特定类别(上面程序:struct
)。 -
NumField() 和 Field() 方法
NumField()
方法返回结构体中字段的数量,而Field(i int)
方法返回字段i
的reflect.Value
。 -
Int() 和 String() 方法
Int
和String
可以帮助分别取出作为int64
和string
的reflect.Value
。func main() { a := 56 x := reflect.ValueOf(a).Int() fmt.Printf("type:%T value:%vn", x, x) // type:int64 value:56 b := "Naveen" y := reflect.ValueOf(b).String() fmt.Printf("type:%T value:%vn", y, y) // type:string value:Naveen }
32. 读取文件
将整个文件读取到内存
使用 ioutil
包中的 ReadFile
函数。
package main
import (
"fmt"
"io/ioutil"
)
func main() {
data, err := ioutil.ReadFile("test.txt") // 程序会读取文件,并返回一个字节切片,而这个切片保存在 data 中
if err != nil {
fmt.Println("File reading error", err)
return
}
fmt.Println("Contents of file:", string(data))
}
分块读取文件
package main
import (
"bufio"
"flag"
"fmt"
"log"
"os"
)
func main() {
fptr := flag.String("fpath", "test.txt", "file path to read from")
flag.Parse()
f, err := os.Open(*fptr) // 使用命令行标记传递的路径,打开文件
if err != nil {
log.Fatal(err)
}
defer func() { // 延迟文件的关闭操作
if err = f.Close(); err != nil {
log.Fatal(err)
}
}()
r := bufio.NewReader(f) // 新建一个缓冲读取器
b := make([]byte, 3) // 创建长度和容量为3的字节切片,程序会把文件的字节读取到切片中。
for {
_, err := r.Read(b) // Read 方法会读取 len(b) 个字节,并返回所读取的字节数
if err != nil {
fmt.Println("Error reading file:", err)
break
}
fmt.Println(string(b))
}
}
逐行读取文件
func main() {
fptr := flag.String("fpath", "test.txt", "file path to read from")
flag.Parse()
f, err := os.Open(*fptr)
if err != nil {
log.Fatal(err)
}
defer func() {
if err = f.Close(); err != nil {
log.Fatal(err)
}
}()
s := bufio.NewScanner(f) // 创建一个新的 scanner
for s.Scan() { // Scan() 方法读取文件的下一行
fmt.Println(s.Text())
}
err = s.Err() //当Scan返回false时(到达文件末尾时返回 nil),否则Err()就会返回扫描过程中出现的错误
if err != nil {
log.Fatal(err)
}
}
33. 写入文件
将字符串写入文件
func main() {
f, err := os.Create("test.txt") // 创建 test.txt 的文件, 返回一个文件描述符
// 如果文件已经存在,那么create函数将停止创建该文件
if err != nil {
fmt.Println(err)
return
}
l, err := f.WriteString("Hello World") // 将字符串入到文件里面, 返回相应写入的字节数
if err != nil {
fmt.Println(err)
f.Close()
return
}
fmt.Println(l, "bytes written successfully")
err = f.Close() //将文件关闭。
if err != nil {
fmt.Println(err)
return
}
}
将字节写入文件
func main() {
f, err := os.Create("/home/naveen/bytes")
if err != nil {
fmt.Println(err)
return
}
d2 := []byte{104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100}
n2, err := f.Write(d2) // 写入字节
if err != nil {
fmt.Println(err)
f.Close()
return
}
fmt.Println(n2, "bytes written successfully")
err = f.Close()
if err != nil {
fmt.Println(err)
return
}
}
将字符串一行一行的写入文件
func main() {
f, err := os.Create("lines")
if err != nil {
fmt.Println(err)
f.Close()
return
}
d := []string{"Welcome to the world of Go1.", "Go is a compiled language.",
"It is easy to learn Go."}
for _, v := range d {
fmt.Fprintln(f, v) // 将 io.writer 做为参数,并且添加一个新的行
if err != nil {
fmt.Println(err)
return
}
}
err = f.Close()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("file written successfully")
}
追加写入文件
追加一行到上节创建的 lines
文件中:
func main() {
f, err := os.OpenFile("lines", os.O_APPEND|os.O_WRONLY, 0644) // 文件以追加的方式打开
if err != nil {
fmt.Println(err)
return
}
newLine := "File handling is easy."
_, err = fmt.Fprintln(f, newLine)
if err != nil {
fmt.Println(err)
f.Close()
return
}
err = f.Close()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("file appended successfully")
}
并发写文件
当多个协程同时(并发)写文件时,会遇到竞争条件。因此,当发生同步写的时候需要一个 信道作为一致写入的条件。
例如:写一个程序,该程序创建 100 个 goroutinues。每个 goroutinue 将并发产生一个随机数,届时将有 100 个随机数产生。这些随机数将被写入到文件里面。
- 创建一个 channel 用来读和写这个随机数。
- 创建 100 个生产者 goroutine。每个 goroutine 将产生随机数并将随机数写入到 channel 里。
- 创建一个消费者 goroutine 用来从 channel 读取随机数并将它写入文件。这样的话就只有一个 goroutinue 向文件中写数据,从而避免竞争条件。
- 一旦完成则关闭文件。
/* 产生随机数 */
func produce(data chan int, wg *sync.WaitGroup) {
n := rand.Intn(999)
data <- n
wg.Done()
}
/* 将数据写到文件 */
func consume(data chan int, done chan bool) {
f, err := os.Create("concurrent")
if err != nil {
fmt.Println(err)
return
}
for d := range data {
_, err = fmt.Fprintln(f, d)
if err != nil {
fmt.Println(err)
f.Close()
done <- false
return
}
}
err = f.Close()
if err != nil {
fmt.Println(err)
done <- false
return
}
done <- true // 通知任务已完成
}
func main() {
data := make(chan int) // 创建写入和读取数据的 channel
done := make(chan bool) // done用于消费者 goroutinue 完成任务之后通知main函数
wg := sync.WaitGroup{} // 用于等待所有生产随机数的 goroutine 完成任务
for i := 0; i < 100; i++ { // 创建 100 个 goroutines
wg.Add(1)
go produce(data, &wg)
}
go consume(data, done)
go func() { // 等待所有的 goroutines 完成随机数的生成, 然后关闭 channel。
wg.Wait()
close(data)
}()
d := <-done // 当 true 写入 done 时, main 函数解除阻塞
if d == true {
fmt.Println("File written successfully")
} else {
fmt.Println("File writing failed")
}
}
本文总结自 Go 系列教程(Golang tutorial series)
最后
以上就是优秀纸飞机为你收集整理的go学习笔记的全部内容,希望文章能够帮你解决go学习笔记所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复