《The Way to Go》阅读笔记
Chapter1
go语言的发展目标
结合静态语言的安全性和高效性,动态语言的易开发性。
注意,go是静态语言(静态类型语言)。是类型安全和内存安全的编程语言。通过goroutine来实现并发并行编程,通过channel来实现gorountine之间通信。
像其他静态语言一样执行本地代码,但依旧运行某种意义上的虚拟机进行垃圾回收,不需要开发人员考虑内存管理的问题。
语言的特性
没有类和继承概念,使用接口(interface)来实现多态性,函数是基本构件
使用静态类型,强类型语言,隐式的类型转换不被允许
Chapter2
平台与架构
Go语言有两个版本编译器:Go原生编译器gc和非原生编译器gccgo,Go语言的编译器和链接器都是C语言编写并产生本地代码
Go的自举:在 1.4 版本后,Go 编译器实现了自举,即通过 1.4 版本来编译安装之后版本的编译器。
Go环境变量
- $GOROOT:Go在电脑上的安装位置,一般是$Home/go(注意不要配置成bin目录了,bin目录是添加到path让你方便执行go.exe的…)
- $GOPATH:此路径下必须包含src / pkg / bin三个目录存放源码,包文件和可执行文件
- $GOMAXPROCS:用于设置应用程序可使用的处理器个数与核数
Go 运行时(runtime)
- 尽管Go编译器产生的是本地可执行代码,但仍然运行在Go的runtime中,这个runtime負責管理包括内存分配、垃圾回收、栈处理、goroutine、channel、切片、map和反射。Go的runtime嵌入到每一个可执行文件当中,所以可执行文件会比相应的源文件大得多。
Chapter4
Go程序基本结构和要素
每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。每个Go文件都属于且仅属于一个包,一个包可以由许多.go源文件组成,文件名和包名一般都是不同的。
如果要编译包名不是为main的源文件如pack1,编译后产生的对象文件将会是pack1.a而不是可执行程序。
所有包名应该使用小写字母。
Go标准库位于Go根目录下的子目录pkg,以64位windows为例,标准库位于windows_amd64下。
包的导入和搜索
如果包名不是以 .
或 /
开头,如 "fmt"
或者 "container/list"
,则
Go 会在全局文件进行查找;如果包名以 ./
开头,则 Go
会在相对目录中查找;如果包名以 /
开头(在 Windows
下也可以这样使用),则会在系统的绝对路径中查找。
可见性规则
包通过这个强制执行的规则决定自身代码是否暴露给外部文件:如果标识符(包括常量、变量、类型、函数名、结构字段等等)以大写字母开头,那么可以被外部包使用,如果以小写字母开头,就对包外不可见,仅在包内可见并且可用。
只有当某个函数需要被外部包调用的时候才使用大写字母开头,并遵循 Pascal 命名法;否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写。
函数
每一个可执行程序必须包含main(),是除init()外启动后第一个执行的函数。如果main包的没有包含main函数会引发build error。main没有参数也没有返回类型。
对相同类型的函数参数,可以这样描述函数
func Add(param1, param2 int) int {
// ...
}
类型
使用 var 声明的变量的值会自动初始化为该类型的零值。
类型定义了某个变量的值的集合与可对其进行操作的集合。
类型可以是基本类型,如:int、float、bool、string;结构化的(复合的),如:struct、array、slice、map、channel;只描述类型的行为的,如:interface。
结构化的类型没有真正的值,它使用 nil 作为默认值(在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是NULL或 0)。值得注意的是,Go 语言中不存在类型继承。
常量
常量值必须在编译时能够确定,同时无法在运行过程修改它的值
变量
使用关键词var
来声明一个变量
在包级别声明变量
var a int = 5 // 显式声明类型
var b = 5 // 隐式声明类型
在函数内部的简短形式,省略var
func Sample() {
a := 5
}
交换变量,不用写tmp了
a, b = b, a
值类型与引用类型
基本数值类型,字符串,数组,结构都是值类型,内容直接存放在变量分配得的内存上
更复杂的类型(?)使用引用类型保存,变量分配得的内存上,存放“内容所在的地址”
格式字符串
用来记录一些输出类型(格式化说明符)
- %v 通用
- %t 布尔型
- %d 格式化整数(%x和%X用于格式化16进制
- %g 格式化浮点型(%f输出浮点数,%e输出科学计数法
- %p 显示地址格式
init函数
不能被人为调用,在包完成初始化(变量全局声明初始化)后自动执行,可以作为另一种初始化途径
初始化以单线程执行,且按照包的依赖关系执行,另一种用途可以在开始执行程序前对数据进行检验修复
基本类型与运算符
注意go没有隐式类型转换,在类型转换是要注意用显式的方式进行
数字类型
Go 也有基于架构的类型,例如:int、uint 和 uintptr。这些类型的长度都是根据运行程序所在的操作系统类型所决定的:
int
和uint
在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64 位(8 个字节)。uintptr
的长度被设定为足够存放一个指针即可。
Go 语言中没有 float 类型。(Go语言中只有 float32 和 float64)没有double类型。
float32 精确到小数点后 7 位,float64 精确到小数点后 15
位。由于精确度的缘故,你在使用 ==
或者 !=
来比较浮点数时应当非常小心。
算术运算符
- 相对于一般规则而言,Go
在进行字符串拼接时允许使用对运算符
+
的重载,但 Go 本身不允许开发者进行自定义的运算符重载 /
对于整数运算而言,结果依旧为整数,例如:9 / 4 -> 2
。取余运算符只能作用于整数:9 % 4 -> 1
。- 你可以将语句
b = b + a
简写为b += a
- 带有
++
和--
的只能作为语句,而非表达式,因此n = i++
这种写法是无效的,
类型别名
给某个类型起另一个名字,新类型不会拥有原类型附带方法,新类型也可自定义方法
字符类型
byte
类型是 uint8
的别名,对于只占用 1
个字节的传统 ASCII 编码的字符来说
\x
总是紧跟着长度为 2 的 16 进制数
var ch byte = 65 或 var ch byte = '\x41' // one byte
Go 同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者
runes,并在内存中使用 int 来表示。在书写 Unicode 字符时,需要在 16
进制数之前加上前缀 \u
或者 \U
。
因为 Unicode 至少占用 2
个字节,所以我们使用 int16
或者 int
类型来表示。如果需要使用到
4
字节,则会加上 \U
前缀;前缀 \u
则总是紧跟着长度为
4 的 16 进制数,前缀 \U
紧跟着长度为 8 的 16 进制数。
var ch int = '\u0041' // two bytes
var ch2 int = '\u03B2'
var ch3 int = '\U00101234' // four bytes
包unicode
包含的测试字符函数
- 判断是否为字母:
unicode.IsLetter(ch)
- 判断是否为数字:
unicode.IsDigit(ch)
- 判断是否为空白符号:
unicode.IsSpace(ch)
字符串
字符串是UTF-8字符的序列,所以字符串内的字符可能根据需要占用1至4字节,与其他语言不同,好处是减少内存、硬盘空间占用,也不需要进行文本编码解码。
双引号内的转义符照常处理;反引号内的会解释为字符串,会将转义符原样输出
`This is a raw string \n` 中的 `\n\` 会被原样输出。
- 可以进行索引,但索引到的是字节
- 根据长度限定,而不是特殊字符’\0’
- 是一种值类型,内容不可变,是字节的定长数组
字符串与[]byte的转换
// 逆转!
func ReverseString(input string) string {
c := []byte(input)
for i := 0; i < len(c)/2; i++ {
other := len(c) - i - 1
c[i], c[other] = c[other], c[i]
}
return string(c)
}
Chapter5
if-else 结构
特点:条件不需要圆括号,但是语句需要大括号
可以使用初始化语句,在if语句的作用域中声明一个变量,需要在声明语句后面跟上一个分号
if val := 10; val > max {
// do something
}
测试多返回值函数的错误
Go语言函数经常使用两个返回值来表示执行是否成功,通常返回零值、nil、false来表示失败,或者使用一个error类型的变量来代替作为第二个返回值;成功执行的话,error值为nil。
如果判断函数执行一定能成功,可以使用_
来接收第二个参数。
有时当错误没有发生时,继续运行就是唯一要做的事情,所以if语句块后面不需要使用else分支(卫语句)
value, err := pack1.Function1(param1)
if err != nil {
fmt.Printf("An error occured in pack1.Function1 with parameter %v", param1)
return err
}
// 未发生错误,继续执行:
switch 语句
与其他语言switch语句的不同之处:
可以同时测试多个符合条件的值:case val1, val2, val3
匹配某个分支后,会退出整个switch语句,所以不需要特别使用break来结束
如果想要执行后续分支的代码,可以使用
fallthrough
关键字switch语句也可以不提供任何被判断的值,然后在每个case分支中进行不同条件的测试,可用于替代测试条件非常多的
if-else
语句switch { case i < 0: f1() case i == 0: f2() case i > 0: f3() }
同样的类似if-else结构,也可以在switch后跟上一个初始化语句
switch result := calculate(); { case result < 0: ... case result > 0: ... default: // 0 }
for 结构
- 常见的for结构与其他语言一致 for A; B; C { }
- 没有while结构,存在根据条件判断进行的 for condition {}
- 无限循环结构形如,for {}
- 形如foreach的结构,使用range关键字,for pos, value range array {} 注意value为集合中对应索引的值拷贝,因此它一般只具有只读性质(如果value为指针,则会产生指针的拷贝,依旧可以修改集合中的原值)写操作并不生效
- (5.5-5.6) break, continue, goto 没有什么不同
Chapter6
函数-介绍
- go中不支持函数重载(函数重载需要多余的类型匹配,影响性能),同名函数将会导致编译报错
- 函数是一等公民(first-class value),可以赋值给变量
- go没有泛型的概念,在注重性能的场合最好为每一个类型单独创建一个函数,使代码可读性更强
函数参数与返回值
go默认使用按值传递来传递参数,也就是传递参数的副本
如果希望直接修改参数的值,可以传递参数的地址(&variable),相当于传递一个指针,可以通过这个指针来修改指向地址上的值。
注意指针也是变量,指针变量的值是另一个变量的地址,所以传递指针也是按值传递。当函数内改变一个占用内存较大的变量时,传递指针不需要赋值变量的值,性能优势就更加明显了。
使用多重返回值:可以使用命名返回值,或者非命名返回值;尽量使用命名返回值,使代码更容易读懂
使用命名返回值时,可以为返回值变量赋值后return,也可以return对应的返回值变量,但以返回的明确值为准
func GetX2AndX3_2(input int) (x2 int, x3 int) { // 假设输入为10 x2 = input * 2 x3 = input * 3 //return // 返回 20,30 //return x2, x3 // 同样返回 20,30 return input, input // 返回10,10,x2和x3也被赋值为10 }
传递变长参数
如果函数最后一个参数采用...type的形式,这个函数就可以处理一个变长参数,接收一个类似于切片(slice)的参数,如下,变量
who
的值为[]string{"Joe", "Anna", "Eileen"}
func Greeting(prefix string, who ...string) Greeting("hello:", "Joe", "Anna", "Eileen")
如果参数存储在slice类型变量
slice
中,可以通过slice...
的形式来传递参数,调用变参函数slice := []int{7,9,3,5,1} x = min(slice...)
defer 和追踪
- 关键字
defer
允许将某个语句或函数的执行推迟到函数返回之前(或者说任意位置的return
语句之后),类似C#与Java中的finally语句块 - 注意如果
defer
的函数a中的某个参数使用了函数b的调用结果,函数b会马上执行而不会等到函数返回前才执行,见示例 6.11
将函数作为参数
函数可以作为其他函数的参数进行传递,函数参数的格式如下:
// strings.IndexFunc
func IndexFunc(s string, f func(c rune) bool) int
// 函数返回值是字符串s中第一个使f(c)返回true的unicode字符
闭包
使用匿名函数,例
func(x, y int) int { return x+ y }
除了没有名字和声明一个函数是一样的这样的一个匿名函数不能独立存在,需要将它赋值于某个变量,再通过这个变量名进行调用;或者直接对这个匿名函数进行调用
对下述例子中的函数变量,变量代表的是
func(int)
,变量的值是一个内存地址for i := 0; i < 4; i++ { g := func(i int) { fmt.Printf("%d ", i) } g(i) fmt.Printf(" - g is of type %T and has value %v\n", g, g) } // -- output 0 - g is of type func(int) and has value 0x681a80 1 - g is of type func(int) and has value 0x681b00 2 - g is of type func(int) and has value 0x681ac0 3 - g is of type func(int) and has value 0x681400
闭包:匿名函数被允许去捕捉一些外部状态,例如函数被创建时的状态;又或者说闭包继承了函数声明时的作用域。作用域内的变量被共享到闭包的环境中,可以在闭包中被操作,直到闭包销毁。
使用闭包调试
使用runtime包中的runtime.Caller来打印当前的执行文件与执行行数
Chapter7
数组与切片 声明和初始化
数组:具有相同唯一类型的长度固定的数据项序列,数组长度必须是常量表达式,编译时需要知道数组长度以便分配内存。
数组的声明方式(值类型,声明和初始化可以同时发生):
var identifier [len]type
// 举例一个数组的初始化
func main() {
var arr1 [5]int // 初始化为全0
for i:=0; i < len(arr1); i++ {
arr1[i] = i * 2
}
for i:=0; i < len(arr1); i++ {
fmt.Printf("Array at index %d is %d\n", i, arr1[i])
}
}
注意,Go语言中的数组是值类型,不像C/C++中是指向首元素的指针,所以把一个数组赋值给另一个时,或者将数组作为参数传入时,都会产生数组拷贝。
将一个大数组传递给函数会消耗很多内存,通过传递数组的指针,或者使用数组的切片来避免这种情况。
数组常量
如果数组内容已经提前知道,那么可以用数组常量来初始化而不用依次赋值
var arrAge = [5]int{18, 20, 15, 22, 16}
var arrKeyValue = [5]string{3: "Chris", 4: "Ron"} // 只有索引3和4被赋予实际的指,其他元素都被设置为空的字符串
切片
切片(slice)是对数组一个连续片段的引用:
切片的长度可以在运行时修改,是一个长度可变的数组
切片提供计算容量的函数
cap()
,对切片s
,cap(s)
就是从s[0]
到切片来源数组末尾的长度,对切片s永远成立:0≤len(s)≤cap(s)
表示同一个数组片段的多个切片之间共享数据,因为是引用,所以不需要使用额外内存
声明切片以及初始化切片的方式:
var identifier []type // 声明,未初始化时为nil var slice1 []type = arr1[start:end] // 初始化,不包含末尾索引,左闭右开 var slice2 []type = arr1[:] // 初始化,包含数组内的所有元素 s := [3]int{1,2,3}[:] // (等价于先用 s := [3]int{1, 2, 3} 生成数组, 再使用 s[:] 转成切片) s := []int{1,2,3} // 甚至更精简的形式,类似数组的初始化方式 type intSlice []int // 使用type alias,初始化更简单了 s := intSlice{1,2,3} // 是 s := intSlice([]int{1,2,3}) 的精简形式
对切片的大小进行操作,切片只能向后移动,向前移动(s2 = s2[-1:]) 会导致编译错误:
slice1 = slice1[:len(slice1)-1] // 去掉最后一个元素 slice1 = slice1[:cap(slice1)] // 扩大到大小上限 s2 = s2[1:] // 可以将 s2 向后移动一位,但是末尾没有移动
不要用指针指向切片,切片本身就是一个指针,已经是引用类型
当数组没有定义时,使用make来初始化/创建一个切片(当然同时也创建了相关数组)
var slice1 []type = make([]type, len) // len 为切片长度 slice1 := make([]type, len) // 精简形式 slice1 := make([]type, len, cap) // 精简形式, 接收cap为切片容器大小
new和make的区别:
new(T)
为类型T分配一片内存,初始化为0并返回类型*T指针,不会做额外的初始化操作(所以内建类型需要make
),适用于值类型如数组和结构体make(T)
返回类型T的初始值,只适用于内建引用类型:slice
、map
与channe
,三者在内存中有多个组成部分,需要对内存中的组成部分初始化才能使用,make就是对三者进行初始化的一种操作方式- 试图
make()
一个结构体变量会引发编译错误,还不算太糟糕,但new
一个map
并试图向其填充数据,将会引发运行时错误,因为new(map)
返回的是nil
指针,它尚未被分配内存
bytes包
类似java / C#
中的StringBuilder类,通过声明bytes.Buffer对象,或者new一个出来,或者通过函数func
NewBuffer(buf []byte)
*Buffer
,创建一个 Buffer
对象并且用 buf
初始化好
for-range 结构
在for-range结构中使用_来忽略索引 / 变量
如果只需要索引,可以直接忽略第二个变量
seasons := []string{"Spring", "Summer", "Autumn", "Winter"} for _, season = range seasons { fmt.Printf("%s\n", season) } for ix := range seasons { fmt.Printf("%d", ix) }
切片重组(reslice)
切片复制与追加
- 使用
copy(to []T, from []T)
来将from切片中的内容拷贝到to切片中,覆盖to现有的元素,函数返回赋值的个数 - 使用
append(s []T, x ...T)
来将多个相同类型元素(如果需要追加切片,将第二个参数拓展成参数列表x = append(x, y...)
)追加到切片后,并返回新的切片。如果原切片容量不足,会分配新的数组给切片,所以新的切片可能不再指向同一个数组。 - 由2可以得到,如果将切片作为函数参数传入,在函数内部对切片进行了扩容,最好再将切片作为返回值传出,才能在函数外部得到扩容后的切片
字符串、数组和切片的应用
- 使用
substr := str[start:end]
来获取从索引start到end-1的子字符串 - 关于切片的操作,可以参考7.6.7 append() 函数常见操作
- 切片底层指向一个数组,只有不被任何切片指向的时候,数组的内存才会被释放,所以注意切片指向的数组,在必要时,以部分内容新建一个切片
- 使用
c := byte[](str)
来将字符串转化为字符切片,从而修改字符串内容,字符串内容无法被直接索引修改(放在等号左边)
Chapter8
Map 的声明、初始化和make
自然的,map是引用类型
map的声明格式如下,不需要长度,因为大小可以动态增长
var map1 map[keytype]valuetype var map1 map[string]int
key需要时可以用操作符比较的类型,value可以是任何类型
map可以用
map[keyType]valueType{key1: val1, key2: val2}
的描述方法来初始化,就像数组和结构体一样引用类型的map使用
make
进行初始化使用
make
初始化map时可以标明初始容量,当增长到容量上限时,map大小会自动+1将value定义成
[]int
类型或者其他类型的切片,来解决一个value对应多个值的问题
键值对检查,元素删除
- 可以使用
val1 = map1[key1]
的方法获取key1
对应的值val1
。如果map
中不存在key1
,val1
就是一个值类型的空值。 - 如何区分key1不存在还是对应value是空值?可以使用
val1, isPresent = map1[key1]
,当key存在于map中时,isPresent
会返回true
for-range 配套用法(遍历map)
- 和前面类似的,如果只关心值不关心key,可以
_,value := range map
,反之如果只关心key不关心值,可以key := range map
- 注意,map的本质是散列表,map的增长扩容会导致重新散列,同时Go设计者也让每次遍历的起点不一样,所以遍历map总是无序的,为了让我们不依赖map的遍历排序中的顺序
- 如果想要获得
map
类型的切片,就需要使用两次make()
函数,第一次用于分配切片,第二次用于分配切片中的每个map
元素;注意,当你使用range
来遍历分配时,获得的只是map值的拷贝,所以并不能真正进行map
元素的初始化
map的排序
- 由于
map
的无序性,当需要为map
排序时,考虑将key或value拷贝到切片中,对切片进行排序,再对切片使用for-range
,打印出所有key-value
Chapter9
标准库概述
简单记录一下,感觉用得上的
- io:基本输入输出
- bufio:缓冲输入输出
- path/filepath:系统目标文件名目录
- strings:字符串操作
- strconv:字符串转换为基础类型
- regexp:正则表达式
- bytes:字符型分片
- math:数学函数
- math/rand:伪随机数生成
- sort:数组排序和自定义集合
- encoding/json:编码解码json数据
- runtime:Go程序运行时交互,垃圾回收/协程创建
- reflect:运行时反射
锁与sync包
sync.Mutex
是一个互斥锁,守护在临界区入口保证同一时间只能有一个线程进入临界区type Info struct { mu sync.Mutex // ... other fields, e.g.: Str string } // example info.mu.Lock() // 临界区修改 info.mu.Unlock()
sync.RWMutex
锁,能通过RLock()
来允许同一时间多个线程对变量进行读操作,但只有一个现成能进行写操作
精密计算与big包
对于整数的高精度计算,提供了 big
包,被包含在 math
包下:有用来表示大整数的 big.Int
和表示大有理数的 big.Rat
类型
自定义包与可见性
import 的一般格式:
import “包的路径或URL地址”
,非自定义本地包一般存放在$GOROOT/src(官方包)下或$GOPATH/src(第三方包)下(module mode)用相对路径(”./”)import自定义包在module mode下不支持,可以使用从module名(在go.mod中定义)开始相对路径的方式进行import
这里是enable go modules integration状态
(module mode)如果需要用go install / go get 来下载package,需要打开module mode
(module mode)通过go env 来查看是否打开了GO MODULE MODE
(module mode)通过go install / go get下载的package,会被下载到$GOPATH的pkg目录下
(module mode)要使用下载的package,需要编辑项目的go.mod(是不是IDE帮我自动编辑了我不是很清楚)
使用
import [name] “path”
,来给import的包起一个别名,使用import _ “path”
,来忽略这个包,仅通过import执行package的init方法
Chapter10
结构与方法
- Go通过类型别名和结构体的形式支持用户自定义类型
- 结构体也是值类型,所以通过
new()
函数来创建 - Go语言中没有类的概念,因此结构体有着更为重要的地位
结构体定义
结构体的定义方式:
type identifier struct { field1 type1 field2 type2 }
声明结构体变量的方式
var t *T t = new(T) // 使用new(),t的类型是*T var s T T.a = 5 // 直接声明结构体变量,t的类型是T //结构体字面量:struct-literal ms := struct1{10, 15.5, "Chris"} //必须按照字段顺序写,ms的类型是struct1 ms := &struct1{10, 15.5, "Chris"} // 同上,ms的类型是*struct1 intr := Interval{end:5, start:1} // 可以不按照字段顺序,可以不给每一个字段赋值 r1 := Rect1{Point{10, 20}, Point{50, 60}} // 嵌套
对于一个结构体类型和一个结构体类型指针,都可以用相同的选择器符(selector-notation),即
.
符号来引用结构体的字段type myStruct struct { i int } var v myStruct // v 是结构体类型变量 var p *myStruct // p 是指向一个结构体类型变量的指针 v.i p.i // 这里会报错,因为p指针为nil
当为结构体定义了一个
alias
类型时,结构体和它的alias
类型具有相同的底层类型,可以互相转换type number struct { f float32 } type nr number // alias type
使用
fmt.Println()
打印一个结构体的默认输出可以很好的显示它的内容,类似使用%v
选项。使用%+v
选项会先输出字段名字再输出字段的值,使用%#v
字段会在前者基础上先输出结构体的名字The name of person is {EDWARD CHEN} The name of person is &{firstName:EDWARD lastName:CHEN} The name of person is chapter10.Person{firstName:"EDWARD", lastName:"CHEN"}
fmt.Printf
/fmt.Print
/fmt.Println
均会自动使用结构体的String 方法,通过给出结构体为接收者的String方法来进行自定义
使用工厂方法创建结构体实例
- Go语言不支持面向对象编程语言中的构造方法,但是可以可以很容易实现“构造子工厂”方法,按惯例以“new…”或“New…”开头
- 通过可见性原则可以将结构体由小写开始,来禁止使用
new()
函数,强制用户使用工厂方法
带标签的结构体
类似注释,在结构体的字段类型后面可以用""
进行字段的解释,称为标签(tag)
type TagType struct { // tags
field1 bool "An important answer"
field2 string "The name of the thing"
field3 int "How much there are"
}
匿名字段和内嵌结构体
- 可以包含一个或多个匿名字段,这些字段只有类型是必须的,匿名字段本身也可以是一个结构体,即内嵌结构体;结构体中每一种数据类型只能有一个匿名字段
- 匿名字段/内嵌结构体 被用来模拟类似继承的行为,Go语言通过内嵌或组合来实现
- 当只有一个内嵌结构体时,可能看起来像是继承;当有多个内嵌结构体,看起来就会像是组合;实际上当只有一个内嵌结构体时,按组合去理解也会更贴近go的设计
- 不难得到,类型A内嵌了类型B时,类型B指针无法指向类型A实例(即使类型B是唯一的内嵌类型)
- 外层结构体可以直接通过选择器符
.
直接访问内层结构体的字段 - 两个字段拥有相同名字时:外层名字覆盖内层名字,同一层级的相同名字字段需要明确来自哪一个类型(c.A.a,c.B.a)
方法
方法
- 结构体像是类的简化形式,而方法是在接收者上的函数,接收者可以是某种类型的值,或者是其他允许类型的指针。
- 接收器不可以是一个接口类型,因为接口是抽象定义。
- 一个结构体加上它的方法等价于面向对象中的一个类,在Go中结构体和绑定的方法可以不在一起,但是需要在同一个包内
- 不允许方法重载,可以重载类型
- 调用方法的方式:
recv.Method1()
,如果recv可以是类型值,或类型指针(自动解引用)
函数方法的区别
函数将变量作为参数:Function1(recv)
,方法在变量上被调用:recv.Method1()
指针或值作为接收者
- recv常见的是一个指向receiver_type的指针,这样就不会产生结构体的拷贝,而且可以修改结构体的内容
- 指针方法和值方法都可以在指针或者非指针(值)上被调用,go会自动为你做类型转换
方法与未导出字段
对于未导出的字段,使用getter
和setter
方法进行修改,在Go中setter
方法会使用Set前缀,而getter
只使用成员名
内嵌类型的方法和继承
匿名类型被内嵌到结构体中时,匿名类型的可见方法也被内嵌,这相当于实现了继承
内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。
外层类型的方法会覆写内嵌类型对应的方法,想要调用内嵌类型的方法,用内嵌类型名进行访问
func (n *NamedPoint) Abs() float64 { return n.Point.Abs() * 100. }
如何在类型中嵌入功能
聚合(组合):包含所需功能类型的具体字段;内嵌:使用内嵌类型,把所需的功能类型内嵌
// 聚合(组合) type Customer struct { Name string log *Log } // 内嵌 type Customer struct { Name string Log }
多重继承
在类型中嵌入必要的父类型就能简单地实现多重继承
与其他面向对象语言的比较…
- go中分离了类型与方法,方法只要定义就可以调用,与其他类型是否存在方法并无关系
- go中并不使用类继承的概念,“继承”的两个好处,代码复用通过组合和委托实现,多态通过接口的使用来实现。又是这也叫组件编程 (Component Programming) 面向对象设计 OOP
垃圾回收与SetFinalizer
如果需要在一个对象被内存移出前做一些特殊操作,可以通过如下方法调用实现
Chapter11
接口
接口定义了一组方法,不包含实现,不包含变量
可以声明一个接口类型的变量,初始值为nil,本质是一个指针(内部的receiver为类型变量,method table ptr指向该类型的方法表)
如果类型的所有方法组成的集合,包含了该接口的方法集,这个类型就实现了这个接口
除了接口被隐式地实现,其他都与你熟悉的接口差不多
接口类型的变量可以指向实现了此接口的实例
type Shaper interface { Area() float32 } type Square struct { side float32 } func (sq *Square) Area() float32 { return sq.side * sq.side } func main() { sq1 := new(Square) sq1.side = 5 var areaIntf Shaper areaIntf = sq1 // shorter,without separate declaration: // areaIntf := Shaper(sq1) // or even: // areaIntf := sq1 fmt.Printf("The square has area: %f\n", areaIntf.Area()) }
实现了同一接口的实例可以填充到同一个数组 / 切片中
接口是一种契约,实现接口的类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口将类型能做什么,与如何做分离开来,使得变量可以在不同时刻表现出不同行为。
接口嵌套接口
一个接口可以包含一个或多个其他接口,相当于将内嵌接口的方法列举在外层接口中
类型断言
使用类型断言来检测接口varI是否包含类型T的值
if v, ok := varI.(T); ok { // checked type assertion Process(v) return } // varI is not of type T // ------- // 如果只是想测试一下是否是类型T if _, ok := varI.(T); ok { // ... }
注意如果实现接口的receiver是
T
类型,那么T
和*T
的检测都可行(不会有编译报错,是否通过取决于3↓);如果receiver是*T
类型,那么T
的检测会有编译报错即:使用T指针类型作为receiver,导致:
- 无法进行接口变量的T类型断言
- 无法将T类型值赋值给接口变量
// 举例:receiver是*T类型 type Shaper interface { Area() float32 } func (sq *Square) Area() float32 { return sq.side * sq.side } func main() { var areaIntf Shaper sq1 := new(Square) sq1.side = 5 areaIntf = sq1 // Is Square the type of areaIntf? if t, ok := areaIntf.(*Square); ok { fmt.Printf("The type of areaIntf is: %T\n", t) } }
至于
T
类型还是*T
类型的检测会通过,取决于赋值给接口变量的是结构体值还是结构体的指针
类型判断
接口变量的类型可以用以下特殊形式(在11.3的类型处使用type
关键字)的switch语句来进行检测:type-switch
switch t := areaIntf.(type) { // <-
case *Square:
fmt.Printf("Type Square %T with value %v\n", t, t)
case *Circle:
fmt.Printf("Type Circle %T with value %v\n", t, t)
case nil:
fmt.Printf("nil value: nothing to check?\n")
default:
fmt.Printf("Unexpected type %T\n", t)
}
测试是否实现了接口
与11.3类似,不同的是将类型替换为接口名,即可检测变量是否实现了特定接口
使用方法集与接口
在方法一节我们讨论过,当调用变量的方法是,变量是指针还是值都能够正常工作。但是当变量是接口类型时多了一些限制,原因是接口变量中存储的具体值是不可寻址的(可以理解为接口变量内存储了一个结构体的值,而非存储了存有结构体地址的指针,但是unsafe.Sizeof打印出来大小都是两个地址的16byte…):
所以分两种调用方法的情况:(1)将结构体值赋值给了接口变量,以及(2)将结构体指针赋值给了接口变量。
对于后者,结构体实现的接口方法中,接收者是值以及是指针的方法都能正常调用;但是对于前者,结构体实现的接口方法中,接收者是指针的方法将无法调用
总结
- 指针方法可以通过指针调用
- 值方法可以通过值调用
- 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
- 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址
结合11.3 可以得到
- 只要存在一个用类型指针作为接收者的接口方法,就不能用值来给接口变量赋值,从而值类型断言也没有意义了;
- 反之,如果所有的接口方法都只用值作为接收者实现,就有很大的局限性(方法不能改变结构成员),反而没有意义;虽然可以自由地用值 / 指针给接口变量赋值,但是意义不大;不用担心无法调用接收者是指针的方法,因为不存在这种方法;
- 所以可以考虑一般情况下:实现接口方法时,接收者是值还是指针从实际出发,并且统一用指针给接口变量赋值,类型断言也用指针类型来判断;虽然无法用值给接口变量赋值,少一种选择反而少一些负担,不用担心无法调用接收者是指针的方法,因为不会用值来给接口变量赋值、也让类型断言变简单,只要考虑接口的指针类型断言就可以了
*实际上是类型A实现了接口还是指针类型A实现了接口本身就是两种情况,不要混为一谈会更好理解。方法调用时不区分值还是指针可以看作是语法糖,而此处指针赋值的接口变量,可以调用接收者是值得方法也是做了额外处理。
空接口
一个空接口对实现不做任何要求
type Any interface {} // 对于函数参数 interface {} 也是可行的
var val interface {} // 空接口类型
空接口类似
Java/C#
中所有类的基类:Object
类,二者的目标也很相近,可以给一个空接口类型变量赋任何类型的值使用空接口来实现类似泛型的功能,将一个空接口类型作为Element
type Vector struct { a []Element } func (p *Vector) At(i int) Element { return p.a[i] } func (p *Vector) Set(i int, e Element) { p.a[i] = e }
不能将特定类型的切片直接赋值给空接口切片,因为他们的内存布局不一致,需要for-range来一个一个显式赋值
一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法,注意这个转换在运行时检查,转换失败将导致一个运行时错误
type myPrintInterface interface { print() } func f3(x myInterface) { x.(myPrintInterface).print() // type assertion to myPrintInterface }
反射
- 使用reflect包来进行反射
reflect.TypeOf
和reflect.ValueOf
用于返回被检查对象的类型和值,他们的参数类型都是interface{}reflect.ValueOf
返回的reflect.Value类型对象存在Type()
返回类型,Kind()
返回一个常量来表示类型,同样当你确定了reflect.Value的确切类型,有叫做Int(),Float()的方法来获取存储值,或者直接用Interface
来获取空接口类型值- 通过反射来修改值时,可以通过对Value的CanSet方法来检查是否可设置,如果想要Value对象可设置,有时需要用指针来调用
ValueOf
,并且需要使用Elem()
函数 - 使用
Field(index int)
来获取结构体的成员,并且通过SetInt
,SetString
等接口设置值 - 使用
NumField
来获取结构体有多少个成员 - 使用
Method(index int)
来获取结构体的方法,并且使用Call()
进行调用
接口与动态类型
Go中没有类,数据(结构体或是一般类型)与方法是一种松耦合的正交关系
Go中的接口都是必须提供一个方法集的实现,但是更灵活,都是隐式实现而不用显式声明,类型和接口之间也是松耦合的
使用接口类型作为参数的函数,类似于动态类型,更看重对象能做什么,而不是对象是什么
Go的动态方法调用,需要编译器静态检查的支持,变量赋值给接口类型变量时,编译器会检查类型是否实现了接口所有的函数。一般传入像
interface{}
这样的泛型变量,之后再用类型断言来进行变量的检查。接口的继承:当类型内嵌另一个实现了接口的类型指针时,该类型就可以使用另一个类型的所有接口方法,通过组合的方式实现了继承
type Task struct { Command string *log.Logger // log.Logger实现了Log()方法 } task.Log() // Task类型的实例调用Logger的Log()方法
Go中的面向对象总结
面向对象中的重要概念:封装、继承、多态
(之前读到的内容说继承是实现多态的一种方式 面向对象设计 OOP …不过这里就先不把问题复杂化了,先看Go语言如何实现面向对象的重要概念)
封装:简化的访问层级,首字母小写的标识符包内可见,首字母大写的标识符包外可见
继承:用内嵌类型这样“组合”的方式来实现
多态:用接口来实现,实现了接口规定方法集的类型实例,可以赋给接口类型变量