Go语言编程快速入门
这个是学习B站杨旭视频做的记录,地址
安装
- https://studygolang.com/
- VsCode 安装 Go 插件
- ctrl+shift+p: 输入go查询,选择 Install/Update Tools,全部勾选,点击OK
- Go代理(执行命令后重启vscode)
#控制台执行命令 go env -w GO111MODULE=on go env -w GOPROXY=https://goproxy.cn,direct
switch
还有一个fallthrough关键字,它用来执行下一个case的body部分。(区别c#不需要关键字)
变量和作用域
短声明
-
在Go里,可以使用var来声明变量
-
但是也可以使用短声明
count :=10
-
不仅声明语句更短,而且可以在无法使用var的地方使用
for count := 10; count > 0; count-- { fmt.Println(count) }
整数类型
uint8
-
uint8可以用来表示8位的颜色(红绿蓝:0-255)
var red, green, blue uint8 = 0, 141, 213
十六进制表示法
-
Go语言里,在数前面加上0x前缀,就可以用十六进制的形式来表示数值
var red, green, blue uint8 = 0, 141, 213 var red, green, blue uint8 = 0x00, 0x8d, 0xd5
整数环绕
- 所有的整数类型都有一个取值范围,超出这个范围,就会发生“环绕”
整数类型的最大值、最小值
-
math包里,为与架构无关的整数类型,定义了最大、最小值常量:
math.MaxInt16 math.MinInt64
-
而int和uint,可能是32或64位的。
比较大的数
数太大了怎办?
- 浮点类型可以存储非常大的数值,但是精度不高
- 整型很精确,但是取值范围有限
- 如果你需要很大的数,而且要求很精确,那么怎么办
- nt64可以容纳很大的数,如果还不行,那么:
- int64可以容纳更大的正数,如果还不行,那么:
- 也可以凑合用浮点类型,但是还有另外一种方法:
- 使用big包。
big包
- 对于较大的整数(超过1018):big.lnt
- 对于任意精度的浮点类型,big.Float
- 对于分数,big.Rat
- 缺点:用起来繁琐,速度比较慢
多语言文本
字符串字面值/原始字符串字面值
- 字符串字面值可以包含转义字符,例如 \n
- 但如果你确实想得到\n而不是换行的话,可以使用`来代替“,这叫做原始字符串字面值。
字符,code points,runes,bytes
- Unicode联盟为超过100万个字符分配了相应的数值,这个数叫做code point.
- 例如:65代表A
- 为了表示这样的unicode code point,Go语言提供了rune这个类型,它是int32的一个类型别名。
- 而byte是uint8类型的别名,目的是用于二进制数据。
- byte 倒是可以表示由ASCIl 定义的英语字符,它是Unicode 的一个子集(共128个字符)
类型别名
-
类型别名就是同一个类型的另一个名字。
- 所以,rune和int32可以互换使用。
-
也可以自定义类型别名,语法如下:
type byte = uint8 type rune = int32
打印
- 如果想打印字符而不是数值,使用%c格式化动词
- 任何整数类型都可以使用%c打印,但是rune意味着该数值表示了一个字符
Go的内置函数
-
len是Go语言的一个内置函数。
str := "hello word" fmt.Println(len(str))
-
本例中 len 返回 message所占的byte 数。
-
Go有很多内置函数,它们不需要import
-
使用utf-8包,它提供可以按rune计算字符串长度的方法。
-
ecodeRunelnString函数会返回第一个字符,以及字符所占的字节数。
str := "hello word,你好呀" fmt.Println(len(str), "bytes") fmt.Println(utf8.RuneCountInString(str), "runes") c, size := utf8.DecodeRuneInString(str) fmt.Printf("First runs: %c %v bytes", c, size)
range
-
·使用range关键字,可以遍历各种集合
str := "hello word,你好呀" for i, c := range str { fmt.Printf("%v %c\n", i, c) }
类型转换
类型转换时需谨慎
- 环绕行为
- 可以通过math包提供的max、min 常量,来判断是否超过最大最小值
字符串转换
-
想把数值转化为string,它的值必须能转化为codepoint。string(65)
-
trconv包的Itoa函数:
num := 10 fmt.Printf("数字是:" + strconv.Itoa(num))
-
另外一种把数值转化为string的方式是使用Sprintf函数,和Printf略类似,但是会返回一个string:
countdown := 9 str := fmt.Sprintf("Launch in T minus %v seconds.", countdown) fmt.Println(str)
-
strconv包里面还有个Atoi(ASCll to Integer)函数。
-
由于字符串里面可能包含任意字符,或者要转换的数字字符串太大,所以Atoi函数可能会发生错误:
countdown,err:=strconv.Atoi("10") if err!=nil{ // oh no,something went wrong } fmt.Println(countdown)
-
如果err的值为nil,那么就代表没发生错误。
函数
函数声明
-
使用func关键字声明函数
func Intn(n int) int
-
在Go里,大写字母开头的函数、变量或其它标识符都会被导出,对其它包可用。
-
小写字母开头的就不行(私有函数吧)
函数声明-多个参数
-
函数的参数可以是多个:
- func Unix(sec int64,nsec int64) Time
- 调用:future := time.Unix(12622780800,0)
-
函数声明时,如果多个形参类型相同,那么该类型只写一次即可:
- func Unix(sec int64,nsec int64)Time
- func Unix(sec,nsec int64)Time
- 这种简化是可选的
函数声明-返回多个值
- Go的函数可以返回多个值:
- cuntdown, err := strconv.Atoi(10")
- 该函数的声明如下:
- func Atoi(s string) (i int, err error)
- 函数的多个返回值需要用括号括起来,每个返回值名字在前,类型在后。声明函数时可以把名字去掉,只保留类型:
- func Atoi(s string) (int, error)
函数声明-可变参数函数
-
Println是一个特殊的函数,它可以接收一个、二个甚至多个参数,参数类型还可以不同:
fmt.Println("Hello,playground") fmt.Println(186,"seconds")
-
Println的声明是这样的:
- func Println(a ...interface) (n int, err error)
- ….表示函数的参数的数量是可变的。
- 参数a的类型为interface{},是一个空接口。
-
... 和空接口组合到一起就可以接受任意数量、类型的参数了
方法
声明新类型
-
关键字type可以用来声明新类型:
type celsis float64 var temperature celsis = 20
-
虽然Celsius是一种全新的类型,但是由于它和float64具有相同的行为和表示,所以赋值操作能顺利执行。
-
为什么要声明新类型:极大的提高代码可读性和可靠性
-
不同的类型是无法混用的
通过方法添加行为
-
在C#、Java里,方法属于类
-
在Go里,它提供了方法,但是没提供类和对象
-
Go比其他语言的方法要灵活
-
可以将方法与同包中声明的任何类型相关联,但不可以是int、float64等预声明的类型进行关联。
type celsius float64 type kelvin float64 // 函数 func kelvinToCelsius(k kelvin) celsius { return celsius(k - 273.15) } // 方法 func (k kelvin) celsius() celsius { return celsius(k - 273.15) }
-
上例中,celsius方法虽然没有参数。但它前面却又一个类型参数的接收者。
-
每个方法可以有多个参数,但只能有一个接收者。
-
在方法体中,接收者的行为和其它参数一样。
一等函数
- 在Go里,函数是头等的,它可以用在整数、字符串或其它类型能用的地方:
- 将函数赋给变量
- 将函数作为参数传递给函数
- 将函数作为函数的返回类型
将函数赋给变量
type kelvin float64
func fakeSensor() kelvin {
return kelvin(rand.Intn(151) + 150)
}
func realSensor() kelvin {
return 0
}
func main() {
sensor := fakeSensor
fmt.Println(sensor())
sensor = realSensor
fmt.Println(sensor())
}
将函数传递给其他函数
import (
"fmt"
"math/rand"
"time"
)
type kelvin float64
func measureTemperature(samples int, sensor func() kelvin) {
for i := 0; i < samples; i++ {
k := sensor()
fmt.Printf("%v K\n", k)
time.Sleep(time.Second)
}
}
func fakeSensor() kelvin {
return kelvin(rand.Intn(151) + 150)
}
func main() {
measureTemperature(3, fakeSensor)
}
声明函数类型
-
为函数声明类型有助于精简和明确调用者的代码。
-
例如:type sensor func() kelvin
-
所以: func measureTemperature(samples int,s func() kelvin)
-
可以精简为:func measureTemperature(samples int,s sensor)
-
闭包和匿名函数
-
匿名函数就是没有名字的函数,在Go里也称作函数字面值。
-
因为函数字面值需要保留外部作用域的变量引用,所以函数字面值都是闭包的。‘
import "fmt" type kelvin float64 // sensor function type type sensor func() kelvin func realSensor() kelvin { return 0 } func calibrate(s sensor, offset kelvin) sensor { return func() kelvin { return s() + offset } } func main() { sensor := calibrate(realSensor, 5) //这里,虽然已经返回函数。但是参数已经被封在里面,下面执行仍然可以访问 fmt.Println(sensor()) }
-
闭包(closure)就是由于匿名函数封闭并包围作用域中的变量而得名的
数组
-
数组是一种固定长度且有序的元素集合。
var planets [8]string
-
数组中的每个元素都可以通过1和一个从0开始的索引进行访问。
-
数组的长度可由内置函数len来确定。
-
在声明数组时,未被赋值元素的值是对应类型的零值
数组越界
- Go编译器在检测到对越界元素的访问时会报错
- 如果Go编译器在编译时未能发现越界错误,那么程序在运行时会出现panic
- Panic会导致程序崩溃
使用复合字面值初始化数组
-
复合字面值(compositeliteral)是一种给复合类型初始化的紧语法。
-
go的复合字面值语法允许我们只用一步就完成数组声明和数组初始化两步操作:
dwarfs := [5]string{"1", "2", "3", "4", "5"}
-
可以在复合字面值里使用…作为数组的长度,这样Go编译器会为你算出数组的元素数量
planets := [...]string{ "1", "2",}
-
无论哪种方式,数组的长度都是固定的。
遍历数组
- for循环
- range
dwarfs := [5]string{"1", "2", "3", "4", "5"}
for i := 0; i < len(dwarfs); i++ {
fmt.Println(i, dwarfs[i])
}
for i, dwarf := range dwarfs {
fmt.Println(i, dwarf)
}
数组的复制
- 无论数组赋值给新的变量还是将它传递给函数,都会产生一个完整的数组副本。(c#的值类型?)
- 数组也是一种值,函数通过值传递来接受参数。所以数组作为函数的参数就非常低效
- 数组的长度也是数组类型的一部分
- 尝试将长度不符的数组作为参数传递,将会报错。
- 函数一般使用slice而不是数组作为参数。
Slice(切片)
Slice指向数组的窗口
- 假设planets是一个数组,那么planets[0:4]就是一个切片,它切分出了数组里前4个元素。
- 切分数组不会导致数组被修改,它只是创建了指向数组的一个窗口或视图,这种视图就是slice类型。
- Slice使用的是半开区间
- 例如planets[0:4],包含索引0、1、2、3对应的元素,不包括索引4对应的元素。
Slice的默认索引
- 忽略掉slice的起始索引,表示从数组的起始位置进行切分;
- 忽略掉slice的结束索引,相当于使用数组的长度作为结束索引。
- 注意:slice的索引不能是负数。
- 如果同时省略掉起始和结束索引,那就是包含数组所有元素的一个slice.
- 切分数组的语法也可以用于切分字符串
- 切分字符串时,索引代表的是字节数而非rune的数。
Slice的复合字面值
-
Go里面很多函数都倾向于使用slice而不是数组作为参数。
-
想要获得与底层数组相同元素的slice,那么可以使用[:]进行切分
-
切分数组并不是创建slice唯一的方法,可以直接声明slice:
slices := []string{"1", "2"}
更大的slice
append函数
-
append函数也是内置函数,它可以将元素添加到slice里面。
slices := []string{"1", "2"} slices = append(slices, "3","4")
长度和容量(length & capacity)
- slice中元素的个数决定了slice的长度。
- 如果slice的底层数组比slice还大,那么就说该slice还有容量可供增长。
三个索引的切分操作
-
G01.2中引入了能够限制新建切片容量的三索引切分操作。
planets := []string{"1", "2", "3", "4", "5", "6"} terrestrial := planets[0:4] words := append(terrestrial, "100") fmt.Println(planets, terrestrial, words)
-
上面执行append会修改planets。如果不想这样,可以指定长度,append后会复制到一个新的数据。terrestrial := planets[0:44]
使用make 函数对slice进行预分配
- 当slice的容量不足以执行append操作时,Go必须创建新数组并复制旧数组中的内容。
- 但通过内置的make函数,可以对slice进行预分配策略。
- 尽量避免额外的内存分配和数组复制操作。
dwarfts:=make([]string, 0,10)
dwarfts:=make([]string, 10)
声明可变参数的函数
-
声明Printf、append这样的可变参数函数,需要在函数的最后一个参数前面加上符号。
import "fmt" func terraform(prefix string, wolds ...string) []string { newWorlds := make([]string, len(wolds)) for i := range wolds { newWorlds[i] = prefix + " " + wolds[i] } return newWorlds } func main() { twoWorlds := terraform("New", "Venus", "Mars") fmt.Println(twoWorlds) planets := []string{"Venus", "Mars", "Jupiter"} newPlanets := terraform("new", planets...) fmt.Println(newPlanets) }
Map(c#的字典)
- Map是Go提供的另外一种集合:
- 它可以将key映射到value。
- 可快速通过key找到对应的value
- 它的key几乎可以是任何类型
声明Map
-
声明map,必须指定key和value的类型:
temperature := map[string]int{ "Earth": 15, "Mars": -65, }
逗号与ok写法
temperature := map[string]int{
"Earth": 15,
"Mars": -65,
}
//没有的话,这样会添加一个key,value
temperature["Venus"] = 100
if moon, ok := temperature["Moon"]; ok {
fmt.Println("on average the moon is %v C.\n", moon)
} else {
fmt.Println("Where is the moon?")
}
map 不会被复制
- 数组、int、float64等类型在赋值给新变量或传递至函数l方法的时候会创建相应的副本
- 但map不会(c#的引用类型)
使用 make 函数对 map 进行预分配
-
除非你使用复合字面值来初始化map,否则必须使用内置的make函数来为map分配空间。
dic := make(map[string]int, 8)
-
创建map时,make函数可接受一个或两个参数
- 第二个参数用于为指定数量的key预先分配空间
-
使用make函数创建的map的初始长度为0
使用 map 和 slice 实现数据分组
temperature := []float64{
-28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}
groups := make(map[float64][]float64)
for _, t := range temperature {
g := math.Trunc(t/10) * 10
groups[g] = append(groups[g], t)
}
for g, temperature := range groups {
fmt.Println("%v: %v\n", g, temperature)
}
结构类型(struct)
- 为了将分散的零件组成一个完整的结构体,Go提供了struct类型。
- struct允许你将不同类型的东西组合在一起。
var curiosity struct {
lat float64
long float64
}
通过类型复用结构体
type location struct {
lat float64
long float64
}
var spirit location
spirit.lat = 1
spirit.long = 2
通过复合字面值初始化struct
-
通过成对的字段和值进行初始化
type location struct { lat, long float64 } opportunitu := location{lat: 1, long: 2}
-
按字段定义的顺序进行初始
spirit = location{1, 2}
由结构体组成的slice
type location struct {
name string
lat, long float64
}
locations := []location{
{name: "aa", lat: 1, long: 2},
{name: "bb", lat: 1, long: 2},
}
将struct编码为JSON
-
Json包的Marshal函数可以将struct中的数据转化为JsoN格式
type location struct { Lat, Long float64 } curiosity := location{1, 2} bytes, _ := json.Marshal(curiosity) fmt.Println(string(bytes))
-
Marshal函数只会对struct中被导出的字段进行编码。
使用struct标签来自定义JSON
-
go语言中的json 包要求struct 中的字段必须以大写字母开头,类似CamelCase驼峰型命名规范。
-
可以为字段注明标签,使得ison包在进行编码的时候能够按照标签里的样式修改字段名
type location struct { Lat float32 `json:"latitude"` Long float64 `json:longitude` }
Go语言没有class
- Go和其它经典语言不同,它没有class,没有对象,也没有继承。
- 但是Go提供了struct和方法。
构造函数
-
可以使用struct复合字面值来初始化你所要的数据。
-
但如果 struct初始化的时候还要做很多事情,那就可以考虑写一个构造用的函数
-
Go语言没有专用的构造函数,但以new或者New开头的函数,通常是用来构造数据的。例如newPerson(),NewPerson()
type location struct{ lat,long float64 } //初始化使用 func newLocation(lat,long float64) location { return location{lat,long} }
New函数
- 有一些用于构造的函数的名称就是New(例如errors包里面的New函数)。
- 这是因为函数调用时使用包名.函数名的形式。
- 如果该函数叫NewError,那么调用的时候就是errors.NewError(),这就不如errors.New()简洁
组合与转发
组合
- 在面向对象的世界中,对象由更小的对象组合而成。
- 术语:对象组合或组合
- Go通过结构体实现组合(composition)。
- Go提供了“嵌入”(embedding)特性,它可以实现方法的转发(forwarding)
- 组合是一种更简单、灵活的方式。
组合机构体
type report struct {
sol int
temperature temperature
location location
}
type temperature struct {
high, low celsius
}
type location struct {
lat, long float64
}
type celsius float64
转发方法
- Go可以通过struct嵌入来实现方法的转发。
- 在struct中只给定字段类型,不给定字段名即可
- 在struct中,可以转发任意类型
type report struct {
sol int
temperature
location
}
type temperature struct {
high, low celsius
}
type location struct {
lat, long float64
}
type celsius float64
func (t temperature) average() celsius {
return (t.high + t.low) / 2
}
func main() {
bradbury := location{-4.5895, 137.4417}
t := temperature{high: -1.0, low: -78}
report := report{
sol: 15,
temperature: t,
location: bradbury,
}
//report可以直接调用temperature的方法
fmt.Print(report.average())
//也可以直接访问字段
fmt.Print(report.high)
}
继承 还是 组合
- 优先使用对象组合而不是类的继承。
- 对传统的继承不是必需的;所有使用继承解决的问题都可以通过其它方法解决。
接口
- 接口关注于类型可以做什么,而不是存储了什么。
- 接口通过列举类型必须满足的一组方法来进行声明。
- 在Go语言中,不需要显式声明接口。
接口类型
- 为了复用,通常会把接口声明为类型。
- 按约定,接口名称通常以er结尾。
- 接口可以与struct嵌入特性一同使用
- 同时使用组合和接口将构成非常强大的设计工具。
import "fmt"
type talker interface {
talk() string
}
type martain struct{}
func (m martain) talk() string {
return "nack nack"
}
func shout(t talker) {
fmt.Println(t.talk())
}
type starship struct {
martain
}
func main() {
shout(martain{})
s := starship{martain{}}
fmt.Println(s.talk())
}
满足接口
-
go标准库导出了很多只有单个方法的接口。
-
go通过简单的、通常只有单个方法的接口….来鼓励组合而不是继承,这些接口在各个组件之间形成了简明易懂的界限。
-
例如fmt包声明的Stringer接口:
type Stringer interface{ String() string }
-
标准库中常用接口还包括:io.Reader,io.Writer,json.Marshaler...
指针
什么是指针
- 指针是指向另一个变量地址的变量。
- Go语言的指针同时也强调安全性,不会出现迷途指针(danglingpointers)
& 和 * 符号
- 变量会将它们的值存储在计算机的RAM里,存储位置就是该变量的内存地址。
- & 表示地址操作符,通过 & 可以获得变量的内存地址
- & 作符无法获得字符串/数值/布尔字面值的地址。
- &42,&“hello”这些都会导致编译器报错
-
- 操作符与 & 的作用相反,它用来解引用,提供内存地址指向的值。
注意
- C语言中的内存地址可以通过例如 address++ 这样的指针运算进行操作,但是在Go里面不允许这种不安全操作。
内存类型
- 指针存储的是内存地址
- 指针类型和其它普通类型一样,出现在所有需要用到类型的地方,如变量声明、函数形参、返回值类型、结构体字段等
- 将*放在类型前面表示声明指针类型
- 将*放在变量前面表示解引用操作
指针就是用来指向的
- 两个指针变量持有相同的内存地址,那么它们就是相等。
func main() {
var administrator *string
scolese := "huihui scolese"
administrator = &scolese
fmt.Println(*administrator)
bolden := "huihui bolden"
administrator = &bolden
fmt.Println(*administrator)
bolden = "huihui bolden change"
fmt.Println(*administrator)
*administrator = "Maj. Gen. Charles Frank Bolden Jr."
fmt.Println(bolden)
major := administrator
*major = "hahaha"
fmt.Print(bolden)
fmt.Println(administrator == major)
}
指向结构的指针
- 与字符串和数值不一样,复合字面量的前面可以放置&
- 访问字段时,对结构体进行解引用并不是必须
func main() {
type person struct {
name, superpower string
age int
}
timmy := &person{
name: "huihui",
age: 10,
}
(*timmy).superpower = "aaa"
timmy.superpower = "bbb"
fmt.Print(timmy)
}
指向数组的指针
-
和结构体一样,可以把&放在数组的复合字面值前面来创建指向数组的指针。
func main() { superpowers := &[3]string{"1", "2", "3"} fmt.Println(superpowers[0]) fmt.Println(superpowers[1:2]) }
-
数组在执行索引或切片操作时会自动解引用。没有必要写(*superpower)[o]这种形式。
-
与C语言不一样,Go里面数组和指针式两种完全独立的类型。
-
Slice和map的复合字面值前面也可以放置&操作符,但是Go并没有为它们提供自动解引用的功能。
实现修改
- Go语言的函数和方法都是按值传递参数的,这意味着函数总是操作于被传递参数的副本。
- 当指针被传递到函数时,函数将接收传入的内存地址的副本。之后函数可以通过解引用内存地址来修改指针指向的值。
指针接收者
- 方法的接收者和方法的参数在处理指针方面是很相似的。
- Go语言在变量通过点标记法进行调用的时候,自动使用&取得变量的内存地址。
- 所以不用写(&anathan).birthday()这种形式也可以正常运行。
type person struct {
name string
age int
}
func (p *person) birthday() {
p.age++
}
func main() {
terry := &person{
name: "terry",
age: 15,
}
terry.birthday()
fmt.Println(terry)
mathan := person{
name: "Nathan",
age: 7,
}
mathan.birthday()
fmt.Println(mathan)
}
注意
- 使用指针作为接收者的策略应该始终如一:
- 如果一种类型的某些方法需要用到指针作为接收者,就应该为这种类型的所有方法都是用指针作为接收者。
内部指针
- Go语言提供了内部指针这种特性。
- 它用于确定结构体中指定字段的内存地址。
- & 操作符不仅可以获得结构体的内存地址,还可以获得结构体中指定字段的内存地址。&animal.dog
修改数组
-
函数通过指针对数组的元素进行修改。
func reset(board *[8][8]rune) { board[0][0] = 'r' } func main() { var board [8][8]rune reset(&board) fmt.Println(board[0][0]) }
隐式的指针
- Go语言里一些内置的集合类型就在暗中使用指针。
- map在被赋值或者呗作为参数传递的时候不会被复制。
- map就是一种隐式指针。
- 这种写法就是多此一举:func demolish(planets *map[string]string)
- map的键和值都可以是指针类型
- 需要将指针指向map的情况并不多见
slice 指向数组
-
之前说过 slice是指向数组的窗口,实际上slice在指向数组元素的时候也使用了指针。
-
每个slice内部都会被表示为一个包含3个元素的结构,它们分别指向:
- 数组的指针
- slice 的容量
- slice的长度
-
当slice被直接传递至函数或方法时,slice的内部指针就可以对底层数据进行修改。
-
指向 slice 的显式指针的唯一作用就是修改slice 本身:slice的长度、容量以及起始偏移量。
func reclassify(planets *[]string) { *planets = (*planets)[0:2] } func main() { planets := []string{ "Mercury", "Venus", "Earth", "Mars", "Pluto", } reclassify(&planets) fmt.Println(planets) }
-
注意:slice超长后,会复制出一个新的
指针和接口
- 本例中,无论martian还是指向martian的指针,都可以满足talker接口。
- 如果方法使用的是指针接收者,那么情况会有所不同。
import (
"fmt"
"strings"
)
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 "nack nack"
}
func main() {
shout(martian{})
shout(&martian{})
}
明智的使用指针
- 应合理使用指针,不要过度使用指针。
nil
- Ni是一个名词,表示“无”或者“零”
- 在Go里,nil是一个零值。
- 如果一个指针没有明确的指向,那么它的值就是nil
- 除了指针,nil还是slice、map和接口的零值。
- Go语言的nil,比以往语言中的null 更为友好,并且用的没那么频繁,但是仍需谨慎使用。
nil 会导致 panic
- 如果指针没有明确的指向,那么程序将无法对其实施的解引用。
- 尝试解引用一个nil指针将导致程序崩溃。
保护你的方法
- 避免nil引发panic
- 因为值为nil的接收者和值为nil的参数在行为上并没有区别,所以Go语言即使在接收者为nil的情况下,也会继续调用方法。
nil 函数值
- 当变量被声明为函数类型时,它的默认值是nil。
- 检查函数值是否为nil,并在有需要时提供默认行为。
nil slice
- 如果slice在声明之后没有使用复合字面值或内置的make函数进行初始化,那么它的值就是nil。
- 幸运的是,range、len、append等内置函数都可以正常处理值为nil的slice。
- 虽然空slice和值为nil的slice并不相等,但它们通常可以替换使用。
nil map
- 和slice一样,如果map在声明后没有使用复合字面值或内置的make函数进行初始化,那么它的值将会是默认的nil
nil 接口
- 声明为接口类型的变量在未被赋值时,它的零值是nil。
- 对于一个未被赋值的接口变量来说,它的接口类型和值都是nil,并且变量本身也等于nil。
- 当接口类型的变量被赋值后,接口就会在内部指向该变量的类型和值。
- 在Go中,接口类型的变量只有在类型和值都为nil时才等于nil。
- 即使接口变量的值仍为nil,但只要它的类型不是nil,那么该变量就不等于nil。
- 检验接口变量的内部表示
错误
错误处理
- go语言允许函数和方法同时返回多个值
- 按照惯例,函数在返回错误时,最后边的返回值应用来表示错误。
- 调用函数后,应立即检查是否发生错误。
- 如果没有错误发生,那么返回的错误值为nil。
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
files, err := ioutil.ReadDir(".")
if err == nil {
fmt.Println(err)
os.Exit(1)
}
for _, file := range files {
fmt.Println(file.Name())
}
}
优雅的错误处理
- 减少错误处理代码的一种策略是:将程序中不会出错的部分和包含潜在错误隐患的部分隔离开来。
- 对于不得不返回错误的代码,应尽力简化相应的错误处理代码。
文件写入
- 写入文件的时候可能出错:
- 路径不正确
- 权限不够
- 磁盘空间不足
- 文件写入完毕后,必须被关闭,确保文件被刷到磁盘上,避免资源的泄露。
import (
"fmt"
"os"
)
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
_, err = fmt.Fprintln(f, "Errors are values")
if err != nil {
f.Close()
return err
}
_, err = fmt.Fprintln(f, "Don't just check errors,handle them gracefully.")
f.Close()
return err
}
func main() {
error := proverbs("proverbs.txt")
if error != nil {
fmt.Println(error)
os.Exit(1)
}
}
内置类型 error
- 内置类型error用来表示错误。
defer 关键字
- 使用defer关键字,Go可以确保所有deferred的动作可以在函数返回前执行。
- 可以defer任意的函数和方法。
- defer并不是专门做错误处理的。
- defer可以消除必须时刻惦记执行资源释放的负担
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
//不用写多遍close
defer f.Close()
_, err = fmt.Fprintln(f, "Errors are values")
if err != nil {
return err
}
_, err = fmt.Fprintln(f, "Don't just check errors,handle them gracefully.")
return err
}
有创意的错误处理
type safeWriter struct {
w io.Writer
err error
}
func (sw *safeWriter) writeln(s string) {
if sw.err != nil {
return
}
_, sw.err = fmt.Fprintln(sw.w, s)
}
New Error
- errors包里有一个构造用New函数,它接收string作为参数用来表示错误信息。该函数返回error类型。
按需返回错误
- 按照惯例,包含错误信息的变量名应以Err开头。
- errors.New这个构造函数是使用指针实现的,所以上例中的switch语句比较的是内存地址,而不是错误包含的文字信息。
自定义错误类型
- error类型是一个内置的接口:任何类型只要实现了返回string的Erroro)方法就满足了该接口。
- 可以创建新的错误类型。
- 按照惯例,自定义错误类型的名字应以Error结尾。
- 有时候名字就是Error,例如 url.Error
类型断言
- 上例中,我们可以使用类型断言来访问每一种错误。
- 使用类型断言,你可以把接口类型转化成底层的具体类型。
- 例如:err.(SudokuError)
- 如果类型满足多个接口,那么类型断言可使它从一个接口类型转化为另一个接口类型。
不要恐慌(don't panic)
- Go没有异常,它有个类似机制panic
- 当panic发生,那么程序就会崩溃。
其它语言的异常vs Go 的错误值
- 其它语言的异常在行为和实现上与Go语言的错误值有很大的不同:
- 如果函数抛出异常,并且附近没人捕获它,那么它就会“冒泡”到函数的调用者那里,如果还没有人进行捕获,那么就继续“冒泡”到更上层的调用者..直到达到栈(Stack)的顶部(例如main函数)。
- 常这种错误处理方式可被看作是可选的:
- 不处理异常,就不需要加入其它代码。
- 想要处理异常,就需要加入相当数量的专用代码。
- Go语言中的错误值更简单灵活:
- 忽略错误是有意识的决定,从代码上看也是显而易见的。
如何 panic
- Go里有一个和其他语言异常类似的机制:panic。
- 实际上,panic很少出现。
- 创建panic
- panic("I forgot my towel")
- panic 的参数可以是任意类型
- panic("I forgot my towel")
错误值、panic、os.Exit?
- 通常,更推荐使用错误值,其次才是panic。
- panic比os.Exit更好:panic后会执行所有defer的动作,而os.Exit则不会。
- 有时候Go程序会panic而不是返回错误值(除以0)
保持冷静并继续
- 为了防止panic导致程序崩溃,Go提供了recover函数。
- defer的动作会在函数返回前执行,即使发生了panic。
- 但如果defer的函数调用了recover,panic就会停止,程序将继续运行。
goroutine 和并发(concurrnet)
goroutine
- 在Go中,独立的任务叫做goroutine
- 虽然goroutine与其它语言中的协程、进程、线程都有相似之处,但goroutine和它们并不完全相同
- goroutine创建效率非常高
- Go能直截了当的协同多个并发(concurrent)操作
- 在某些语言中,将顺序式代码转化为并发式代码需要做大量修改
- 在Go里,无需修改现有顺序式的代码,就可以通过goroutine以并发的方式运行任意数量的任务。
启动goroutine
-
只需在调用前面加一个go关键字。
func sleepyGopher() { time.Sleep(3 * time.Second) fmt.Println("...snore...") } func main() { go sleepyGopher() //主线路 time.Sleep(2 * time.Second) //分支线路 }
-
如果main函数返回了,goroutine没执行完也不会执行了
不止一个 goroutine
- 每次使用go关键字都会产生一个新的goroutine。
- 表面上看,goroutine似乎在同时运行,但由于计算机处理单元有限,其实技术上来说,这些goroutine不是真的在同时运行。
- 计算机处理器会使用“分时”技术,在多个goroutine上轮流花费一些时间
- 在使用goroutine时,各个goroutine的执行顺序无法确定。
gorputine 的参数
- 向goroutine传递参数就跟向函数传递参数一样,参数都是按值传递的(传入的是副本)
通道 channel
- 通道(channel)可以在多个goroutine之间安全的传值。
- 通道可以用作变量、函数参数、结构体字段…
- 创建通道用make函数,并指定其传输数据的类型
- c:= make(chan int)
通道 channel 发送、接收
- 使用左箭头操作符 <- 向通道发送值或从通道接收值
- 向通道发送值:c <- 99
- 从通道接收值:r := <- c
- 发送操作会等待直到另一个goroutine尝试对该通道进行接收操作为止。
- 执行发送操作的goroutine在等待期间将无法执行其它操作
- 未在等待通道操作的goroutine让然可以继续自由的运行
- 执行接收操作的goroutine将等待直到另一个goroutine尝试向该通道进行发送操作为止。
func sleepyGopher(id int, c chan int) {
time.Sleep(3 * time.Second)
fmt.Println("...", id, " snore ... ")
c <- id
}
func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
go sleepyGopher(i, c)
}
for i := 0; i < 5; i++ {
gopherID := <-c
fmt.Println("gopher", gopherID, " has finished sleeping")
}
}
使用 select 处理多个通道
-
等待不同类型的值。
-
time.After函数,返回一个通道,该通道在指定时间后会接收到一个值(发送该值的goroutine是Go运行时的一部分)。
-
select和switch有点像。
- 该语句包含的每个case都持有一个通道,用来发送或接收数据。
- select会等待直到某个case分支的操作就绪,然后就会执行该case分支。
import ( "fmt" "math/rand" "time" ) func sleepyGopher(id int, c chan int) { time.Sleep(time.Duration(rand.Intn(4000)) * time.Millisecond) c <- id } func main() { c := make(chan int) for i := 0; i < 5; i++ { go sleepyGopher(i, c) } timeout := time.After(1 * time.Second) for i := 0; i < 5; i++ { select { case gopherID := <-c: fmt.Println("gopher", gopherID, " has finished sleeping") case <-timeout: fmt.Println("my patience ran out") return } } }
-
注意:即使已经停止等待goroutine,但只要main函数还没返回,仍在运行的goroutine将会继续占用内存。
-
select语句在不包含任何case的情况下将永远等下去。
nil 通道
- 如果不使用make初始化通道,那么通道变量的值就是nil(零值)
- 对nil通道进行发送或接收不会引起panic,但会导致永久阻塞。
- 对nil通道执行close函数,那么会引起panic
- nil通道的用处:
- 对于包含select语句的循环,如果不希望每次循环都等待select所涉及的所有通道,那么可以先将某些通道设为nil,等到发送值准备就绪之后,再将通道变成一个非nil值并执行发送操作。
阻塞和死锁
- 当goroutine在等待通道的发送或接收时,我们就说它被阻塞了。
- 除了goroutine本身占用少量的内存外,被阻塞的goroutine并不消耗任何其它资源。
- goroutine静静的停在那里,等待导致其阻塞的事情来解除阻塞。
- 当一个或多个goroutine因为某些永远无法发生的事情被阻塞时,我们称这种情况为死锁。而出现死锁的程序通常会崩溃或挂起。
地鼠装配线
- Go允许在没有值可供发送的情况下通过close函数关闭通道
- 例如 close(c)
- 通道被关闭后无法写入任何值,如果尝试写入将引发panic。
- 尝试读取被关闭的通道会获得与通道类型对应的零值。
- 注意:如果循环里读取一个已关闭的通道,并没检查通道是否关闭,那么该循环可能会一直运转下去,耗费大量CPU时间
- 执行以下代码可得知通道是否被关闭:
- v,ok:=-c
import (
"fmt"
"strings"
)
func sourceGopher(downstream chan string) {
for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
downstream <- v
}
close(downstream)
}
func filterGopher(upstream, downstream chan string) {
for {
item := <-upstream
if item == "" {
close(upstream)
return
}
if !strings.Contains(item, "bad") {
downstream <- item
}
}
}
func printGopher(upstream chan string) {
for {
v := <-upstream
if v == "" {
return
}
fmt.Println(v)
}
}
func main() {
c0 := make(chan string)
c1 := make(chan string)
go sourceGopher(c0)
go filterGopher(c0, c1)
printGopher(c1)
}
常用模式
- 从通道里面读取值,直到它关闭为止。
- 可以使用range关键字达到该目的
import (
"fmt"
"strings"
)
func sourceGopher(downstream chan string) {
for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
downstream <- v
}
close(downstream)
}
func filterGopher(upstream, downstream chan string) {
for item := range upstream {
if !strings.Contains(item, "bad") {
downstream <- item
}
}
close(downstream)
}
func printGopher(upstream chan string) {
for v := range upstream {
fmt.Println(v)
}
}
func main() {
c0 := make(chan string)
c1 := make(chan string)
go sourceGopher(c0)
go filterGopher(c0, c1)
printGopher(c1)
}
并发状态
- 共享值
- 竞争条件(race condition)
Go 的互斥锁(mutex)
- mutex = mutex exclusive
- Lock(), Unlock()
- sync包
- 互斥锁定义在被保护的变量之上
import (
"sync"
)
var mu sync.Mutex
func main() {
mu.Lock()
defer mu.Lock()
}
互斥锁的隐患
- 死锁
- 为保证互厅锁的安全使用,我们须遵守以下规则:
- 1.尽可能的简化互厅锁保护的代码
- 2、对每一份共享状态只使用一个互厅锁
长时间运行的工作进程
-
工作进程(worker)
-
通常会被写成包含select语句的for循环。
func worker() { for { select {} } } func main() { go worker() }
-
时间循环和goroutine
- 事件循环(event loop)
- 中心循环(central loop)'
- go通过提供goroutine作为核心概念,消除了对中心循环的需求。