栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Go语言

GO语言开发入门

Go语言 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

GO语言开发入门

GO语言开发入门
    • 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语言是一门编译语言

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
Go语言:

笔记参考杨旭大佬视频:https://www.bilibili.com/video/BV1fD4y1m7TD?from=search&seid=16942972441851783535

1. hello Word
// 文件名为: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 作为核心概念,消除了对中心循环的需求
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/944444.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号