- Go 开发环境准备
- Go语言:
- 1. hello Word
- 2. 运算符、常量、变量、打印控制
- 3. 分支与循环
- 4. 浮点数、整数、大数big、大常量
- 5. 字符串(len, utf8, range),类型转化
- 6. 函数、新类型
- 7. 方法
- 8. 一等函数(函数式编程)、匿名函数、闭包
- 9. 数组
- 10. 切片、可变参数的函数
- 11. Map
- 12. 结构体
- 13. Class、构造函数
- 14. 组合与转发
- 15. 接口
- 16.指针
- 17. nil
- 18. error
- 20. goroutine
- 21. 并发 concurrent
Go语言是一门编译语言
GO安装包:https://studygolang.com/dl
- 之后打开命令行,输入go,验证是否安装成功
- 改变代理,命令行:
-
- go env -w GO111MODULE=on
-
- go env -w GOPROXY=https://goproxy.io,direct
-
- 代理参考:https://blog.csdn.net/qq_41065919/article/details/107710144
VS code :https://code.visualstudio.com/Download (如果下载失败,可以在软件管理里下)
- 安装Go插件:直接搜索该插件安装,
-
- 重启之后,快捷键 Ctrl+shift+P 输入go,点击Go: Install/Update Tools
1. hello Word笔记参考杨旭大佬视频:https://www.bilibili.com/video/BV1fD4y1m7TD?from=search&seid=16942972441851783535
// 文件名为:hello.go
package main
import (
"fmt"
// fmt的功能?实现 I/O 格式化,源自于C
)
func main() {
// 必须要有main包和main函数,是程序开始的地方
// 函数体的左大括号必须与函数名在同一行,右大括号必须单独自己在一行
fmt.Println("hello world")
}
// expected 'package', found 'EOF' 是go文件没有保存所导致的
// expected 'STRING', found '{' 是语法错误,哪个放写错了
// import and not used: "strings" 引入包,但没到,也会报错
// 在VS code的命名行中输入:go run hello.go
2. 运算符、常量、变量、打印控制
package main
import (
"fmt"
"math/rand"
"strings"
)
func main() {
// Print,Println函数: 可以传递若干个参数,之间用逗号分开,参数可以是字符串、数字、数学表达式等
fmt.Println("hello", 2021) // hello 2021
// Printf 可以实现打印控制,第一个参数必须为字符串,其中有像%v这样的格式化动词,后面依次接参数
fmt.Printf("%v %v.n", "hello", 2021) // hello 2021.
fmt.Printf("%-15v ||%4vn", "hello world", 94) // hello world || 94
fmt.Printf("%-15v ||%4vn", "operation", 198) // operation || 198
// 常量与变量 const var, 不同声明方式
const x, y = 8, 9
var a = 10
var (
b = 11
c = 12
)
var d, e = 13, 14
f := 15 // 短声明,功能一样,但必须在func中
e += a
e++ // go中没有前加加
fmt.Println(x, y, a, b, c, d, e, f) // 8 9 10 11 12 13 25 15
// 随机数: rand包 [0, N)
var num = rand.Intn(10) + 1
fmt.Println(num)
// Boolean 类型
var strs = "walk outside"
var exit = strings.Contains(strs, "outside")
fmt.Println(exit) // true
// 逻辑运算:|| 表示或,&& 表示与, ! 表示取反,同时使用了短路逻辑
var year = 2008
var leap = !(year%100 == 0 || (year%4 == 0 && year%100 != 100))
fmt.Println(leap) // false
}
// 算数运算: +、-、*、/、% 同C语言一样
// 比较运算:==、<=、<、!=、>=、>、同C语言一样
// Boolean类型,只有true是真,false是假, 10不能代表真假
// Go 变量的作用范围一般在{}中,或者在package中(package级中不能使用短声明)
3. 分支与循环
package main
import (
"fmt"
"time"
)
func main() {
// if 后面跟一个返回 Boolean 类型的表达式 可以用来做分支
if command := "go east"; command == "go east" {
fmt.Println("1")
} else if command == "east" {
fmt.Println("2")
} else {
fmt.Println("3")
}
// if 打印:1n
// switch 后面跟一个变量,可以是字符串,也可以是数字, 用来做分支
// switch 中还有一个 fallthrough 关键字,它用来执行下一个 case 的 body 部分
switch strs := "go east"; strs {
case "go":
fmt.Println("1")
case "go east", "goto": // 这里放了两个值,是或的关系
fmt.Println("2")
fallthrough
case "east":
fmt.Println("3")
}
// switch 打印:2n 3n
// for 后边没有跟条件,那就是无限循环(可以使用 break 跳出循环)
// for 有3中形式,以下演示2中,还有一种是不跟条件的无限循环
var count = 3
for count > 0 {
fmt.Println(count)
time.Sleep(time.Second * 2) // 休息两秒
count--
if count == 1 {
break
}
}
// for 打印:3n 2n
for i := 0; i < 3; i++ {
fmt.Println(i)
}
// for 打印:0n 1n 2n
}
4. 浮点数、整数、大数big、大常量
package main
import (
"fmt"
// Printf 详解:https://www.jianshu.com/p/8be8d36e779c
"math"
"math/big"
"time"
)
func main() {
// 1. 浮点数声明,三种声明方式相同,只要数字含有小数部分,那么它的类型就是 float64
days1 := 1.0 / 3
var days2 = 1.0 / 3
var days3 float64 = 1.0 / 3
fmt.Println(days1) // 0.3333333333333333
fmt.Printf("%vn", days2) // 0.3333333333333333
fmt.Printf("%fn", days3) // 0.333333
fmt.Printf("%.3fn", days3) // 0.333
fmt.Printf("%6.2fn", days3) // 0.33
fmt.Printf("%6.2fn", days3) // 000.33
// 精度是有问题的,建议先做乘法再做除法,如何避免舍入错误?
fmt.Println(days1 + days2 + days3) // 1
a := 0.1
a += 0.2
fmt.Println(a) // 0.30000000000000004
fmt.Println(math.Abs(a-0.3) < 0.001) // true
// 2. 整数
var year int = 2021 // 有符号整数
var month uint = 2021 // 无符号整数
fmt.Printf("%Tn", year) // int
fmt.Printf("%Tn", month) // uint
// uint8 可以用来表示8位的颜色(红绿蓝:0-255)
// 十六进制表示,在数前面加上 0x 前缀
var red, green, blue uint8 = 0, 141, 213
fmt.Printf("%x %x %xn", red, green, blue) // 0 8d d5
red, green, blue = 0x00, 0x8d, 0xd5
fmt.Printf("#%02x%02x%02xn", red, green, blue) // #008dd5
fmt.Printf("%d %d %dn", red, green, blue) // 0 141 213
// 整数最大值(math里只定义了与架构无关的最大最小值)
var max = math.MaxInt16
fmt.Println(max) // 32767
future := time.Unix(2100000000, 0)
fmt.Println(future) // 2036-07-18 21:20:00 +0800 CST
// 3. 大数表示,可以使用big包
lightSpeed := big.NewInt(299792) // 从int64转化
fmt.Println(lightSpeed) // 299792
distance := new(big.Int)
distance.SetString("240000000000000000000000000000", 10) // 数 和 十进制
fmt.Println(distance) // 240000000000000000000000000000
seconds := new(big.Int)
seconds.Div(distance, lightSpeed) // 除法
fmt.Println(seconds) // 800555051502374979986123
// 4. 较大数值的常量(大常量)
// 在 Go 里面,可以为常量指明类型(但可能会报错,超出类型范围)
// 对于常量,也可以不指明常量的类型,Go 会使用类型推断,常量是可以无类型的(untyped)
// 这种无类型的数值字面值就是由 big 包所支持的。这使你可以操作很大的数(超过18的10¹⁸)
// 尽管 Go 编译器使用 big 包来处理无类型的数值常量,但是常量和 big.Int 的值是不能互换的
// 针对字面值和常量的计算是在编译阶段完成的
}
// Go 语言默认浮点数类型为float64,还有一个是float32(当处理大量数据时,牺牲精度换内存是有意义的,除此之外首选float64
// Go 提供了10种整数类型:有符号和无符号各五个;实际上是只有8种(取值范围不同,与CPU架构无关):int8,int16,int32,int64...
// int 和 uint 是针对目标设备优化的类型
// 在树莓派 2、比较老的移动设备上,int 和 uint 都是 32 位的
// 在比较新的计算机上,int 和 uint 都是 64 位的
// 如果在较老的32位设备上,使用了超过20亿的整数,并且代码还能运行,最好使用int64和uint64来代替int和uint
// 整数环绕:所有的整数类型都有一个取值范围,超出这个范围,就会发生“环绕”
// Unix 系统里,时间是以1970年1月1日至今的秒数来表示的,但是在2038年,这个数就会超过20多亿,也就是超过了int32的范围
// 应使用:int64 或 uint64
// 如果没有为指数形式的数值指定类型的话,那么 Go 将会将它视作 float64 类型
// 对于较大的整数(超过10e18 ):big.Int
// 对于任意精度的浮点类型,big.Float
// 对于分数,big.Rat
// 一旦使用了 big.Int,那么等式里其它的部分也必须使用 big.Int
// 缺点:用起来繁琐,且速度慢
5. 字符串(len, utf8, range),类型转化
package main
import (
"fmt"
"math"
"strconv"
"unicode/utf8"
)
func main() {
// 1. 字符串编码是UTF-8
// 字符串声明 可一个给某个变量赋予不同的 string 值,但是 string 本身是不可变的
peace1 := "peace"
var peace2 = "peace"
var peace3 string = "peace"
var peace4 string // 零值 空字符串
fmt.Printf("%v %v %v %vn", peace1, peace2, peace3, peace4)
fmt.Printf("%cn", peace1[0]) // p
// len 是GO的一个内置函数,表示字符串的字节长度,不需要import;字符的编码长度是可变的,8位,16位...
// 求字符长度,可以先解码成rune,再进行操作
question := "πάω!"
fmt.Println(len(question)) // 7
fmt.Println(utf8.RuneCountInString(question), "runes") // 4 runes
c, size := utf8.DecodeRuneInString(question) // 返回字符串第一个字符和大小
fmt.Printf("%c %vn", c, size) // π 2
// range 遍历各种集合, 可以使用下划线代替不使用的变量
for i, c := range question {
fmt.Printf("%v %c ", i, c) // 0 π 2 ά 4 ω 6 !
}
fmt.Println()
// 字面值 和 原始字符串字面值(可以打印出转义字符),上面都是字面值
peace5 := `peace /n`
fmt.Println(peace5) // peace /n
// 字符,code points, runes, bytes
// Unicode 联盟为超过 100 万个字符分配了相应的数值,这个数叫做 code point
// rune 这个类型,它是 int32 的一个类型别名。为了表示这样的 unicode code point
// byte 是 uint8 类型的别名,目的是用于二进制数据
// 自定义类型别名:
type ydd = uint8
var temp ydd = 18
fmt.Printf("%v type is %Tn", temp, temp) // 18 type is uint8
// 打印字符 %c, 任何整数类型都可以使用 %c 打印,但是 rune 意味着该数值表示了一个字符
var pi rune = 960
var alpha rune = 940
var omega rune = 969
var bang byte = 33
fmt.Printf("%v %v %v %vn", pi, alpha, omega, bang) // 960 940 969 33
fmt.Printf("%c%c%c%cn", pi, alpha, omega, bang) // πάω!
fmt.Printf("%v type is %Tn", pi, pi) // 960 type is int32
// 字符 单引号括起来,默认类型为rune
grade := 'A'
fmt.Printf("%v type is %Tn", grade, grade) // 65 type is int32
// 2. 类型转换(用目标类型包裹起来) 类型是不能混着用(运算什么的都不能)
// 可以从浮点类型转化为整数类型,小数点后边的部分会被截断,而不是舍入
// 无符号和有符号整数类型之间也需要转换
// 不同大小的整数类型之间也需要转换
age := 24
marsAge := float64(age)
fmt.Printf("%v type is %Tn", age, age) // 24 type is int
fmt.Printf("%v type is %Tn", marsAge, marsAge) // 24 type is float64
// 类型转换时,可能发生“环绕行为”
var bh float64 = 32768
if bh < math.MinInt16 || bh > math.MaxInt16 {
fmt.Println("bh 不能转换成int16类型")
bhInt16 := int16(bh)
fmt.Println(bhInt16) // -32768
}
// 字符串转化 把rune byte 转化成 string
fmt.Println(string(pi), string(alpha)) // π ά
// 数值转字符串, Itoa 是 Integer to ASCII 的意思
str := "hello " + strconv.Itoa(2021)
fmt.Println(str) // hello 2021
countdown := 9
str = fmt.Sprintf("hello %v", countdown)
fmt.Println(str) // hello 9
// boolean 类型的转换,只能打印成字符串true,不能使用 string(false), int(false);bool(1), bool("yes")
isTF := true
str = fmt.Sprintf("%v", isTF)
fmt.Println(str) // true
}
6. 函数、新类型
package main
import "fmt"
// 1. 函数声明:
// func 函数名 (参数 参数类型,参数 参数类型) 返回类型 {}
// 大写字母开头的函数、变量或其它标识符都会被导出,对其它包可用。小写字母开头的就不行
// 函数声明时,如果多个形参类型相同,那么该类型只写一次即可(写最后一个)
// 函数可以返回多个值
// 函数的多个返回值需要用括号括起来,每个返回值名字在前,类型在后。声明函数时可以把名字去掉,只保留类型
// … 和空接口组合到一起就可以接受任意数量、类型的参数 如Println函数
// 同一个包中声明的函数在调用彼此时不需要加上包名
// 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, "Cn") // 294K is 20.850000000000023C
// 声明新类型,虽然底层是一样的,但是不是别名,不能混用
type cels float64
const degrees = 20 // 无类型
var temperature cels = degrees // cels 类型
temperature += 10
fmt.Printf("%v type is %Tn", temperature, temperature) // 30 type is main.cels
}
7. 方法
// 7. 方法
package main
import "fmt"
// 方法, Go 提供了方法,但是没提供类和对象
// 方法也是一种函数,但这种函数输入某一个类型,或者和某一个类型所关联的,这个时候就叫方法
// 方法可以有多个参数,但只能有一个接收者
// 不可以是 int、float64 等预声明的类型进行关联
// 声明新类型
type celsius float64
type kelvin float64
// 通过方法为新类型添加行为
// func (variable_name variable_data_type) function_name() [return_type]{}
func (k kelvin) toCelsius() celsius {
return celsius(k - 273.15) // 这里是先计算,再了类型转换
}
func main() {
var k kelvin = 294
c := k.toCelsius()
fmt.Printf("%v type is %Tn", c, c) // 20.850000000000023 type is main.celsius
}
8. 一等函数(函数式编程)、匿名函数、闭包
package main
// 1.一等函数(或高阶函数、头等函数)属于函数式编程里的概念,将函数当做变量看待
// 函数可以赋值给变量
// 作为参数传递给函数
// 作为函数的返回类型
import (
"fmt"
"math/rand"
"time"
)
type kelvin float64
func fakeSecsor() kelvin {
return kelvin(rand.Intn(151) + 150)
}
func measureTemperature(samples int, sensor func() kelvin) {
for i := 0; i < samples; i++ {
fmt.Println(sensor())
time.Sleep(time.Second)
}
}
// 声明函数类型,简化书写
type sensorFunc func() kelvin
func measureTemperatureTwo(samples int, sensor sensorFunc) {
}
// 2. 匿名函数,名字的函数,在 Go 里也称作函数字面值,下面方式1
var f = func() {
fmt.Println("匿名函数")
}
func main() {
// 1. 方法
// 赋值给变量,函数遇到小括号才能执行
sensor := fakeSecsor
fmt.Println(sensor()) // 156
// 作为参数传递给函数
measureTemperature(3, fakeSecsor)
// 2. 匿名函数
// 匿名函数 方式1
f() // 匿名函数
// 匿名函数 方式2
ff := func(message string) {
fmt.Println(message)
}
ff("匿名函数 方式2") // 匿名函数 方式2
// 匿名函数 方式3
func() {
fmt.Println("匿名函数 方式3")
}()
// 3. 闭包: sens 是已经返回的匿名函数,但是sens依然可以访问其外部的变量s和offset,这个就叫闭包
sens := calibrate(fakeSecsor, 3)
fmt.Println(sens())
}
// 3. 闭包
// 函数字面值需要保留外部作用域的变量引用,所以函数字面值都是闭包的
// 闭包(closure)就是由于匿名函数封闭并包围作用域中的变量而得名的
func calibrate(s sensorFunc, offset kelvin) sensorFunc {
// 匿名函数 作为函数的返回类型
return func() kelvin {
return s() + offset
}
}
9. 数组
package main
import (
"fmt"
)
func main() {
// 数组是一种固定长度且有序的元素集合
// 数组的长度可由内置函数 len 来确定
// 声明数组时,未被赋值元素的值是对应类型的零值
var planets [8]string
planets[0] = "earth"
planets[1] = "hello"
earth := planets[0]
fmt.Println(earth) // earth
fmt.Println(len(planets)) // 8
fmt.Println(planets[2] == "") // true
// Go 编译器在编译时未能发现越界错误,那么程序在运行时会出现 panic, 会导致程序崩溃
// i := 8
// planets[i] = "planets" // panic: runtime error: index out of range [8] with length 8
// 使用复合字面值初始化数组, 无论哪种方式,数组的长度都是固定的
number := [3]int{1, 2, 3}
grades := [...]string{ // 会自动推算出个数
"grade",
"number",
}
fmt.Println(number) // [1 2 3]
fmt.Println(grades) // [grade number]
// 数组遍历
for i := 0; i < len(grades); i++ {
grade := grades[i]
fmt.Println(i, grade)
}
// 0 grade
// 1 number
for i, grade := range grades {
fmt.Println(i, grade)
}
// 数组的复制
// 无论数组赋值给新的变量还是将它传递给函数,都会产生一个完整的数组副本
// 数组也是一种值,函数通过值传递来接受参数。所以数组作为函数的参数就非常低效
// 数组的长度也是数组类型的一部分,所以数组作为参数时,长度不一样,也会导致出错
// 函数一般使用 slice 而不是数组作为参数,因为slice 的长度不是类型的一部分,所以可以代替数组作为参数
// 二维数组 数组的数组
var board [8][8]string
board[0][0] = "bo"
board[0][7] = "ar"
for column := range board[1] {
board[1][column] = "p"
}
fmt.Print(board)
// [[bo ar] [p p p p p p p p] [ ] [ ] [ ] [ ] [ ] [ ]]
}
10. 切片、可变参数的函数
package main
import (
"fmt"
"sort"
)
// 打印长度 和 容量
func dump(label string, slice []string) {
fmt.Printf("%v: length %v, capacity %v %vn", label, len(slice), cap(slice), slice)
}
// 声明可变参数的函数
// 声明 Printf、append 这样的可变参数函数,需要在函数的最后一个参数前面加上 … 符号
// 类型前面加...,表示这个变量是该类型的切片,切片变量后面加...,表示将切片展开成独立的个体
func pir(name string, args ...string) {
fmt.Printf("%v: %vn", name, args)
}
func main() {
// Slice 指向数组的窗口或者叫视图
// planets 是一个数组,那么 planets[0:4] 就是一个切片(左闭右开的),它切分出了数组里前 4 个元素
// 切分数组不会导致数组被修改,它只是创建了指向数组的一个窗口或视图,这种视图就是 slice 类型
dwarfArray := [...]string{"a", "b"} // 数组声明,自动推断元素个数
dwarfSlice := dwarfArray[:]
fmt.Println(dwarfSlice) // [a b]
// 忽略掉 slice 的起始索引,表示从数组的起始位置进行切分;
// 忽略掉 slice 的结束索引,相当于使用数组的长度作为结束索引。
// 注意:slice 的索引不能是负数。
// 如果同时省略掉起始和结束索引,那就是包含数组所有元素的一个 slice。
// 切分数组的语法也可以用于切分字符串,切分字符串时,索引代表的是字节数而非 rune 的数
// Slice 的复合字面值
// Go 里面很多函数都倾向于使用 slice 而不是数组作为参数
// 获得与底层数组相同元素的 slice,那么可以使用 [:] 进行切分
// 切分数组并不是创建 slice 唯一的方法,可以直接声明 slice 如下:[]类型
dwarfs := []string{"aa", "bb"} // 自动推断元素个数,也是先建一个底层数组,再切片
fmt.Println(dwarfs) // [aa bb]
fmt.Printf("%T, %Tn", dwarfArray, dwarfs) // [2]string, []string
// slice 的长度不是类型的一部分,所以可以代替数组作为参数
// func TrimSpace(s string) string :返回将s前后端所有空白(unicode.IsSpace指定)都去掉的字符串
// func Join(a []string, sep string) string:func Join(a []string, sep string) string
// 带有方法的切片: func (x StringSlice) Sort() { Sort(x) }
sort.StringSlice(dwarfSlice).Sort()
// append 内置函数: 可变参数函数
dwarfs = append(dwarfs, "cc")
dwarfs = append(dwarfs, "dd", "ff")
// 长度 和 容量
// Slice 中元素的个数决定了 slice 的长度
// 容量 是对应底层数组的长度(元素)
// 如果 slice 的底层数组比 slice 还大,那么就说该 slice 还有容量可供增长
// 如果 slice 的底层数组比 slice 小,则需要新分配一个底层数组,容量是以前的2倍
// 如果多个 slice 的底层数组都是同一个,则修改一个元素,会影响所有,反之不然
dump("dwarfs", dwarfs) // dwarfs: length 5, capacity 8 [aa bb cc dd ff]
dump("dwarfs[1:2]", dwarfs[1:2]) // dwarfs[1:2]: length 1, capacity 7 [bb]
// 三个索引的切分操作(主要来区分底层数组的)
// Go 1.2 中引入了能够 限制新建切片容量 的三索引切分操作
terrestrial := dwarfs[0:4:4] // [0 4) 容量为4
dump("terrestrial", terrestrial) // terrestrial: length 4, capacity 4 [aa bb cc dd]
terrestrial = append(terrestrial, "ee")
dump("terrestrial", terrestrial) // terrestrial: length 5, capacity 8 [aa bb cc dd ee]
// 使用 make 函数对 slice 进行预分配:尽量避免额外的内存分配和数组复制操作
col := make([]string, 0, 3) // string 切片,长度为0,容量为3
col = append(col, "R", "G")
dump("col", col) // col: length 2, capacity 3 [R G]
arr := make([]int, 5) // int 切片,长度和容量都是5(默认值是类型的0值)
fmt.Printf("%v: length %v, capacity %v %vn", "arr", len(arr), cap(arr), arr)
// arr: length 5, capacity 5 [0 0 0 0 0]
// 可变参数的函数
pir("dwarfs", "hh", "ff") // dwarfs: [hh ff]
pir("dwarfs", dwarfs...) // dwarfs: [aa bb cc dd ff]
// 问:当切片作为参数时,在函数里修改了切片,会影响外面的切片吗?测试不会
}
11. Map
package main
import (
"fmt"
"math"
"sort"
)
func main() {
// 将 key 映射到 value
// key 几乎可以是任何类型
// 声明 map,必须指定 key 和 value 的类型:map[keyType]valueTpye
// 复合字面值声明方式:
temperature := map[string]int{
"earth": 15,
"mars": 16,
}
fmt.Println(temperature["earth"]) // 15
// 如果有key存在,就修改值,不存在就添加
temperature["earth"] = 55
temperature["venus"] = 44
delete(temperature, "earth") // 删除
fmt.Println(temperature) // map[mars:16 venus:44]
// 如果不存在key,会返回对应类型的零值
fmt.Println(temperature["moon"]) // 0
// 逗号与 ok 写法, ok 是boolean类型
if moon, ok := temperature["moon"]; ok {
fmt.Printf("On auerage the moon is %vn", moon)
} else {
fmt.Println("where is the moon?")
}
// where is the moon?
// map 不会被复制
// 数组、int、float64 等类型在 赋值 给新变量 或 传递至函数/方法 的时候会创建相应的副本
// 使用 make 函数对 map 进行预分配
// 创建 map 时,make 函数可接受一个或两个参数,使用 make 函数创建的 map 的初始长度为 0
// 第二个参数用于为指定数量的 key 预先分配空间
earth := make(map[float64]int, 8)
fmt.Println(len(earth)) // 0
// 使用 map 作计数器(依据key的值不能重复)
moon := []float64{ // 声明切片
-28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}
frequency := make(map[float64]int) // 声明map
for _, i := range moon { // 统计
frequency[i]++
}
for i, num := range frequency { // 打印,遍历的顺序是无法保证的
fmt.Printf("%+.2f occurs %d timesn", i, num)
}
// -28.00 occurs 2 times
// +32.00 occurs 1 times
// -31.00 occurs 1 times
// -29.00 occurs 2 times
// -23.00 occurs 1 times
// -33.00 occurs 1 times
// 使用 map 和 slice 实现数据分组
groups := make(map[float64][]float64)
for _, t := range moon {
g := math.Trunc(t/10) * 10
groups[g] = append(groups[g], t)
}
for g, tem := range groups {
fmt.Printf("%v: %vn", g, tem)
}
// 30: [32]
// -30: [-31 -33]
// -20: [-28 -29 -23 -29 -28]
// 将 map 用作 set
// Go 语言里没有提供 set 集合
// Set 这种集合与数组类似,但元素不会重复
set := make(map[float64]bool)
for _, t := range moon {
set[t] = true
}
if set[-28.0] {
fmt.Println("set is true")
}
fmt.Println(set)
// set is true
// map[-33:true -31:true -29:true -28:true -23:true 32:true]
// 排序
unique := make([]float64, 0, len(set))
for k := range set {
unique = append(unique, k)
}
sort.Float64s(unique)
fmt.Println(unique) // [-33 -31 -29 -28 -23 32]
}
12. 结构体
package main
import (
"encoding/json"
"fmt"
"os"
)
func main() {
// 为了将分散的零件组成一个完整的结构体,Go 提供了 struct 类型
// struct 允许你将不同类型的东西组合在一起,声明(直接声明成变量,不推荐,相当于匿名):
var cur struct {
x, y float64
}
cur.x = 2.0
fmt.Println(cur.x) // 2
fmt.Println(cur) // {2 0}
// 声明,类型复用结构体,通常放main外面
type loca struct {
x, y float64
}
var spirit loca
spirit.x = 5.2
// 通过复合字面值初始化 struct(两种)
// 通过成对的字段和值进行初始化(推荐):字段的顺序可以随意或省略部分字段
opportunity := loca{x: 1.2, y: 3.4}
fmt.Println(opportunity) // {1.2 3.4}
// 按字段定义的顺序进行初始化
op := loca{6.5, 7.9}
fmt.Println(op) // {6.5 7.9}
// 打印 struct(例子 21.5)
// %v,打印出 {-4.5895 137.4417}
// %+v,打印出 {lat:-4.5895 long:137.4417}
// struct 的复制时 是完整的复制了一份,是副本
// 由结构体组成的 slice
locations := []loca{
{x: 1.2, y: 3.4},
{x: 1.2, y: 3.4},
}
fmt.Println(locations) // [{1.2 3.4} {1.2 3.4}]
// 将 struct 编码为 JSON
// JSON(JavaScript Object Notation,JavaScript 对象表示法)常用于 Web API
// json 包的 Marshal 函数可以将 struct 中的数据转化为 JSON 格式
// Marshal 函数只会对 struct 中被导出的字段进行编码
type location struct {
Lat, Long float64
}
curiosity := location{Lat: -4.5, Long: 6.5}
bytes, err := json.Marshal(curiosity)
if err != nil {
fmt.Println(err)
os.Exit(1) // 不是0就会退出
}
fmt.Println(string(bytes)) // {"Lat":-4.5,"Long":6.5}
// 使用 struct 标签来自定义 JSON
// Go 语言中的 json 包要求 struct 中的字段必须以大写字母开头,类似 CamelCase 驼峰型命名规范
// 但有时候我们需要 snake_case 蛇形命名规范,那么该怎么办?
// 在struct中 可以为字段注明标签,使得 json 包在进行编码的时候能够按照标签里的样式修改字段名
type location1 struct {
Lat float64 `json:"latitude"`
Long float64 `json:"longitude"`
}
curiosity1 := location1{Lat: -4.5, Long: 6.5}
bytes1, err1 := json.Marshal(curiosity1)
if err1 != nil {
fmt.Println(err1)
os.Exit(1) // 不是0就会退出
}
fmt.Println(string(bytes1)) // {"latitude":-4.5,"longitude":6.5}
}
13. Class、构造函数
package main
import (
"fmt"
)
// Go 和其它经典语言不同,它没有 class,没有对象,也没有继承,但是 Go 提供了 struct 和方法
// 方法可以被关联到你声明的类型上,从而代替类
type coordinate struct {
d, m, s float64
h rune
}
func (c coordinate) decimal() float64 {
sign := 1.0
switch c.h {
case 'S', 's', 'W', 'w':
sign = -1
}
return sign * (c.d + c.m/60 + c.s/3600)
}
// 所谓的构造函数(其实没有,就是一个普通的方法),方法里还可以做其他事
// Go没有专用的构造函数,但以 new 或 New 开头的函数,通常是用来构造数据的。例如 newPerson()
// 如果该函数叫 NewError,那么调用的时候就是 errors.NewError(),这就不如 errors.New() 简洁
func newCoordinate(d, m, s float64, h rune) coordinate {
return coordinate{d, m, s, h}
}
func main() {
lat := coordinate{d: 4, m: 35, s: 22.2, h: 'S'}
long := newCoordinate(137, 26, 30.12, 'E')
fmt.Println(lat.decimal(), long.decimal()) // -4.5895 137.4417
fmt.Println("ok")
}
14. 组合与转发
package main
import (
"fmt"
)
// Go 通过结构体实现组合(composition)。
// Go 提供了“嵌入”(embedding)特性,它可以实现方法的转发(forwarding)
// 组合与转发 实现 继承,组合是一种更简单、灵活的方式
// sturct 实现 组合
type report struct {
sol int
template template
location location
}
type template struct {
high, low float64
}
type location struct {
lat, long float64
}
// 方法
func (t template) average() float64 {
return (t.high + t.low) / 2
}
// 这就是一个转发(这是显示声明)
func (r report) average() float64 {
return r.template.average()
}
func main() {
t := template{-1.0, -4.6}
l := location{-4.2, 137.2}
r := report{15, t, l}
fmt.Printf("%+vn", r) // {sol:15 template:{high:-1 low:-4.6} location:{lat:-4.2 long:137.2}}
fmt.Printf("high is %vn", r.template.high) // high is -1
fmt.Printf("average is %vn", t.average()) // average is -2.8
fmt.Printf("average is %vn", r.average()) // average is -2.8
fmt.Printf("average is %vn", r.template.average()) // average is -2.8
}
// 转发
package main
import (
"fmt"
)
// Go 可以通过 struct 嵌入 来实现方法的转发
// 在 struct 中只给定字段类型,不给定字段名即可
// 在 struct 中,可以转发任意类型
// 对于嵌入的类型,它的方法可以被上层使用
type report struct {
sol
template // 底层是有一个同名变量,同:template template
location
}
type sol int
type template struct {
high, low float64
}
type location struct {
lat, long float64
}
// 方法,嵌入之后,就隐式转发
func (t template) average() float64 {
return (t.high + t.low) / 2
}
// 命名冲突,如果嵌入的类型都含有同一个方法名,就需要显示指定
func (l location) average() float64 {
return (l.lat + l.long) / 2
}
func (r report) average() float64 { // 显示指定
return r.template.average()
}
func main() {
t := template{-1.0, -4.6}
l := location{-4.2, 137.2}
r := report{15, t, l}
fmt.Printf("average is %vn", t.average()) // average is -2.8
fmt.Printf("average is %vn", l.average()) // average is 66.5
fmt.Printf("average is %vn", r.average()) // average is -2.8
fmt.Printf("average is %vn", r.template.average()) // average is -2.8
}
15. 接口
// 16. 接口
package main
import (
"fmt"
"strings"
)
// 类型 关注自己存储了什么,而接口 关注类型可以做什么
// 类型 通过方法来表达自己行为, 而接口 通过列举类型必须满足的一组方法来进行声明
// 在 Go 语言中,不需要显式声明接口
// Go 标准库导出了很多只有单个方法的接口
// Go 通过简单的、通常只有单个方法的接口……来鼓励组合而不是继承,这些接口在各个组件之间形成了简明易懂的界限
// 1.声明一个接口变量 inte
// 任何类型 任何值 只要满足这个接口的要求(就是定义了相同的talk方法),那么它就能成为这个接口变量的值
var inte interface {
talk() string
}
// 声明一个 struct
type martian struct{}
// 声明一个talk方法;这两个声明,就说明martain这个类型,满足了这个接口,或实现了这个接口(是隐式的)
func (m martian) talk() string {
return "martian"
}
// 2.接口类型
// 为了复用,通常会把接口声明为类型
// 按约定,接口名称通常以 er 结尾
// 接口可以与 struct 嵌入 特性一同使用
// 同时使用组合和接口将构成非常强大的设计工具
type talker interface {
talk() string
}
// 满足talker接口,才能作为参数
func shout(t talker) {
louder := strings.ToUpper(t.talk())
fmt.Println(louder)
}
// 3. 嵌入和接口可以一同使用
type starship struct {
martian
}
func main() {
// 给接口变量赋值
inte = martian{}
fmt.Println(inte.talk()) // martian
// 接口转发
shout(martian{}) // MARTIAN
// 嵌入和接口
s := starship{martian: martian{}}
shout(s) // MARTIAN
}
16.指针
package main
import (
"fmt"
"strings"
)
// 指针: 合理使用指针,不要过度使用指针
// 指针是指向另一个变量地址的变量
// Go 语言的指针同时也强调安全性,不会出现迷途指针
// 指针接收者
type pers struct {
age int
}
func (p *pers) birthday() {
p.age++
}
// 内部指针
type stats struct {
level int
}
func levelUp(s *stats) {
s.level++
}
type character struct {
stats stats
}
func main() {
// & 和 * 符号
// & 表示地址操作符,通过 & 可以获得变量的内存地址
// & 操作符无法获得字符串/数值/布尔字面值的地址
// &42,&“hello”这些都会导致编译器报错
// * 操作符与 & 的作用相反,它用来解引用,提供内存地址指向的值
// C 语言中的内存地址可以通过例如 address++ 这样的指针运算进行操作,但是在 Go 里面不允许这种不安全操作
// 所以是不能用这个类遍历数组
answer := 42
fmt.Println(&answer) // 0xc000128058
address := &answer
fmt.Println(*address) // 42
fmt.Printf("address is a %Tn", address) // address is a *int
// 指针类型
// 指针存储的是内存地址
// 指针类型和其它普通类型一样,出现在所有需要用到类型的地方,如变量声明、函数形参、返回值类型、结构体字段等
// 将 * 放在类型前面表示声明指针类型
// 将 * 放在变量前面表示解引用操作
canda := "canda"
var home *string
home = &canda
fmt.Println(*home) // canda
fmt.Printf("home is a %Tn", home) // home is a *string
// 指针就是用来指向的
// 两个指针变量持有相同的内存地址,那么它们就是相等
// 指向结构的指针
// 与字符串和数值不一样,复合字面量的前面可以放置 &
// 访问字段时,对结构体进行解引用并不是必须的
var person struct {
age int
}
person.age = 16
timmy := &person
timmy.age = 18
(*timmy).age = 18
// 指向数组的指针
// 和结构体一样,可以把 & 放在数组的复合字面值前面来创建指向数组的指针
// 数组在执行索引或切片操作时会自动解引用。没有必要写 (*superpower)[0] 这种形式
// 与 C 语言不一样,Go 里面数组和指针式两种完全独立的类型。
// Slice 和 map 的复合字面值前面也可以放置 & 操作符,但是 Go 并没有为它们提供自动解引用的功能
super := &[...]string{"flight", "super"}
fmt.Printf("type is %Tn", super) // type is *[2]string
fmt.Println(super[0]) // flight
fmt.Println(super[1:2]) // [super]
// 实现修改
// Go 语言的函数和方法都是按值传递参数的,这意味着函数总是操作于被传递参数的副本。
// 当指针被传递到函数时,函数将接收传入的内存地址的副本。之后函数可以通过解引用内存地址来修改指针指向的值。
// 指针接收者
// 方法的接收者和方法的参数在处理指针方面是很相似的,都会修改数据
// Go 语言在变量通过点标记法进行调用的时候,自动使用 & 取得变量的内存地址
// 所以不用写 (&nathan).birthday() 这种形式也可以正常运行
// 如果一种类型的某些方法需要用到指针作为接收者,就应该为这种类型的所有方法都是用指针作为接收者
teer := &pers{age: 15}
teer.birthday()
fmt.Printf("%+vn", teer) // &{age:16}
fmt.Printf("%+vn", *teer) // {age:16}
nath := pers{age: 16}
nath.birthday() // 推荐这么写
(&nath).birthday()
fmt.Printf("%+vn", nath) // {age:18}
// 内部指针
// Go 语言提供了 内部指针 这种特性。
// 它用于确定结构体中指定字段的内存地址。
// & 操作符不仅可以获得结构体的内存地址,还可以获得结构体中指定字段的内存地址
player := character{}
levelUp(&player.stats)
fmt.Printf("%+vn", player.stats) // {level:1}
// 修改数组
// 函数通过指针对数组的元素进行修改
var board [8][8]int
boardPointer := &board
boardPointer[6][6] = 5
fmt.Println(board[6][6]) // 5
// 隐式的指针
// Go 语言里一些内置的集合类型就在暗中使用指针。
// map 在被赋值或者呗作为参数传递的时候不会被复制。
// map 就是一种隐式指针。
// 这种写法就是多此一举:func demolish(planets *map[string]string)
// map 的键和值都可以是指针类型
// 需要将指针指向 map 的情况并不多见
// slice 指向数组
// 之前说过 slice 是指向数组的窗口,实际上 slice 在指向数组元素的时候也使用了指针。
// 每个 slice 内部都会被表示为一个包含 3 个元素的结构,它们分别指向:
// 数组的指针
// slice 的容量
// slice 的长度
// 当 slice 被直接传递至函数或方法时,slice 的内部指针就可以对底层数据进行修改。
// 指向 slice 的显式指针的唯一作用就是修改 slice 本身:slice 的长度、容量以及起始偏移量
planets := []int{1, 2, 3}
planetsPointer := &planets
*planetsPointer = (*planetsPointer)[0:2]
fmt.Println(planets) // [1 2]
// 指针和接口
// 如果是方法是 类型 作为接受者 既可以传变量,也可以指针
shout(martian{}) // MARTIAN
shout(&martian{}) // MARTIAN
// 如果是方法是 类型指针 作为接受者 则只能传指针
pew := laser(2)
shout(&pew) // HELLOHELLO
}
// 指针和接口
type talker interface {
talk() string
}
func shout(t talker) {
louder := strings.ToUpper(t.talk())
fmt.Println(louder)
}
// 方法是 类型 作为接受者
type martian struct{}
func (m martian) talk() string {
return "martian"
}
// 方法是 类型指针 作为接受者
type laser int
func (l *laser) talk() string {
return strings.Repeat("hello", int(*l))
}
17. nil
package main
import (
"fmt"
)
// 保护你的方法
type person struct {
age int
}
func (p *person) brithday() {
if p == nil {
return
}
p.age++ // 这里自动解引用,本来是(*p).age++,所以要考虑nil的情况
}
func main() {
// Nil 是一个名词,表示“无”或者“零”。
// 在 Go 里,nil 是一个零值。
// 如果一个指针没有明确的指向,那么它的值就是 nil
// 除了指针,nil 还是 slice、map 和接口的零值。
// Go 语言的 nil,比以往语言中的 null 更为友好,并且用的没那么频繁,但是仍需谨慎使用
// nil 会导致 panic(恐慌)
// 如果指针没有明确的指向,那么程序将无法对其实施的解引用。
// 尝试解引用一个 nil 指针将导致程序崩溃
var here *int
fmt.Println(here) //
// fmt.Println(*here) // 这个语句会出现下面这个错误
// panic: runtime error: invalid memory address or nil pointer dereference
if here != nil {
fmt.Println(*here)
} else {
fmt.Println("panic")
}
// panic
// 保护你的方法
// 避免 nil 引发 panic
// 因为值为 nil 的接收者和值为 nil 的参数在行为上并没有区别,所以 Go 语言即使在接收者为 nil 的情况下,也会继续调用方法
// 例:上面的person类型
// nil 函数值
// 当变量被声明为函数类型时,它的默认值是 nil。
// 检查函数值是否为 nil,并在有需要时提供默认行为(推荐:最佳实践)(为nil的函数,写了新函数)
var fn func(a int) int
fmt.Println(fn == nil) // true
// nil slice
// 如果 slice 在声明之后没有使用复合字面值或内置的 make 函数进行初始化,那么它的值就是 nil。
// 幸运的是,range、len、append 等内置函数都可以正常处理值为 nil 的 slice。
// 虽然空 slice 和值为 nil 的 slice 并不相等,但它们通常可以替换使用
var soup []string
fmt.Println(soup == nil) // true
fmt.Println(len(soup)) // 0
// nil map
// 和 slice 一样,如果 map 在声明后没有使用复合字面值或内置的 make 函数进行初始化,那么它的值将会是默认的 nil
// 对 nil map的 取值 range 都不会报错
// nil 接口
// 声明为接口类型的变量在未被赋值时,它的零值是 nil。
// 对于一个未被赋值的接口变量来说,它的接口类型和值都是 nil,并且变量本身也等于 nil
var v interface{}
fmt.Printf("%T, %v, %vn", v, v, v == nil) // , , true
// 当接口类型的变量被赋值后,接口就会在内部指向该变量的类型和值。
var p *int
v = p
fmt.Printf("%T, %v, %vn", v, v, v == nil) // *int, , false
// 在 Go 中,接口类型的变量只有在类型和值都为 nil 时才等于 nil。
// 即使接口变量的值仍为 nil,但只要它的类型不是 nil,那么该变量就不等于 nil。
// 检验接口变量的内部表示: 类型和值
fmt.Printf("%#vn", v) // (*int)(nil)
// nil 之外的另一个选择
// 对于是否为nil,可以加一个boolean值来判断
}
18. error
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
)
// 文件写入 和 defer
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
// 延迟调用 在函数proverbs函数调用结束前会执行这个语句,其他地方的close就可以删掉了
defer f.Close()
_, err = fmt.Fprintln(f, "hello go")
if err != nil {
f.Close()
return err
}
_, err = fmt.Fprintln(f, "hhhhhh")
f.Close()
return err
}
// 有创意的错误处理,把错误存储在结构体里
type safeWeiter struct {
w io.Writer
err error
}
func (sw *safeWeiter) writeln(s string) {
if sw.err != nil {
return
}
_, sw.err = fmt.Fprintln(sw.w, s)
}
func writeFile(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
defer f.Close()
sw := safeWeiter{w: f}
sw.writeln("ffffff")
sw.writeln("ffffff")
sw.writeln("ffffff")
return sw.err
}
// 按需返回错误
var (
ErrBounds = errors.New("out of bounds")
ErrDigit = errors.New("invalid digit")
)
func testErr() error {
return ErrBounds
}
// SudokuError ...
type SudokuError []error // 自定义错误
func (se SudokuError) Error() string { // 实现了Error接口
var s []string
for _, err := range se {
s = append(s, err.Error())
}
return strings.Join(s, ", ")
}
func testErrors() error {
var errors SudokuError
errors = append(errors, ErrBounds)
errors = append(errors, ErrDigit)
return errors
}
func main() {
// 处理错误
// Go 语言允许函数和方法同时返回多个值
// 按照惯例,函数在返回错误时,最后边的返回值应用来表示错误。
// 调用函数后,应立即检查是否发生错误。
// 如果没有错误发生,那么返回的错误值为 nil
files, err := ioutil.ReadDir(".") // 读当前目录下的目录和文件
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, file := range files {
fmt.Println(file.Name())
}
// 优雅的错误处理
// 减少错误处理代码的一种策略是:将程序中不会出错的部分和包含潜在错误隐患的部分隔离开来。
// 对于不得不返回错误的代码,应尽力简化相应的错误处理代码
// 文件写入
// 写入文件的时候可能出错:
// 路径不正确
// 权限不够
// 磁盘空间不足
// …
// 文件写入完毕后,必须被关闭,确保文件被刷到磁盘上,避免资源的泄露。
err = proverbs("test.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// 内置类型 error
// 内置类型 error 用来表示错误
// defer 关键字 表示 延迟
// 使用 defer 关键字,Go 可以确保所有 deferred 的动作可以在函数返回前执行。
// 可以 defer 任意的函数和方法。
// defer 并不是专门做错误处理的。
// defer 可以消除必须时刻惦记执行资源释放的负担
// 有创意的错误处理
err = writeFile("test.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// New error
// errors 包里有一个构造用 New函数,它接收 string 作为参数用来表示错误信息。该函数返回 error 类型
err = errors.New("here is error")
if err != nil {
fmt.Println(err)
}
// here is error
// 提示
// 错误信息应具有信息性
// 可以把错误信息当作用户界面的一部分,无论对最终用户还是开发者
// 按需返回错误
// 按照惯例,包含错误信息的变量名应以 Err 开头。
err = testErr()
if err != nil {
fmt.Println(err) // out of bounds
}
// errors.New 这个构造函数是使用指针实现的,所以上例中的 switch 语句比较的是内存地址,而不是错误包含的文字信息
if err != nil {
switch err {
case ErrBounds, ErrDigit:
fmt.Println("shuchu自定义错误")
default:
fmt.Println(err)
}
}
// shuchu自定义错误
// 自定义错误类型
// error 类型是一个内置的接口:任何类型只要实现了返回 string 的 Error() 方法就满足了该接口。
// 可以创建新的错误类型。
// 按照惯例,自定义错误类型的名字应以 Error 结尾。
// 有时候名字就是 Error,例如 url.Error
err = testErrors()
if err != nil {
switch err {
case ErrBounds, ErrDigit:
fmt.Println("shuchu自定义错误")
default:
fmt.Println(err)
}
}
// out of bounds, invalid digit
// 类型断言
// 上例中,我们可以使用类型断言来访问每一种错误。
// 使用类型断言,你可以把接口类型转化成底层的具体类型。
// 例如:err.(SudokuError)
// 如果类型满足多个接口,那么类型断言可使它从一个接口类型转化为另一个接口类型。
if err, ok := err.(SudokuError); ok {
fmt.Printf("如果err是SudokuError类型,ok为true err is %vn", err)
}
// 如果err是SudokuError类型,ok为true err is out of bounds, invalid digit
// 不要恐慌(don’t panic)
// Go 没有异常,它有个类似机制 panic。
// 当 panic 发生,那么程序就会崩溃。
// 其它语言的异常 vs Go 的错误值
// 其它语言的异常在行为和实现上与 Go 语言的错误值有很大的不同:
// 如果函数抛出异常,并且附近没人捕获它,那么它就会“冒泡”到函数的调用者那里,如果还没有人进行捕获,那么就继续“冒泡”到更上层的调用者… 直到达到栈(Stack)的顶部(例如 main 函数)。
// 异常这种错误处理方式可被看作是可选的:
// 不处理异常,就不需要加入其它代码。
// 想要处理异常,就需要加入相当数量的专用代码。
// Go 语言中的错误值更简单灵活:
// 忽略错误是有意识的决定,从代码上看也是显而易见的
// 如何 panic
// Go 里有一个和其他语言异常类似的机制:panic。
// 实际上,panic 很少出现。
// 创建 panic:
// panic(“I forgot my towel”)
// panic 的参数可以是任意类型
// 错误值、panic、os.Exit ?
// 通常,更推荐使用错误值,其次才是 panic。
// panic 比 os.Exit 更好:panic 后会执行所有 defer 的动作,而 os.Exit 则不会。
// 有时候 Go 程序会 panic 而不是返回错误值
// var zero int
// _ = 42 / zero // panic: runtime error: integer divide by zero
// 保持冷静并继续
// 为了防止 panic 导致程序崩溃,Go 提供了 recover 函数。
// defer 的动作会在函数返回前执行,即使发生了 panic。
// 但如果 defer 的函数调用了 recover,panic 就会停止,程序将继续运行。
defer func() {
if e := recover(); e != nil {
fmt.Println(e)
fmt.Println("catch except")
}
}()
panic("i forget my towel")
// i forget my towel
// catch except
}
20. goroutine
package main
import (
"fmt"
"math/rand"
"strings"
"time"
)
func sleepOne() {
time.Sleep(time.Second)
fmt.Println("1休息一秒")
}
func sleepTwo(id int) {
time.Sleep(time.Second)
fmt.Println("2休息一秒", id)
}
func sleepThree(id int, c chan int) {
time.Sleep(time.Second)
fmt.Println("3休息一秒", id)
c <- id
}
func sleepFour(id int, c chan int) {
time.Sleep(time.Duration(rand.Intn(4000)) * time.Millisecond)
c <- id
}
// 地鼠装配线
func sourceGopher(downstream chan string) {
source := []string{"hello world", "a bad apple", "goodbye all"}
for _, v := range source {
downstream <- v
}
close(downstream)
}
func filterGopher(upstream, downstream chan string) {
for {
item, ok := <-upstream
if !ok {
close(downstream)
return
}
// 筛选字符串,有 bad 就舍弃
if !strings.Contains(item, "bad") {
downstream <- item
}
}
}
func printGopher(upstream chan string) {
for {
v, ok := <-upstream
if !ok {
return
}
fmt.Println(v)
}
}
// 常用模式
func filterGopherRange(upstream, downstream chan string) {
for item := range upstream {
if !strings.Contains(item, "bad") {
downstream <- item
}
}
close(downstream)
}
func main() {
// goroutine
// 在 Go 中,独立的任务叫做 goroutine
// 虽然 goroutine 与其它语言中的协程、进程、线程都有相似之处,但 goroutine 和它们并不完全相同
// Goroutine 创建效率非常高
// Go 能直截了当的协同多个并发(concurrent)操作
// 在某些语言中,将顺序式代码转化为并发式代码需要做大量修改
// 在 Go 里,无需修改现有顺序式的代码,就可以通过 goroutine 以并发的方式运行任意数量的任务
// 启动 goroutine
// 只需在调用前面加一个 go 关键字
go sleepOne() // 相当于开一个新线程,主线程结束,这个线程会被终止
time.Sleep(time.Second * 2)
// 不止一个 goroutine
// 每次使用 go 关键字都会产生一个新的 goroutine。
// 表面上看,goroutine 似乎在同时运行,但由于计算机处理单元有限,其实技术上来说,这些 goroutine 不是真的在同时运行
// 计算机处理器会使用“分时”技术,在多个 goroutine 上轮流花费一些时间
// 在使用 goroutine 时,各个 goroutine 的执行顺序无法确定
for i := 0; i < 3; i++ {
go sleepOne()
}
time.Sleep(time.Second * 2)
// goroutine 的参数
// 向 goroutine 传递参数就跟向函数传递参数一样,参数都是按值传递的(传入的是副本)
for i := 0; i < 3; i++ {
go sleepTwo(i)
}
time.Sleep(time.Second * 2)
// 2休息一秒 1
// 2休息一秒 2
// 2休息一秒 0
// 通道 channel
// 通道(channel)可以在多个 goroutine 之间安全的传值
// 通道可以用作变量、函数参数、结构体字段…
// 创建通道用 make 函数,并指定其传输数据的类型
// c := make(chan int)
// 通道 channel 发送、接收
// 使用左箭头操作符 <- 向通道发送值 或 从通道接收值
// 向通道发送值:c <- 99
// 从通道接收值:r := <- c
// 发送操作会等待直到另一个 goroutine 尝试对该通道进行接收操作为止
// 执行发送操作的 goroutine 在等待期间将无法执行其它操作
// 未在等待通道操作的 goroutine 让然可以继续自由的运行
// 执行接收操作的 goroutine 将等待直到另一个 goroutine 尝试向该通道进行发送操作为止
c := make(chan int) // 这个chan相当于 临界区 有才能取 空才能放
for i := 0; i < 3; i++ {
go sleepThree(i, c)
}
for i := 0; i < 3; i++ {
gopherID := <-c // 这里使用一个通道等待多个通道
fmt.Println("gopher", gopherID, " has finished sleeping")
}
// 3休息一秒 1
// gopher 1 has finished sleeping
// 3休息一秒 2
// gopher 2 has finished sleeping
// 3休息一秒 0
// gopher 0 has finished sleeping
// 使用 select 处理多个通道
// 等待不同类型的值。
// time.After 函数,返回一个通道,该通道在指定时间后会接收到一个其他通道发来的值(发送该值的 goroutine 是 Go 运行时的一部分)
// select 和 switch 有点像
// 该语句包含的每个 case 都持有一个通道,用来发送或接收数据
// select 会等待直到某个 case 分支的操作就绪,然后就会执行该 case 分支
// 注意:即使已经停止等待 goroutine,但只要 main 函数还没返回,仍在运行的 goroutine 将会继续占用内存
for i := 0; i < 5; i++ {
go sleepFour(i, c)
}
timeout := time.After(2 * time.Second)
for i := 0; i < 5; i++ {
select {
case goID := <-c:
fmt.Println("gopher", goID, " has finished sleeping")
case <-timeout:
fmt.Println("timeout")
}
}
// gopher 2 has finished sleeping
// gopher 3 has finished sleeping
// timeout
// gopher 4 has finished sleeping
// gopher 0 has finished sleeping
// gopher 2 has finished sleeping
// select 语句
// select 语句在不包含任何 case 的情况下将永远等下去
// nil 通道
// 如果不使用 make 初始化通道,那么通道变量的值就是 nil(零值)
// 对 nil 通道进行发送或接收不会引起 panic,但会导致永久阻塞。
// 对 nil 通道执行 close 函数,那么会引起 panic
// nil 通道的用处:
// 对于包含 select 语句的循环,如果不希望每次循环都等待 select 所涉及的所有通道,那么可以先将某些通道设为 nil,等到发送值准备就绪之后,再将通道变成一个非 nil 值并执行发送操作。
// 阻塞和死锁
// 当 goroutine 在等待通道的发送或接收时,我们就说它被阻塞了。
// 除了 goroutine 本身占用少量的内存外,被阻塞的 goroutine 并不消耗任何其它资源。
// goroutine 静静的停在那里,等待导致其阻塞的事情来解除阻塞。
// 当一个或多个 goroutine 因为某些永远无法发生的事情被阻塞时,我们称这种情况为死锁。而出现死锁的程序通常会崩溃或挂起
go func() { c <- 2 }()
<-c // 单独只有这个语句就会到导致死锁,必须要先赋值再取值
// 地鼠装配线
// Go 允许在没有值可供发送的情况下通过 close 函数关闭通道
// 例如 close(c)
// 通道被关闭后无法写入任何值,如果尝试写入将引发 panic
// 尝试读取被关闭的通道会获得与通道类型对应的零值。
// 注意:如果循环里读取一个已关闭的通道,并没检查通道是否关闭,那么该循环可能会一直运转下去,耗费大量 CPU 时间
// 执行以下代码可得知通道是否被关闭:
// v, ok := <- c
c0 := make(chan string)
c1 := make(chan string)
go sourceGopher(c0)
go filterGopher(c0, c1)
printGopher(c1) // 等待另两个地鼠传值过来
// hello world
// goodbye all
// 常用模式
// 从通道里面读取值,直到它关闭为止
// 可以使用 range 关键字达到该目的
c2 := make(chan string)
c3 := make(chan string)
go sourceGopher(c2)
go filterGopherRange(c2, c3)
printGopher(c3) // 等待另两个地鼠传值过来
// hello world
// goodbye all
}
21. 并发 concurrent
package main
import (
"fmt"
"sync"
"time"
)
type visit struct {
mu sync.Mutex
visited map[string]int
}
func (v *visit) visitLink(url string) int {
v.mu.Lock()
defer v.mu.Unlock()
count := v.visited[url]
count++
v.visited[url] = count
return count
}
func worker() {
n := 0
next := time.After(time.Second)
for {
select {
case <-next:
n++
fmt.Println(n)
next = time.After(time.Second)
}
}
}
func main() {
// 并发状态
// 共享值: 临界区的临界资源
// 竞争条件(race condition):多个goroutine竞争一个共享值(保证同一时间内,只有一个goroutine在使用共享值)
// Go 的互斥锁(mutex)
// mutex = mutual exclusive
// Lock(),Unlock()
// sync 包
// 互斥锁定义在被保护的变量之上
var mn sync.Mutex
mn.Lock()
defer mn.Unlock()
// 互斥锁的隐患
// 死锁
// 为保证互斥锁的安全使用,我们须遵守以下规则:
// 尽可能的简化互斥锁保护的代码
// 对每一份共享状态只使用一个互斥锁
// 长时间运行的工作进程
// 工作进程(worker)
// 通常会被写成包含 select 语句的 for 循环。
go worker()
// 事件循环和 goroutine
// 事件循环(event loop)
// 中心循环(central loop)
// Go 通过提供 goroutine 作为核心概念,消除了对中心循环的需求
}



