开发环境
GOROOT: go的安装目录, go原生的工具在该目录下
GOPATH: 通常存放自己开发的代码或第三方依赖库(go env -w GOPATH='/Users/y/Desktop/Learn')
GO111MOUDLE=on: go会忽略GOPATH和vendor文件夹, 只根据go.mod下载依赖, 从1.16开始其默认值为on
- go.mod是Golang1.11版本新引入的官方包管理工具,在Go语言1.13版本之后不再需要设置环境变量。通过设置GO111MODULES可以开启或关闭go module工具
go go env -w GO111MODULE="on" go mod init demo
GOPROXY: 下载依赖库时走哪个镜像代理, 可以公司内部自建镜像
PATH: PATH下的二进制文件可以在任意目录运行
在$GOPATH目录建三个子目录: src, bin, pkg - 下载的第三方依赖, 存储在$GOPATH/pkg/mod下
- go install生成的可执行文件存储在$GOPATH/bin下
依赖包查找顺序- 工作目录
- $GOPATH/pkg/mod
- $GOROOT/src //go语言自带的系统库
GOPATH和源码 - 要分开放
- Golang运行时三大件:
- 内存分配器
- 垃圾回收器
- Goroutine调度
初始化Go项目
- go代码入口main()函数必须放在
package main
下面, 才有效 - 创建项目文件夹 mkdir name
- 进入文件夹 ---> go mod init mod名称
跨包引用
绝对路径是 mod名/文件夹(不用到文件)
package main
import (
"fmt"
"learnGo/myPath" //learnGo 是go.mod的名字
)
func main() {
fmt.Println("Hello Go")
a := 8
b := 9
c := mypath.Add(a, b) //调用的时候, 用包名调用, 包名可以和文件夹名称不一样
fmt.Println(c)
}
go常用命令
- go help build 列出命令的解释
- go build 对源码和依赖进行打包(把go文件,编译成计算机的可执行文件)
- go run
- go install 它和go build类似,不过它可以在编译后,把生成的可执行文件或者库安装到对应的目录下,以供使用。
- go get 主要是用来动态获取远程代码包的
- go mod init 初始化项目
- go mod tidy
- go test
- go tool 执行go自带工具
- go vet 检查代码中的静态错误
- go fmt
- go doc 查看文档
- go version
- go env 当前的go环境信息
源文件
- 源码文件使用UTF-8编码,对Unicode支持良好
- 每个源文件都属于包的一部分,在文件头部用package声明所属包名称。
- 入口函数main没有参数,且必须放在main包中。
- 用import导入标准库或第三方包。
数据类型
- var 定义变量
- const 定义常量
- func 定义函数
- x := 10
- 支持类型推断
- 编译器确保变量总是被初始化为零值,避免出现意外状况。
- "" 字符串 '' 字符
- iota是go语言的常量计数器,只能在常量的表达式中使用。
使用iota时只需要记住以下两点- iota在const关键字出现时将被重置为0。
- const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。
使用iota能简化定义,在定义枚举时很有用。
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)
const (
n1 = iota //0
n2 //1
_ //丢弃该值,常用在错误处理中
n4 //3
)
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0
const (
_ = iota
KB = 1 << (10 * iota) // <<移位操作,速度比乘除法快
MB = 1 << (10 * iota) // 1<<3 相当于1*2*2*2 0001 -> 1000
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)
const (
a, b = iota + 1, iota + 2 //1,2
c, d //2,3
e, f //3,4
)
- go语言中的 int 的大小是和操作系统位数相关的
- 在未指定类型定义float时,默认的类型是float64
- bool默认值为false
切片(切片是引用类型, 数组是值类型)
切片区别于数组,是引用类型, 不是值类型。数组是固定长度的,而切片长度是可变的,我的理解是:切片是对数组一个片段的引用。
- 分别使用len()、cap()获得切片的长度和容量
由数组得到切片
a1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
s4 := a1[2:4] //[3 4]
s5 := a1[:4] //[1 2 3 4]
s6 := a1[2:] //[3 4 5 6 7 8 9]
s7 := a1[:] //[1 2 3 4 5 6 7 8 9]
fmt.Println(s4)
fmt.Println(s5)
fmt.Println(s6)
fmt.Println(s7)
- 切片的本质是操作数组,只是数组是固定长度的,而切片的长度可变的
- 切片是引用类型,可以理解为引用数组的一个片段;而数组是值类型,把数组A赋值给数组B,会为数组B开辟新的内存空间,修改数组B的值并不会影响数组A。而切片作为引用类型,指向同一个内存地址,是会互相影响的。
- make()
s1 := make([]int,5,10)
//定义一个长度为5,容量为10的整型切片 - append(s1, value)
这是Go语言对切片的自动扩容机制。append()追加元素,原来的底层数据容量不够时,go底层会把底层数组替换,是go语言的一套扩容策略
切片的容量就是引用数组的容量, 如果切片添加的元素数量超出容量, 切片容量会成倍增加
- copy(): 复制切片, 这是深拷贝
- 删除切片中的元素 不能直接删除 可以组合使用分割+append的方式删除切片中的元素(真麻烦)
s3 := []int{1, 2, 3}
s3 = append(s3[:1], s3[2:]...) //第一个不用拆开 原因是一个作为被接受的一方 是把后面的元素追加到第一个
fmt.Println(s3)
- 数组转切片(就是把数组全切了)
a1 := [...]int{1,2,3}
s1 := a1[:]
fmt.Printf("a1类型:%T\ns1类型:%T",a1,s1)
- [...]的用法: [...]设置数组长度时,会根据初始值自动判断数组的长度
map
map和切片一样,也是引用类型,不是值类型。
var m1 map[string]int
m1 = make(map[string]int, 10) //要估算好map的容量,避免在程序运行期间在动态扩容(动态扩容会影响程序运行效率)
m1["lucky"] = 18
m1["jason"] = 24
fmt.Println(m1)
- map查询不存在的key不会报错的,返回了空值。
我们如何判断取到的值是否为空值呢?建议大家map取值的时候这么写
value,ok := m1["jason1"] if ok { fmt.Println(value) }else { fmt.Println("不存在") }
- delete() //删除不存在的key,也不会报错
package main
import (
"fmt"
)
func main() {
// 方式一
var a map[string]string
// 在使用 map 前,需要先 make, make 的作用就是给map分配数据空间。
a = make(map[string]string, 10)
a["no1"] = "宋江"
a["no2"] = "吴用"
a["no1"] = "武松"
a["no3"] = "吴用"
fmt.Println(a)
// 方式二
cities := make(map[string]string)
cities["no1"] = "北京"
cities["no2"] = "天津"
cities["no3"] = "上海"
fmt.Println(cities)
// 方式三
heroes := map[string]string{
"hero1": "宋江",
"hero2": "卢俊义",
"hero3": "吴用",
}
heroes["hero4"] = "林冲"
fmt.Println("heroes=", heroes)
}
rune
rune它是int32的别名(-2147483648~2147483647),相比于byte(-128~127),可表示的字符更多。由于rune可表示的范围更大,所以能处理一切字符,当然也包括中文字符。在平时计算中文字符,可用rune。
- 字符串修改是不能直接修改的,需要转成rune切片后再修改
s2 := "小白兔"
s3 := []rune(s2) //把字符串强制转成rune切片
s3[0] = '大' //注意 这里需要使用单引号的字符,而不是双引号的字符串
fmt.Println(string(s3)) //把rune类型的s3强转成字符串
字符和字符串的区别
c1 := "红"
c2 := '红'
fmt.Printf("c1的类型:%T c2的类型:%T \n", c1, c2)
c3 := "H"
c4 := 'H'
fmt.Printf("c3的类型:%T c4的类型:%T \n", c3, c4)
//print
//c1的类型:string c2的类型:int32
//c3的类型:string c4的类型:int32
总结:我们发现只要是双引号包裹的类型就是string,只要是单引号包裹的类型就是int32,也就是rune。和中英文无关。
知识点:rune的别名是int32
字符串的修改要转成rune切片,而不能像PHP一样直接修改。
指针pointer
- go语言不存在指针操作
- 只有2个符号
- &取内存地址
- *根据内存地址取值
n := 18
// 取地址
fmt.Println(&n)
fmt.Println(*&n)
流控制语句
Go仅有三种流控制语句,与大多数语言相比,都可称得上简单。
- if
- switch
finger := 2
switch finger {
case 1:
fmt.Println("大拇指")
case 2:
fmt.Println("食指")
case 5:
fmt.Println("小拇指")
default:
fmt.Println("无效")
}
- 在一个 switch 块内,每个 case 无需声明 break 来终止 , 如果想顺序执行使用 fallthrough
- 在一个switch块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。
- for(包含fori和for range)
s3 := make([]int, 3, 3)
s3 = []int{1, 2, 3}
for i := 0; i < len(s3); i++ {
fmt.Println(s3[i])
}
for i, v := range s3 {
fmt.Println(i, v)
}
- break: 跳出for循环,不再继续执行
- continue: 跳出本次for循环,继续执行后面的for循环
- goto
举个栗子:我们设置一个双重for循环,当遇到C时就跳出循环,不再继续执行。
//普通写法
flag := false
for i := 0; i < 10; i++ {
for j := 'A'; j < 'Z'; j++ {
if j == 'C' {
flag = true
break
}
fmt.Printf("%v-%c\n", i, j)
}
if flag {
fmt.Println("over")
break
}
}
使用goto
for i := 0; i < 10; i++ {
for j := 'A'; j < 'Z'; j++ {
if j == 'C' {
goto xx //跳转到定义的label语句,即直接跳出了for循环
}
fmt.Printf("%v-%c\n", i, j)
}
}
// 定义label
xx:
fmt.Println("over")
总结:用法其实很简单,当我们项目复杂时,合理的使用goto能简化代码;但是千万不要滥用goto,否则会导致代码晦涩难懂。
函数
- 函数可定义多个返回值,甚至对其命名。
- 函数是第一类型,可作为参数或返回值。
- 用defer定义延迟调用,无论函数是否出错,它都确保结束前被调用。(用于释放资源, 解除锁定, 或执行一些清理工作)
- 匿名函数
匿名函数就是没有名字的函数。匿名函数多用于实现回调函数和闭包。
- 在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。
- 匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
func main() { // 将匿名函数保存到变量 add := func(x, y int) { fmt.Println(x + y) } add(10, 20) // 通过变量调用匿名函数 //自执行函数:匿名函数定义完加()直接执行 func(x, y int) { fmt.Println(x + y) }(10, 20) }
总结:自执行函数就是在匿名函数后面追加(),表示不需要外部调用,直接执行。
闭包(闭包是一个函数,这个函数包含了他外部作用域的一个变量)
闭包=函数+引用环境
package main
import "fmt"
func adder(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
f1 := adder(1) //把引用环境的x带着了
ret := f1(2)
fmt.Println(ret)
}
方法
- 可以为当前包内的任意类型定义方法。
package main
type X int
func(x*X)inc() { // 名称前的参数称作receiver,作用类似python self
*x++
}
func main() {
var x X
x.inc()
println(x)
}
- 还可直接调用匿名字段的方法,这种方式可实现与继承类似的功能。
package main
import(
"fmt"
)
type user struct{
name string
age byte
}
func(u user)ToString()string{
return fmt.Sprintf("%+v",u)
}
type manager struct{
user
title string
}
func main() {
var m manager
m.name= "Tom"
m.age=29
println(m.ToString()) // 调用user.ToString()
}
go语言中函数与方法重要的区别:
1.调用方式不一样
函数的调用方式:函数名(实参列表)
方法的调用方式:变量.方法名(实参列表)
2.对于普通函数,接受者为值类型时候,不能传递指针类型
func (p Person) Test(){
p.Name="json"
}
func (p *Person) Test01(){
p.Name="tom"
}
重要点:
1.不管调用形式如何,真正决定是值拷贝还是地址拷贝的,
看这个方法是和哪个类型绑定
2.方法如果是和值类型绑定,如(p Person),则是值拷贝,
如果是和指针类型绑定,如(p *Person)则是地址拷贝。
数据
- 切片(slice)可实现类似动态数组的功能。
- 将字典(map)类型内置,可直接从运行时层面获得性能优化。
- 将字典(map)类型内置,可直接从运行时层面获得性能优化。
package main
import(
"fmt"
)
type user struct{ // 结构体类型
name string
age byte
}
type manager struct{
user // 匿名嵌入其他类型
title string
}
func main() {
var m manager
m.name= "Tom" // 直接访问匿名字段的成员
m.age=29
m.title= "CTO"
fmt.Println(m)
}
结构体
- 定义结构体
- 实例化结构体
- 初始化结构体
- 结构体内嵌
Go语言的结构体内嵌是一种组合特性,使用结构体内嵌可构建一种面向对象编程思想中的继承关系。
type Book struct {
title string
author string
num int
id int
}
type BookBorrow struct {
Book // 继承了Book的基本属性和方法
borrowTime string
}
type BookNotBorrow struct {
Book // 继承了Book的基本属性和方法
readTime string
}
// 初始化结构体内嵌
func main() {
bookBorrow := &BookBorrow {
Book:Book{
"Go语言",
"Tom",
20,
152368,
},
borrowTime:"30",
}
}
- 结构体方法
方法和函数比较像,区别是函数属于包,通过包调用函数,而方法属于结构体,通过结构体变量调用。
func (变量名 结构体类型) 方法名(参数列表) (返回值列表) {
// 方法体
}
对于结构体方法,接收者可以是结构体类型的值或指针。
- 指针类型接收者:当接收者类型为指针时,可以通过该方法改变该接收者的成员变量值,即使你使用了非指针类型的实例调用该函数,也可以改变实例对应的成员变量值。(浅拷贝)
- 值类型接收者:该方法操作对应接收者值得副本,即使你使用了指针类型的实例调用该函数,也无法改变成员变量的值。(深拷贝)
并发
goroutines & channel极大方便了并发编程和线程通讯。
- 通过全局变量加锁同步来实现通讯,并不利于多个协程对全局变量的读写操作。
- 加锁虽然可以解决goroutine对全局变量的抢占资源问题,但是影响性能,违背了原则。
- 总结:为了解决上述的问题,我们可以引入channel,使用channel进行协程goroutine间的通信。
答案:
- Go语言的并发基于CSP(Communication Sequential Process,通信顺序进程)模型,CSP模型是用于描述两个独立的并发实体通过共享的通信管道(channel)进行通信的并发模型。
- CSP中channel是一类对象,它不关注发送消息的实体,而关注发送消息时使用的通信管道。 简单来说,CSP模型提倡通过通信来共享内存,而非通过共享内存来通信。
基于CSP模型,Go语言通过通信的方式,通过安全的通道发送和接收数据以实现同步,避免了显式锁的问题,大大简化了并发编程的编写。
goroutine
goroutine是Go并发设计的核心,也叫协程,它比线程更加轻量,因此可以同时运行成千上万个并发任务。
不仅如此,Go语言内部已经实现了goroutine之间的内存共享,它比线程更加易用、高效和轻便。
goroutine定义
在Go语言中,每一个并发的执行单元叫作一个goroutine。我们只需要在调用的函数前面添加go关键字,就能使这个函数以协程的方式运行。
go 函数名(函数参数)
- 一旦我们使用了go关键字,函数的返回值就会被忽略,故不能使用函数返回值来与主线程进行数据交换,而只能使用channel。
- 当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们称之为main goroutine。所有的goroutine在main函数结束时会一并结束
runtime包
Go语言中runtime(运行时)包实现了一个小型的任务调度器。这个调度器的工作原理和系统对线程的调度类似,Go语言调度器可以高效地将CPU资源分配给每一个任务。以下主要介绍三个函数:Gosched()、Goexit()、GOMAXPROCS()。
- Gosched(): 用于让出CPU时间片,让出当前goroutime的执行权限,调度器安排其他等待任务运行,并在下次某个时候从该位置恢复执行。
Go语言的协程是抢占式调度的,当遇到长时间执行或者进行系统调用时,会主动把当前goroutine的CPU §转让出去,让其他goroutine能被调度并执行。
一般出现如下几种情况,goroutine就会发生调度:
- syscall
- C函数调用(本质上和syscall一样)
- 主动调用runtime.Gosched
- 某个goroutine的调用时间超过100ms,并且这个goroutine调用了非内联的函数。
- Goexit()
Goexit()终止调用它的Go协程,但其他Go协程不会受影响。Goexit()会在终止该Go协程前执行所有defer的函数。
- GOMAXPROCS()
GOMAXPROCS(n int)函数可以设置程序在运行中所使用的CPU数,Go语言程序默认会使用最大CPU数进行计算。
channel(实现原理上其实就是一个阻塞的消息队列。)
Go语言中channel的关键字是chan,声明、初始化channel的语法如下:
- 操作符:<- (左读右写)
var 通道变量 chan 通道类型
var 通道变量 chan <- 通道类型 // 单向channel,只写
var 通道变量 <- chan 通道类型 // 单向channel,只读
通道变量 := make(chan Type) // 无缓存channel
通道变量 := make(chan Type, 0) // 无缓存channel,与上面等价
通道变量 := make(chan Type, capacity int) // 有缓存channel, capacity > 0
- 通道变量是保存通道的引用变量;通道类型是指该通道可传输的数据类型。
- 当capacity为0时,channel是无缓冲阻塞读写的
无缓冲 = 只能有一个元素, 阻塞读写 = 只有把写入的元素接收才能再次写入
- 当capacity大于0时,channel是有缓冲、非阻塞的,直到写满capacity个元素才阻塞写入。
有缓冲 = 可以写入capacity个元素, 非阻塞读写 = 只有超过capacity才会阻塞写入
- 一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送到通道的元素值一定会先被接收。
- 元素值的发送和接收都需要用到操作符<-。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 2
ch <- 1
ch <- 3
elem := <-ch
fmt.Printf("The first element received from channel ch: %v\n", elem)
}
//打印结果 --- 为什么只接收一次呢? 不是应该等于3吗
//The first element received from channel ch: 2
channel缓冲机制
channel按是否支持缓冲区可分为无缓冲的通道(unbuffered channel)和有缓冲的通道(buffered channel)。
很好的解释
可以类比寄快递的过程。我们要从家里寄出一本书,如果没有缓存,必须等快递员取走后我们才能出门。如果有快递柜(缓存),我们可以把书放快递柜,就可以出门了。但是前提是,快递柜必须有空闲的柜子,我们才能把书放进去,然后才能出门。如果快递柜一直是满的,我们就必须等,等快递员从快递柜取走已有的快件。
无缓冲的通道
无缓冲的通道是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。如果两个goroutine没有同时准备好,会导致先执行发送或接收操作的goroutine阻塞等待。
这种对通道进行发送和接收的交互行为本身就是同步的,其中任意一个操作都无法离开另一个操作单独存在。
有缓冲的通道
有缓冲通道是一种在被接收前能存储一个或多个值的通道。
这种类型的通道并不强制要求goroutine之间必须同时完成接收和发送。通道阻塞发送和接收的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
这导致有缓冲的通道和无缓冲的通道之间有一个很大的不同:无缓冲的通道保证进行发送和接收的goroutine会在同一时间进行数据交换,有缓冲的通道没有这种保证。
close和range
当发送者知道没有更多的值需要发送到channel时,让接收者也能及时知道没有更多的值需要接收是很有必要的,因为这样就可以让接收者停止不必要的等待
- channel不像文件一样需要经常去关闭,只有当你确实没有任何需要发送的数据时,或者想要显式地结束range循环之类的,才会去关闭channel。
- 关闭channel后,无法向channel再次发送数据,再次发送将会引发panic错误。
- 关闭channel后,可以继续从channel接收数据。
- 对于nil channel,无论接收还是发送都会被阻塞。
————————————————
这可以通过内置的close函数和range关键字来实现。
- close
- range: channel的遍历: for … range
for ... range 语法可以用于处理 Channel。接收 Channel 中发送的值,作为迭代值执行循环体,直到 channel 被关闭
func main() {
go func() {
time.Sleep(1 * time.Hour)
}()
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c) // 如果不关闭会一直阻塞!!!
}()
for i := range c {
fmt.Println(i)
}
fmt.Println("Finished")
}
- select: 选择一组符合条件的send与receive
select和switch一样,不是循环,只会选择一个case来处理
import "time"
import "fmt"
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}
//time.After(tme)超时处理机制
- time.After(tme)
通过添加一个time.After(time)方法的case,利用它返回一个类型为<-chan Time的单向channel,在指定的时间发送一个当前时间给返回的channel中。
Timer 和 Ticker
Timer 和 Ticker 是关于时间的两个 Channel
- timer:定时器,代表未来的一个单一事件,可以设置一个等待时间,它提供一个Channel,到时间后该Channel提供一个时间值。
单次
- ticker是一个定时触发的计时器,它会以一个间隔(interval)向Channel中发送一个事件(当前时间),而Channel的接受者可以以固定的时间间隔从Channel中读取事件。
多次
信道同步
channel可以用在goroutine之间的同步。
示例:
main goroutine通过done channel等待worker完成任务。 worker做完任务后只需往channel发送一个数据就可以通知main goroutine任务完成。
import (
"fmt"
"time"
)
func worker(done chan bool) {
time.Sleep(time.Second) //做耗时操作, 例如网络请求
// 通知任务已完成
done <- true //完成后发送信号
}
func main() {
done := make(chan bool, 1)
go worker(done)
// 等待任务完成
<-done //任务完成会接到信号
}
不要通过共享内存来通信,要通过通信来共享内存
- 一般做法
某个A线程释放了锁,B线程能获取到锁并开始运行,这个不涉及数据的交换。数据的交换主要还是通过共享内存(共享变量或者队列)来实现,为了保证数据的安全和正确性,共享内存就必需要加锁等线程同步机制。而线程同步使用起来特别麻烦,容易造成死锁,且过多的锁会造成线程的阻塞以及这个过程中上下文切换带来的额外开销。
- go做法
GO语言中,要传递某个数据给另一个goroutine(协程),可以把这个数据封装成一个对象,然后把这个对象的指针传入某个channel中,另外一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。GO从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,为开发者提供了一种优雅简单的工具,所以GO的做法就是使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信。
类比: 100个人同时下单寄件, 就需要cpu协调快递员去取件的时间, go语言通过语言成面保证, 快递员一次只能收到一条取件信息, 避免了cpu的协调时间
反射(reflect)
- 静态类型:每个变量都有一个静态类型,这个类型是在编译时(compile time)就已知且固定的。
- 动态类型:接口类型的变量还有一个动态类型,是在运行时(run time)分配给变量的值的一个非接口类型。(除非分配给变量的值是nil,因为nil没有类型)。
id类型
- 空接口类型interface{} (别名any)表示空的方法集,它可以是任何值的类型,因为任何值都满足有0或多个方法(有0个方法一定是任何值的子集)。
swift any
- 一个接口类型的变量存储一对内容:分配给变量的具体的值,以及该值的类型描述符。可以示意性地表示为(value, type)对,这里的type是具体的类型,而不是接口类型。
在基本层面,反射只是检测存储在接口变量中的(value, type)对的一种机制。
可以使用reflect包的 reflect.ValueOf和reflect.TypeOf方法,获取接口变量值中的(value, type)对,类型分别为reflect.Value和reflect.Type。
var a interface{} = 1
var b interface{} = 1.11
var c string = "aaa"
// 将接口类型的变量运行时存储的具体的值和类型显示地获取到
fmt.Println("type:", reflect.TypeOf(a)) // type: int
fmt.Println("value:", reflect.ValueOf(a)) // value: 1
fmt.Println("type:", reflect.TypeOf(nil)) // type: <nil>
fmt.Println("value:", reflect.ValueOf(nil)) // value: <invalid reflect.Value>
fmt.Println("type:", reflect.TypeOf(b)) // type: float64
fmt.Println("value:", reflect.ValueOf(b)) // value: 1.11
fmt.Println("type:", reflect.TypeOf(c)) // type: string
fmt.Println("value:", reflect.ValueOf(c)) // value: aaa
接口
如果说goroutine和channel是Go并发的两大基石,那么接口是Go语言编程中数据类型的关键。在Go语言的实际编程中,几乎所有的数据结构都围绕接口展开,接口是Go语言中所有数据结构的核心。
interface 底层结构
根据 interface 是否包含有 method,底层实现上用两种 struct 来表示:iface 和 eface。eface表示不含 method 的 interface 结构,或者叫 empty interface。对于 Golang 中的大部分数据类型都可以抽象出来 _type 结构,同时针对不同的类型还会有一些其他信息。
type eface struct {
_type *_type
data unsafe.Pointer
}
type _type struct {
size uintptr // type size
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32 // hash of type; avoids computation in hash tables
tflag tflag // extra type information flags
align uint8 // alignment of variable with this type
fieldalign uint8 // alignment of struct field with this type
kind uint8 // enumeration for C
alg *typeAlg // algorithm table
gcdata *byte // garbage collection data
str nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}
iface 表示 non-empty interface 的底层实现。相比于 empty interface,non-empty 要包含一些 method。method 的具体实现存放在 itab.fun 变量里。
type iface struct {
tab *itab
data unsafe.Pointer
}
// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
type itab struct {
inter *interfacetype
_type *_type
link *itab
bad int32
inhash int32 // has this itab been added to hash?
fun [1]uintptr // variable sized
}
代码中创建了一个叫Animal的接口,结构体Cat实现了Animal接口的两个方法,因此我们就可以认为Cat是Animal接口的实例。
type Animal interface {
Name() string
Speak() string
}
type Cat struct {
}
func (cat Cat) Name() string {
return "Cat"
}
func (cat Cat) Speak() string {
return "喵喵喵"
}
多态
同一件事情由于条件不同产生的结果不同即为多态,多态在代码层面最常见的一种方式是接口作为方法参数。
type Live interface {
run()
}
type People struct{}
type Animate struct{}
func (p *People) run() {
fmt.Println("人在跑")
}
func (a *Animate) run() {
fmt.Println("动物在跑")
}
func sport(live Live) {
live.run()
}
func main() {
peo := &People{}
sport(peo) //输出:人在跑
ani := &Animate{}
sport(ani) //输出:动物在跑
}
- 结构体实现了接口的全部方法,就认为结构体属于接口类型,这时可以把结构体变量赋值给接口变量。
空接口(any类型)
在Go语言中,空接口(interface{})不包含任何方法,也正因如此,所有的类型都实现了空接口,因此空接口可以存储任意类型的数值。
func Log(name string, i interface{}) {
fmt.Printf("Name = %s, Type = %T, value = %v\n",
name, i , i)
}
func main() {
var v1 interface{} = 1
var v2 interface{} = "abc"
var v3 interface{} = true
var v4 interface{} = &v1
var v5 interface{} = struct {
Name string
}{"Xiao Ming"}
var v6 interface{} = &v5
Log("v1", v1)
Log("v2", v2)
Log("v3", v3)
Log("v4", v4)
Log("v5", v5)
Log("v6", v6)
}
类型断言
类型断言是使用在接口变量上的操作,简单来说,接口类型向普通类型的转换就是类型断言。格式如下:
t, ok := X.(T)
这句代码的含义是判断X的类型是否是T。
如果断言成功,则ok为true,t为接口变量X的动态值;如果断言失败,则ok为false,t为类型T的初始值。即t的类型始终为T。
接口类型断言有两种方式,一种是ok-pattern,另一种是switch-type。
- ok-pattern
if value, ok := 接口变量.(类型); ok == true {
// 接口变量是该类型时的处理
}
- switch-type
type Person struct {
Name string
Age int
}
func main() {
s := make([]interface{}, 3)
s[0] = 1
s[1] = "abc"
s[2] = Person{"张三", 20}
for index, data := range s {
switch value := data.(type) {
case int:
fmt.Printf("s[%d] Type = int, value = %d\n", index, value)
case string:
fmt.Printf("s[%d] Type = string, value = %s\n", index, value)
case Person:
fmt.Printf("s[%d] Type = Person, Person.name = %v, Person.Age = %d\n", index, value.Name,value.Age)
}
}
}
非侵入式接口
- 侵入式接口:需要显示地创建一个类去实现接口。
- 非侵入式接口,不需要显示地创建一个类去实现一个接口。
大部分语言的接口都是侵入式接口,Go语言的接口是非侵入式的。如果接口A的方法集是接口B方法集的子集,那么接口B的实例可以直接给接口A赋值。
Go语言错误处理
- defer: 允许我们将某个语句或函数延迟到函数返回之前才发生,常用于释放某些已分配的资源。
错误处理
我们在编写程序时,为了加强程序的健壮性,往往会考虑到对程序中可能出现的错误和异常进行处理。
- Go语言中使用builtin包下error接口作为错误类型,error接口只包含了一个方法,返回值是string,表示错误信息。官方源码定义如下:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
- Go语言设计者认为类似try-catch-finally的传统异常处理机制很容易造成开发者对异常机制的滥用,从而使代码结构变得很混乱。因此,Go语言中会使用多值返回来返回错误。
- 在Go语言标准库的errors包中提供了error接口的实现结构体errorString,还额外提供了快速创建错误的函数。
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
- 如果错误信息由很多变量组成,可以借助fmt.Errorf(format string, a ...interface{})完成错误信息格式化,因为底层还是errors.New()
错误处理的方式
file, err := os.Open("abc.txt")
if err != nil {
if pathError, ok := err.(*os.PathError); ok {
fmt.Println(pathError.Err)
} else {
fmt.Println("unknown error", err)
}
} else {
fmt.Println(file)
}
Go语言宕机 panic
panic方法是Go语言的一个内置函数。panic有点类似其他编程语言的throw,抛出异常。当执行到panic后,停止当前函数的执行,一直向上返回,执行每一层的defer,如果没有遇见recover,程序退出,并打印错误栈信息。
panic的源码如下:
func panic(v interface{})
我们可以传入任意类型的值作为宕机内容。
一般而言,只有当程序发生不可逆的错误时,才会使用panic方法来触发宕机。如果遇到以下情形,可以调用panic方法来退出程序:
- 程序处于失控状态且无法恢复,继续执行将影响其他正常程序,引发操作系统异常甚至是死机。
- 发生不可预知的错误。
宕机恢复
recover捕获宕机
Go语言通过内置函数recover来捕获宕机,类似于其他编程语言中的try-catch机制。recover源码如下:
func recover() interface{}
由于defer语句延迟执行的特性,我们可以通过“defer语句+匿名函数+recover方法”来完成对宕机的捕获。如果没有panic信息返回nil,如果有panic,recover会把panic状态取消,恢复程序的正常运行。
func protect() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
panic("Serious bug")
}
func main() {
protect()
fmt.Println("Invalid code")
}