Go
常量
- 定义格式 const Name [Type] = Value
变量
- 定义格式 var Name [Type]
- 系统自动赋予初值
- 全局变量希望能被外部包所使用,则需要将变量首字母大写
- 函数体内定义的变量为局部变量,否则为全局变量
- 可以省去Type,变量在被赋值时编辑器会在编译阶段做类型推断
- 当你在函数体内声明局部变量时,应使用简短声明语法
:=
- 值类型和引用类型
- 值类型用等号赋值的时候,实际上是在内存中做了值拷贝
- int float bool string 数组 struct
- 存储在栈内
- 引用类型变量存储的是值所在的内存地址
- 指针 slices maps channel
- 存储在堆中
- 局部变量的简短化创建形式a := 50
- 局部变量不可以声明了但却不使用,全局变量可以
- init函数
- 变量可以在init函数中被初始化,init函数在每个包完成初始化后自动执行,优先级比main高
- 值类型用等号赋值的时候,实际上是在内存中做了值拷贝
基本类型和运算符
- bool 格式化输出时,可以用%t来表示要输出的类型为bool
- 数值类型:
- int和uint根据操作系统的位数,决定数值的长度(4位或者8位)
- uintptr长度被设定为足够放一个指针即可
- 整数:
- int8(-128 -> 127)
- int16(-32768 -> 32767)
- int32(-2,147,483,648 -> 2,147,483,647)
- int64(-9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)
- 无符号整数:
- uint8(0 -> 255)
- uint16(0 -> 65,535)
- uint32(0 -> 4,294,967,295)
- uint64(0 -> 18,446,744,073,709,551,615)
- 浮点数:
- float32(+- 1e-45 -> +- 3.4 * 1e38):小数点后7位
- float64(+- 5 1e-324 -> 107 1e308):小数点后15位
- 复数:
- complex64(32位实数和虚数)
- Complex128(64位实数和虚数)
- 随机数:
- rand包
- 类型别名:
- type 别名 类型
- 字符类型
- char
- 实际存储了整型
- 字符串
- 字符串是字节的定长数组
- 解释字符串,用双引号括起来
- 非解释字符串,反引号括起来
- 字符串的二元运算符比较,逐个字节对比
- 获取字符串中某个字节的地址是非法的$str[i]
- 字符串使用+拼接
- strings和strconv包(String 库函数的使用)
- 指针:
- 一个指针变量可以指向任何一个值得内存地址
控制结构(省去了condition两侧的括号,使得代码更加整洁,执行语句中的括号在任何情况下都不能被省略)
- if-else
- if-else if-else
- 测试多返回值函数的错误
- 方法可以返回多个返回值,第二个返回值可以是错误的详细信息,如果第二个返回值不为Nil,则代表发生了错误。
- switch case:
- 不需要写break
- 如果希望匹配到之后还继续执行后面的分支,用“fallthrough”关键字
- switch 语句的第二种形式是不提供任何被判断的值(实际上默认为判断是否为 true),然后在每个 case 分支中进行测试不同的条件。当任一分支的测试结果为 true 时,该分支的代码会被执行。这看起来非常像链式的 if-else 语句,但是在测试条件非常多的情况下,提供了可读性更好的书写方式。
- switch的第三种形式是condition中可以对两个变量进行计算赋值。随后在case分支中根据变量的值进行具体的行为
- 循环:for结构
- 基本形式:for 初始化语句;条件语句;修饰语句{}
- 第二种形式,类似于while循环。没有初始化语句和index更新语句
- 第三种形式,无限循环。for {}
- for-range结构:
for ix, val := range coll { }
- Break 和 continue
- label和goto(不推荐使用,没看)
函数
分类:普通的带有名字的函数、匿名函数、方法
go里面函数重载是不允许的,没有泛型,为了效率
函数的一般定义:
func f(name1 type1,name 2type2) 返回值类型
,参数可以没有参数名。函数都是按照值传递的
带命名的返回值,只需要在函数尾部直接return
不带命名的返回值,需要用()装起来写在return后面
空白符
_
匹配一些不需要的值,然后丢掉通过传递指针来改变函数外部变量的值
变长参数函数
形式:
func myFunc(a,b,arg ...int){}
如果一个变长参数的类型没有被指定,则可以使用默认的空接口
interface{}
,这样就可以接受任何类型的参数`func typecheck(..,..,values … interface{}) {
for _, value := range values { switch v := value.(type) { case int: … case float: … case string: … case bool: … default: … } }
}`
defer和追踪
- defer作用:类似于finally,用于一些资源的释放
- 使用defer来记录函数的参数和返回值
将函数作为参数
func IndexFunc(s string, f func(c int) bool) int
闭包
- 匿名函数赋值给变量:
fplus := func(x, y int) int { return x + y }
- 直接调用匿名函数:
func(x, y int) int { return x + y } (3, 4)
- 匿名函数的调用,在匿名函数后加一对()表示对其的调用
- 匿名函数赋值给变量:
应用闭包:将函数作为返回值
闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量。
在闭包中使用到的变量可以是在闭包函数体内声明的,也可以是在外部函数声明的:
`var g int
go func(i int) {s := 0 for j := 0; j < i; j++ { s += j } g = s
}(1000) // Passes argument 1000 to the function literal.`
数组与切片
数组
- 声明语句:
var identifier [len]type
- 使用for循环遍历
`for i:=0; i < len(arr1); i++{
arr1[i] = ...
}`
- 使用for-range遍历
`for i:=0; i < len(arr1); i++{
arr1[i] = ...
}`
- Go 语言中的数组是一种 值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过
new()
来创建:var arr1 = new([5]int)
。arr1的类型是*[5]int,把arr1赋值给另一个时,需要做一次数组内存的拷贝。 - 讲数组作为函数参数时,会做一次数组的拷贝,如果需要修改传入数组的值,需要用引用传递的方式
- 数组可以在声明时使用{}来初始化
- 声明语句:
切片
定义和相关特性
- 切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型
- 和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个 长度可变的数组。
- 多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。
优点:
- 因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片比数组更常用。
声明:
var identifier []type
(不需要说明长度)。初始化:
var slice1 []type = arr1[start:end]
头闭尾开区间- 类似数组的初始化:
var x = []int{2, 3, 5, 7, 11}
。这样就创建了一个长度为 5 的数组并且创建了一个相关切片。
长度:存储的值的个数
容量:
cap()
可以测量切片最长可以达到多少:它等于切片从第一个元素开始,到相关数组末尾的元素个数对于每个切片,以下状态总是成立:
s == s[:i] + s[i:] // i是一个整数且: 0 <= i <= len(s) len(s) <= cap(s)
切片的存储类似结构体:
- 指向相关数组的指针
- 长度
- 容量
将切片传递给函数:
`func sum(a []int) int {
s := 0 for i := 0; i < len(a); i++ { s += a[i] } return s
}
func main() {
var arr = [5]int{0, 1, 2, 3, 4} sum(arr[:])
}`
使用make创造一个切片:
var slice1 []type = make([]type, len)
。- 简写为:
slice1 := make([]type, len)
- make 的使用方式是:
func make([]T, len, cap)
,其中 cap 是可选参数。 - 以下两种创建切片的方法等效:
make([]int, 50, 100)
new([100]int)[0:50]
make和new的区别
- 看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。
- new (T) 为每个新的类型 T 分配一片内存,初始化为 0 并且返回类型为 * T 的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体(参见第 10 章);它相当于 &T{}。
- make(T) 返回一个类型为 T 的初始值,它只适用于 3 种内建的引用类型:切片、map 和 channel
- 换言之,new 函数分配内存,make 函数初始化
bytes包Buffer(类似于java里面的StringBuilder)
- 申明方式:
var buffer bytes.Buffer
- 获取指针:
var r *bytes.Buffer = new(bytes.Buffer)
- 或者通过函数:
func NewBuffer(buf []byte) *Buffer
- 申明方式:
切片的for-range
单维切片:
`for ix, value := range slice1 {
...
}`
多维切片:
`for row := range screen {
for column := range screen[row] { screen[row][column] = 1 }
}`
切片重组(扩容)
- 扩展一位:
sl = sl[0:len(sl)+1]
- 扩展一位:
切片的复制与增加
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原切片的内容都拷贝过来。
通过
func append(s[]T, x ...T) []T
在切片中追加内容- 例子
sl3 := []int{1, 2, 3} sl3 = append(sl3, 4, 5, 6)
- 例子
通过拷贝讲切片复制到新的切片中
func copy(dst, src []T) int
,返回拷贝的元素的个数例子:
sl_from := []int{1, 2, 3}
sl_to := make([]int, 10)
n := copy(sl_to, sl_from)
Map
声明:
var map1 map[keytype]valuetype
赋值:
map1[key1] = val1
取值:
v := map1[key1]
获取长度:
len(map1)
初始化:
{key1:val1, key2:val2}
make初始化:
map1 := make(map[keytype]valuetype)
相当于``mapCreated := map[string]float32{}`切片作为map的值:
mp1 := make(map[int][]int) mp2 := make(map[int]*[]int)
检验key是否存在:
`if _, ok := map1[key1]; ok {
// ...
}`
删除kv:
delete(map1, key1)
for-range遍历:
kv:
`for key, value := range map1 {
...
}`
只关心value
`for _, value := range map1 {
...
}`
只关心key
`for key := range map1 {
fmt.Printf("key is: %d\n", key)
}`
切片:
两次make,第一次分配切片,第二次分配切片中每个map元素
`items := make([]map[int]int, 5)
for i:= range items {items[i] = make(map[int]int, 1) items[i][1] = 2
}`
map排序
- 拷贝出key,对key排序,然后顺序遍历key取出value(会不会效率太低了?)
将kv对调:
- 拷贝一个新的大小相同的map,遍历原始map,复制数据到新的map
包
- 标准库
- regexp包
- sync包
- 紧密计算big包
- 自定义包和可见性:
- Import with . :import . “./pack1”,当使用. 来做为包的别名时,可以不通过包名来使用其中的项目。例如:test := ReturnStr()。在当前的命名空间导入 pack1 包,一般是为了具有更好的测试效果。
- Import with _ :import _ “./pack1”,pack1 包只导入其副作用,也就是说,只执行它的 init 函数并初始化其中的全局变量。
- 导入外部安装包:先通过go install安装
结构体和方法(struct & method)
结构体:
定义:
1
2
3
4
5type identifier struct {
field1 type1
field2 type2
...
}使用new
t := new(T)
使用声明:
var t T
:分配内存并零值化内存使用
.
选择器来访问结构体的属性,无论变量是结构体还是结构体类型的指针1
2
3
4
5type myStruct struct { i int }
var v myStruct // v是结构体类型变量
var p *myStruct // p是指向一个结构体类型变量的指针
v.i
p.i使用
{}
初始化一个结构体1
2ms := &struct1{10, 15.5, "Chris"}
// 此时ms的类型是 *struct1表达式
new(Type)
和&Type{}
是等价的。或者
1
2var ms struct1
ms = struct1{10, 15.5, "Chris"}或者制定字段key来初始化
1
2
3intr := Interval{0, 3} (A)
intr := Interval{end:5, start:1} (B)
intr := Interval{end:5} (C)结构体的内存布局:结构体和它所包含的数据在内存中是以连续快的形式存在的。
递归结构体:可以用来定义链表的节点或者二叉树的节点
make不能用于struct
结构体可以带Tag,通过反射获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package main
import (
"fmt"
"reflect"
)
type TagType struct { // tags
field1 bool "An important answer"
field2 string "The name of the thing"
field3 int "How much there are"
}
func main() {
tt := TagType{true, "Barak Obama", 1}
for i := 0; i < 3; i++ {
refTag(tt, i)
}
}
func refTag(tt TagType, ix int) {
ttType := reflect.TypeOf(tt)
ixField := ttType.Field(ix)
fmt.Printf("%v\n", ixField.Tag)
}匿名字段和内嵌结构体
- 匿名字段:通过
结构体名字.字段类型
来访问匿名字段,没个结构体针对每一种数据类型只能有一个匿名字段 - 内嵌结构体:结构体可以通过
结构体名字.内嵌结构体字段
来访问内嵌匿名结构体的字段,类似于软件工程领域的组合设计模式 - 命名冲突:外层结构体的相同命名字段会覆盖内层结构体的相同命名字段,访问外层结构体的相同命名字段
A.b
,访问内层结构体的相同命名字段A.B.b
- 匿名字段:通过
方法:
在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。
类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集。
因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的:
1
2func (a *denseMatrix) Add(b Matrix) Matrix
func (a *sparseMatrix) Add(b Matrix) Matrix定义方法的格式
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
方法的调用方式:
recv.methodName()
,recv类似于面向对象语言中的this或者self一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28package main
import "fmt"
type TwoInts struct {
a int
b int
}
func main() {
two1 := new(TwoInts)
two1.a = 12
two1.b = 10
fmt.Printf("The sum is: %d\n", two1.AddThem())
fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20))
two2 := TwoInts{3, 4}
fmt.Printf("The sum is: %d\n", two2.AddThem())
}
func (tn *TwoInts) AddThem() int {
return tn.a + tn.b
}
func (tn *TwoInts) AddToParam(param int) int {
return tn.a + tn.b + param
}函数和方法的区别:
函数将变量作为参数:Function1(recv)
方法在变量上被调用:recv.Method1()
方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。
指针作为接受者:
- 传入指针或者值都是合法的,go会自动解引用
- 指针方法和值方法都可以在指针或者非指针上被调用
获取或者设置对象的值使用getter和setter
多重继承可以通过一个类型内嵌多个匿名类型来实现,匿名类型的方法会被提升为此父类型的方法
总结:
- 在Go中,类型就是类
- Go拥有类似面向对象语言的嘞继承的概念以实现代码复用和多态
- go中代码复用通过组合和委托实现,多态用接口来实现。
- 类型可以覆写内嵌匿名类型的方法
接口与反射
接口
定义:接口提供了一种方式来说明对象的行为:如果谁能搞定这件事,它就可以用在这儿。
接口定义了一组方法,但是这些方法不包含实现,接口内也不能拥有变量
接口定义语法:
1
2
3
4
5type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。
实现某个接口的类型(除了实现接口方法外)可以有其他的方法。
一个类型可以实现多个接口。
接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)。
例子(go中的多态?)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28package main
import "fmt"
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())
}接口嵌套接口
一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}
类型断言:如何监测和转换接口变量的类型?
定义:一个接口类型的变量varI中可以包含任何类型的值,必须有一种方式来检测它的动态类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。通常我们可以使用类型断言来测试某个时刻varI是否包含类型T的值:
v := varI.(T)
varI必须是一个接口类型变量。类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:
1
2
3
4
5if v, ok := varI.(T); ok { // checked type assertion
Process(v)
return
}
// varI is not of type T如果转换合法,
v
是varI
转换到类型T
的值,ok
会是true
;否则v
是类型T
的零值,ok
是false
,也没有运行时错误发生。例子:(暂时还不能明白这个的用处2021/12/2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43package main
import (
"fmt"
"math"
)
type Square struct {
side float32
}
type Circle struct {
radius float32
}
type Shaper interface {
Area() float32
}
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)
}
if u, ok := areaIntf.(*Circle); ok {
fmt.Printf("The type of areaIntf is: %T\n", u)
} else {
fmt.Println("areaIntf does not contain a variable of type Circle")
}
}
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
func (ci *Circle) Area() float32 {
return ci.radius * ci.radius * math.Pi
}
类型判断:type-switch
接口变量的类型也可以使用type-switch结构来判断
接口类型变量可以代表任何类型,所以需要有类型判断
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18func classifier(items ...interface{}) {
for i, x := range items {
switch x.(type) {
case bool:
fmt.Printf("Param #%d is a bool\n", i)
case float64:
fmt.Printf("Param #%d is a float64\n", i)
case int, int64:
fmt.Printf("Param #%d is a int\n", i)
case nil:
fmt.Printf("Param #%d is a nil\n", i)
case string:
fmt.Printf("Param #%d is a string\n", i)
default:
fmt.Printf("Param #%d is unknown\n", i)
}
}
}可以这样调用此方法:classifier(13, -14.3, “BELGIUM”, complex(1, 2), nil, false) 。
在处理来自于外部的、类型未知的数据时,比如解析诸如 JSON 或 XML 编码的数据,类型测试和转换会非常有用。
测试一个值是否实现了某个接口:
1
2
3
4
5
6
7type Stringer interface {
String() string
}
if sv, ok := v.(Stringer); ok {
fmt.Printf("v implements String(): %s\n", sv.String()) // note: sv, not v
}接口是一种契约,实现类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口彻底将类型能做什么,以及如何做分离开来,使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。
使用方法集与接口(这节有点晦涩)
总结
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以从具体类型 P 直接可以辨识的:
- 指针方法可以通过指针调用
- 值方法可以通过值调用
- 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
- 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址
Go 语言规范定义了接口方法集的调用规则:
- 类型 *T 的可调用方法集包含接受者为 *T 或 T 的所有方法集
- 类型 T 的可调用方法集包含接受者为 T 的所有方法
- 类型 T 的可调用方法集不包含接受者为 *T 的方法
空接口
概念:不包含任何方法,它对实现不做任何要求(类似于Java中的Object对象)
可以给一个空接口类型的变量
var val interface {}
赋值任何类型的值复制数据切片到空接口切片是不允许的,因为内存布局不一致,需要用for-range逐个复制
1
2
3
4
5var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
interfaceSlice[i] = d
}一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法
反射
概念:反射是用程序检查气所拥有的的结构,尤其是类型的一种能力;这是元编程的一种形式,反射可以在运行时检查类型和变量,例如大小方法和动态的调用这些方法。
Go中反射包Type用来表示一个Go类型,反射包Value为Go值提供了发射接口
两个函数:
1
2func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value通过反射修改或者设置值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
v := reflect.ValueOf(x)
// setting a value:
// v.SetFloat(3.1415) // Error: will panic: reflect.Value.SetFloat using unaddressable value
fmt.Println("settability of v:", v.CanSet())
v = reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of v:", v.Type())
fmt.Println("settability of v:", v.CanSet())
v = v.Elem()
fmt.Println("The Elem of v is: ", v)
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(3.1415) // this works!
fmt.Println(v.Interface())
fmt.Println(v)
}
//输出:
settability of v: false
type of v: *float64
settability of v: false
The Elem of v is: <float64 Value>
settability of v: true
3.1415
<float64 Value>反射结构体:
- ``NumField()
- 通过for循环用索引取得每个字段的值
Field(i)
- 用签名在结构体上的方法,例如,使用索引 n 来调用:
Method(n).Call(nil)
- ``NumField()
接口与动态类型:
Go 中的接口跟 Java/C# 类似:都是必须提供一个指定方法集的实现。但是更加灵活通用:任何提供了接口方法实现代码的类型都隐式地实现了该接口,而不用显式地声明。
和其它语言相比,Go 是唯一结合了接口值,静态类型检查(是否该类型实现了某个接口),运行时动态转换的语言,并且不需要显式地声明类型是否满足某个接口。该特性允许我们在不改变已有的代码的情况下定义和使用新接口。
接收一个(或多个)接口类型作为参数的函数,其实参可以是任何实现了该接口的类型。 实现了某个接口的类型可以被传给任何以此接口为参数的函数 。
接口的继承:
当一个类型包含(内嵌)另一个类型(实现了一个或多个接口)的指针时,这个类型就可以使用(另一个类型)所有的接口方法。类型可以通过继承多个接口来提供像
多重继承
一样的特性:1
2
3
4type ReaderWriter struct {
*io.Reader
*io.Writer
}
Go中的面相对象总结:
- Go 没有类,而是松耦合的类型、方法对接口的实现。
- 封装:
- 包范围内的:通过标识符首字母小写,
对象
只在它所在的包内可见 - 可导出的:通过标识符首字母大写,
对象
对所在包以外也可见
- 包范围内的:通过标识符首字母小写,
- 继承:
- 用组合实现:内嵌一个(或多个)包含想要的行为(字段和方法)的类型;多重继承可以通过内嵌多个类型实现
- 多态:
- 用接口实现:某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体,而且:接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。
错误处理与测试:
Go中预定义的error类型接口
1
2
3type error interface {
Error() string
}定义错误:
err := errors.New(“math - square root of negative number”)
运行时异常和Panic
当发生像数组下标越界或类型断言失败这样的运行错误时,Go 运行时会触发运行时 panic,伴随着程序的崩溃抛出一个 runtime.Error 接口类型的值。这个错误值有个 RuntimeError() 方法用于区别普通错误。
panic 可以直接从代码初始化:当错误条件(我们所测试的代码)很严苛且不可恢复,程序不能继续运行时,可以使用 panic 函数产生一个中止程序的运行时错误。panic 接收一个做任意类型的参数,通常是字符串,在程序死亡时被打印出来。Go 运行时负责中止程序并给出调试信息。
Panic的调用方式:
在多层嵌套的函数调用中调用 panic,可以马上中止当前函数的执行,所有的 defer 语句都会保证执行并把控制权交还给接收到 panic 的函数调用者。这样向上冒泡直到最顶层,并执行(每层的) defer,在栈顶处程序崩溃,并在命令行中用传给 panic 的值报告错误情况:这个终止过程就是 panicking。
从Panic中恢复
- panic 会导致栈被展开直到 defer 修饰的 recover () 被调用或者程序中止。
The Cargo Book
Cargo Guide
What is cargo?
Cargo is the Rust package manager. It is a tool that allows Rust packages to declare their various dependencies and ensure that you’ll always get a repeatable build.
Creating a new package
cargo new hello_world --bin
cargo build
cargo run
cargo build --release
Working on an existing package
git clone
cargo build
: fetch dependencies and build them
Package layout
Cargo.toml
andCargo.lock
are stored in the root of your packagesrc
for source codesrc/lib.rs
for default library files- Other executables can be placed in
src/bin/
. - Benchmarks go in the
benches
directory. - Examples go in the
examples
directory. - Integration tests go in the
tests
directory.
toml and lock
Cargo.toml
is about describing your dependencies in a broad sense, and is written by you.Cargo.lock
contains exact information about your dependencies. It is maintained by Cargo and should not be manually edited.cargo update
will update dependencites to newest version
Tests
- Command
cargo test
- run unit tests in /src/tests dir
- run integration-style tests in /tests dir
- Command
Cargo Reference
- Specifying Dependencies
- specifying dependencites from crates.io:default choice
- Caret requirements:an update is allowed if the new version number does not modify the lefct-most non-zero digit in the major,minor,patch grouping
- Tilde requirements:specify a minimal version with some ability to update(not specified part can be modified)
- Wildcard requirements:allow for any version where the wildcard is positioned
- comparison requirements: allow manually specifying a version range or an exiact version to depend on
- multiple requirements:eperated with comma
- specifying dependencies from other registries
- specifying depemdencies from git repositories
- specifying path dependencies
- Mutiple locations
- Platform specified dependencies
- Specifying Dependencies
Cargo commands
- General Commands
- cargo
- cargo help
- cargo version
- Build Commands
cargo bench
:execute benchmarks of a packagecargo build
:Compile the current packagecargo check
: check a local package and all of its dependencies for errorscargo clean
:remove artifacts feom the target directory that Cargo has generated in the pastcargo doc
:build the documentation for the local pakage and all dependencies.the output is placed in target/doccargo fetch
:fetch dependencies of a pakage from the networkcargo fix
:automatically fix lint warnings reported by rustccargo run
:run binary or exaple if local packagecargo rustc
:copile the current packagecargo rustdoc
:build a pakage’s documentationcargo test
: execute unit and integration test of package
- Manifest Commands
cargo generate-lockfile
:create cargo.lock file for the curren package or workspace.if already exists, rebuild the lastest avaliable version of every packagecargo locate-project
: print a JSON object to stdout with the full path to the Cargo.toml manifestcargo metadata
:output JSON to stdout containning info about the memebers and resolved deoendencies of the current package,–format-version is recommendedcargo pkgid
: print out the fully qualified package ID specifier for a package or dependency in the curren workspacecargo tree
:display a tree of dependencies to the terminalcargo update
:update dependencies as recorded in the local lock filecargo vendor
:vendor all crates.io and git dependencies for a project into the specified directory at<path>
. After this command completes the vendor directory specified by<path>
will contain all remote sources from dependencies specified.cargo verify-project
:parse the local manifest and check it’s validity
- Package commands
cargo init
:create a new cargo manifest in the current directory.cargo install
:This command manages Cargo’s local set of installed binary crates. Only packages which have executable[[bin]]
or[[example]]
targets can be installed, and all executables are installed into the installation root’sbin
folder.cargo new
:create a new cargo package in the given directory.cargo search
:this performs a textual search for crates on cargo repository.The matching crates will be displayed along with their descriptioin in TOML format suitable for copying into a Cargo.html manifest.cargo uninstall
:by default all binaries are removed for a crate but –bin and –example flags can be used to only remove particular binaries.
- Publishing Commands
cargo login
:This command will save the API token to disk so that commands that require authentication, such as cargo-publish(1), will be automatically authenticated. The token is saved in$CARGO_HOME/credentials.toml
.CARGO_HOME
defaults to.cargo
in your home directory.cargo owner
:This command will modify the owners for a crate on the registry. Owners of a crate can upload new versions and yank old versions. Non-team owners can also modify the set of owners, so take care!cargo package
:This command will create a distributable, compressed.crate
file with the source code of the package in the current directory. The resulting file will be stored in thetarget/package
directory.cargo publish
:This command will create a distributable, compressed.crate
file with the source code of the package in the current directory and upload it to a registry.cargo yank
:The yank command removes a previously published crate’s version from the server’s index. This command does not delete any data, and the crate will still be available for download via the registry’s download link.
- General Commands
The Rust Programming Language
Programming a Guessing Game
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main(){
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
// println!("The secret number is: {}", secret_number);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
println!("You guessed :{}", guess);
}
}Common Programming Concepts
Variables and Mutablity
- by default variables are immutable
- if want mutable, use keyword
mut
before name of viariance - Difference between variables and constants
mut
can not be used with constants- declare constants using
const
instead oflet
- constants can be declared in any scope
- constants may be set only to a constant expression
- constants naming convention:use all upercase with underscores between words
- Shadowing
- we can shadow a variable by using the same variable’s name and repeating the use of
let
keyword - shadowing allow us using the same name for different types
- we can shadow a variable by using the same variable’s name and repeating the use of
Data types
Scalar Types
Integer Types
Length Signed Unsigned 8-bit i8
u8
16-bit i16
u16
32-bit i32
u32
64-bit i64
u64
128-bit i128
u128
arch isize
usize
Range caculation: -(2^n - 1^) to 2^n - 1^ - 1
e.g. 1_000 = 1000, 57u8 = u8 type of value 57
more examples:
Number literals Example Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte ( u8
only)b'A'
Floating-Point Types
- single-percision:
f32
- double-percision:
f64
- single-percision:
Boolean type
bool
Character Type
- size:4 bytes
- Unicode
char
Compound Types
The Tuple Type
group together a number of values with a variety of types into one compound type
fix length
e.g.
1
2
3fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}visited by index
The Array Type
every element of an array muse have the same type
fix length
e.g
1
2
3
4
5fn main() {
let a = [1, 2, 3, 4, 5];
let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [3; 5]// size 5 with initial value 3
}visited by index
Functions
- Coding stype: snake case. All letters are lowercase and underscores separate words
- start with
fn
- Function Parameters
- type of each parameter must be declared
- multiple parameters separated with commas
- Function bodies contain statements and expressions
- Expressions do not include ending semicolons, if have, expression turns to statement,which will not return a value
- Functions with return values
- Declare return vlaue types after a
->
- the return value of the function is synonymous with the value of the final expression in the block of the body of a function.
- Declare return vlaue types after a
Comments
//
Control Flow
If Expressions(if-else if-else)
Loop(loop)
1
2
3
4
5fn main() {
loop {
println!("again!");
}
}break & continue can use lable to apply to the labled loop
you can add the calue you want returned after the
break
expressionconditional loop with
while
For loop:
for element in collection
Understanding Ownership
What is ownership?
- Memory is managed through a system of ownership with a set of rules that the compiler checks at compile time.
- Stack and Heap
- stack for known, fixed size data at compile time
- heap is less organized, freee space must be serached
- hense stack is more efficiency
- when code calls a function ,value passed into function and variables get pushed onto the stack , when the function is over, those values get poped off the stack
- heap is controlled by ownership
- Ownership rules
- Each value in Rust has variable that’s called its owner
- There can only be one owner at a time
- When the owner goes out of scope,the value will be dropped
- Variable Scope
- When varianle comes into scope, it is valid
- it remains valid until it goes out of scope
- The String type
- string is immutable but String not
- Memory and Allocation
- Ways Variables and Data interact:Move
- primitive types allocated on stack
- Reference allocated on stack
- Data allocated on heap
- For ptimitive types: s1 = s2 means copy on stack
- For non-primitive types: s1 = s2 means referece copied on stack and data on heap did not do anything
- Ways Varianles and Data interact:Clone
- if we do want to deeply copy the heap data of the String, not just the stack data, we can use a common method called
clone
- if we do want to deeply copy the heap data of the String, not just the stack data, we can use a common method called
- Stack-Only data:copy
- Types such as integers that have a known size at compile time are strored entirely on the stack ,so copies of the actual values are quick to make.
- if a type implements the
copy
trait, an doler variable is still usable after assignment.
- Ownership and functions
- The semantics for passing a value to a function are similar to those for assigning a value to a variable.Passing a variable to a funtion will move or Copy.For ptimitive types, after funciton calling, variable is still valid, but for other(e.g. String) types, calling function means ownership moving ,which meams s will be invalid after function call.
- Return values and scope
- returning values can also transfer ownership
- The ownership of a variable follows the same pattern every time: assigning a vlaue to another variable moves it.When a variable that includes data on the heap goes out of scope,the value will be cleaned by
drop
unless the data has been moved to be owned by another variable.
References and Borrowing
reference allow you to refer to some value without taking ownership of it.
The
&s1
syntax lets us create a reference that refers to the value ofs1
but does not own it. Because it does not own it, the value it points to will not be dropped when the reference stops being used.When functions have rederences as parameters intead of the actual values, we won’t need to return the values in order to give back ownership, because we never had ownership.We call the action of creating a reference borrowing.
Modifying something borrowed is not allowed, just as variables are immutable by default, so are references.
Mutable references, e.g.
1
2
3
4
5
6
7
8
9fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}Mutable references have one big restiction:you can have only one mutable reference to a particular piece of data at a time.The restriction preventing multiple mutable references to the same data at the same time allows for mutation but in a very controlled fashion.Rust prevents data races even in compile time.Samelly, combining mutable and immutable references is also no permitted.(considered it is same as RW lock mechanism).Note that a reference’s scope starts from where it is introduced and continues through the last time that references is used.
Dangling References
In languages with pointers, it’s easy to erroneously create a dangling pointer, a pointer that references a location in memory that may have been given to someone else, by freeing some memory while preserving a pointer to that memory. In Rust, by contrast, the compiler guarantees that references will never be dangling references: if you have a reference to some data, the compiler will ensure that the data will not go out of scope before the reference to the data does.
The rules of References
- At any given time, you can have either one mutable references or any number of immutable references.
- Referebces must always be valid.
The Slice Type
- A String Slice is a reference to part of a String.
- We can create slices using a range within brackets by specifying
[starting_index..ending_index]
, wherestarting_index
is the first position in the slice andending_index
is one more than the last position in the slice. - if starting_index omitted, which means start from zero, if ending_index omitted, which means end to last byte, if all ommited, which means reference the total string
- The type that signifies “string slice” is written as
&str
- The compiler will ensure the references into the String remain valid.
- String literals are slices:
let s = "Hello, World"
, the type ofs
here is&s
, it is a slice poting to that specified piont of the binary, This is also why string literals are immutable,&str
is an immutable reference.
Using structs to structure related data
Defining an instantiating structs
- Structs are similar to tuples, but unlike with tuple, you will name each piece of data so it is clear what the values mean.Structs are more flexible than tuples:you don’t have to rely on the order of the data so specify or access the value of an instance.
- kv pairs visited(read or write) by dot
.
Creating instances from other instances with struct update syntax
Using struct update syntax, we can achieve the same effect with less code, as shown in Listing 5-7. The syntax
..
specifies that the remaining fields not explicitly set should have the same value as the fields in the given instance.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
Using Tuple Structs Without Named Fields to Create Different Types
1
struct Color(i32, i32, i32);
Unit-Like Structs Without Any Fields
1
2
3
4
5fn main() {
struct AlwaysEqual;
let subject = AlwaysEqual;
}Method Syntax
Method are similar to functions, but methods are different from functions in that they’re defined within the context of a struct and their first parameter is always
self
, which represents the instance of the stuct the method is being called on.Where is the
->
Operator?- Rust has a featured called automatic referencing and dereferencing.
Associated Functions
All functions defined within an
impl
block are called associaterd functions because they’re associated with the type name after theimpl
Associated functions that aren’t methods are often used for constructors that will return a new instance of the struct.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3);
}
Multiple imlp Blocks
- It is valid to seperate methods into multiple impl blocks but not recomended.
Enums and Pattern Matching
Defining an Enum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}Enum Values
1
2
3
4
5
6
7
8enum IpAddrKind{
v4(String),
v6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));Any type can be values for Enums.
Enum Methods
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17fn main() {
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// method body would be defined here
}
}
let m = Message::Write(String::from("hello"));
m.call();
}The
Option
Enum and it’s advantages over Null values- When we have
Some
value,we know that a value is present and the value is held within the Some - When we have a
None
value,in some sense, it means the same thing as null - In other words, you have to convert an
Option<T>
to aT
before you can performT
operations with it.Generally, this helps catch one of the most common issues with null: assuming that something isn’t null when it actually is. match
expression is a control flow construct that does just this when used with enums.
- When we have
The match control flow operator
1
2
3
4
5
6
7
8
9
10
11fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}Matches Are Exhaustive
- Matches in Rust are exhaustive, we must exhaust every last possibility in order for the code to be valid.Especially inthe case of
Option<T>
, when Rust prevent us from forgetting to explicitly handle None case.
- Matches in Rust are exhaustive, we must exhaust every last possibility in order for the code to be valid.Especially inthe case of
Catch-all Patterns and the _ Placeholder
for match default arm, we can use
other
and_
to handle this.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
}
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
}
Concise Control Flow with if let
The
if let
syntax lets you combine if and let into a less verbose way to handle values that match one pattern while ignoring the rest.In other words, you can think of
if let
as syntax sugar for amatch
that runs code when the value matches one pattern and then ignores all other values.1
2
3
4
5
6
7
8
9
10
11
12
13
14fn main() {
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
}
fn main() {
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {
let coin = Coin::Penny;
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
_ => count += 1,
}
}
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {
let coin = Coin::Penny;
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}
}
Managing Growing projects with packages,Crates,and Modules
(haven’t read)
Common Collections
Types:
- A vector allows you to store a variable number of values next to each other
- A string is a collection of characters
- A hash map allows you to associate a value with a particular key
Storing Lists of Values with Vectors
Creating a new vector
let v: Vec<i32> = Vec::new()
Simply
let v = vec![1,2,3]
Update a vector
v.push(1)
Drop a vector Drops its elements
1
2
3
4
5
6
7fn main() {
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
}Read elements of vectors
with index syntax or the
get
method1
2
3
4
5
6
7
8
9
10
11fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {}", third);
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
}Iterating over the values in a vector
1
2
3
4
5
6
7
8
9
10
11
12fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
}
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}Using am Emum to store multiple Types
1
2
3
4
5
6
7
8
9
10
11
12
13fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
Storing UTF-8 Encoded Text with Strings
Create a new String
let data = "initial contents".to_string()
let s = String::from("initial contents")
Update a String
s.push_str("bar")
s.push('l')
1
2
3
4let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
// + use fn add(self, s: &str) -> String {use
format!
Macro is recomended1
2
3
4
5let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);format!
macro works in the same way asptintln!
but instead of printing the output to the screen,it returns aString
with the contents. The code generated by theformat!
macro uses references so that this call doesn’t take ownership of any of its parameters.Index into Strings
- index to a String is not valid.
Slice Strings
s[start..end]
Methods for Iterating Over Strings
for c in "abcde".chars()
for b in "abcde".bytes()
Storing keys with associated values in hash maps
Creating a hash map
1
2
3
4use std::collections::HashMap;
let mut scores = HashMap::new();
scores.inert(String::from("Blue"), 10);
scores.indert(String::from("Yellow"),50);Another way with two vec
1
2
3
4use std::collections::HashMap;
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let mut scores: HashMap<_, _> = teams.into_iter().zip(initial_scores.into_iter()).collect();HashMaps and ownership
- For types that implement the
copy
trait, the values are copied into the hashmap.For owned values likeString
, the values will be moved and the hashmao will be the owner of those values.
- For types that implement the
Accessing values in a hashmap
let team_name = String::from("Blue")
let score = scores.get(&team_name)
1
2
3
4
5
6use std:collections::Hashmap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
for(key, value) int &scores {
println!("{}:{}",key, value);
}Updating a hashmap
overwitring a value
1
2scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 20);Only inserting a value if the key has no value
1
scores.entry(String::from("Yellow")).or_insert(50);
The
or_insert
method onEntry
is defined to return a mutable reference to the value for the correspondingEntry
key if that key exists, and if not ,inserts the parameter as the new value for this key and returns a mutable reference to the new value.Updating a value based on the old value
1
2
3
4
5
6let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text,split_whitespcae() {
let count = map.entry(word).or_intsert(0);
*count += 1;
}The
or_insert
method actually returns a mutable reference(&mut v
) to the value for this key.
Error Handling
Unrecoverable Erros with panic!
Recoverable Errors with Result
Resuit Enum
1
2
3
4
5
6
7
8use std::fs::File;
fn main(){
let f = File::open("hello.txt");
let f = match f {
OK(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}Mathcing on different errors
1
2
3
4
5
6
7
8
9
10
11fn main() {
let f = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}Shortcuts for panic on error:unwrap and expect
unwrap
is a shortcut method that is implemented just like thematch
expression.if theResult
value isOk
variant,unwrap
will return the value inside theOk
.If theResult
is theErr
variant,unwrap
will call thepanic!
macro for us.let f = File::open("hello.txt").unwrap()
We use
expect
in the same way asunwrap
: to return the file handle or call thepanic!
macro. The error message used byexpect
in its call topanic!
will be the parameter that we pass toexpect
, rather than the defaultpanic!
message thatunwrap
uses.let f = File::open("hello.txt").expect("Failed to open")
Propagating Errors
A shortcut for prapagating errors:
?
operator1
2
3
4
5
6fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}If the value of the
Result
is anOK
,tge value insideOk
will get returned from this expression, and the program will continue.if the value is anErr
, theErr
will be returned from the whole function as if we had used return keyword so the error value gets propagated to the calling code.The
?
operator can be used in functions that have type ofResult
.
To panic! Or Not To panic!
So how do you decide when you should call
panic!
and when you should returnResult
? When code panics, there’s no way to recover. You could callpanic!
for any error situation, whether there’s a possible way to recover or not, but then you’re making the decision on behalf of the code calling your code that a situation is unrecoverable. When you choose to return aResult
value, you give the calling code options rather than making the decision for it. The calling code could choose to attempt to recover in a way that’s appropriate for its situation, or it could decide that anErr
value in this case is unrecoverable, so it can callpanic!
and turn your recoverable error into an unrecoverable one. Therefore, returningResult
is a good default choice when you’re defining a function that might fail.
Generic Types, Traits, and LifeTImes
Generic Data Types
In Function Definitions
Before we use a para,eter in the body of the function, we have to declare the parameter name in the signature so the compiller knows what that name means.
fn largest<T>(list: &[T]) -> T {...}
In Struct Definitions
1
2
3
4struct Point<T> {
X: T,
y: T,
}1
2
3
4struct Point<T, U> {
x: T,
y: U,
}In Enum Definitions
1
2
3
4enum Option<T> {
Some(T),
None,
}1
2
3
4enum Result<T, E> {
Ok(T),
Err(E),
}In Method Definitions
1
2
3
4
5
6
7
8
9struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
Traits:Definning Shared Behavior
A trait tells the Rust compiler about functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way. We can use trait bounds to specify that a generic type can be any type that has certain behavior.
Defining a trait
1
2
3pub trait Summary {
fn summarize(&self) -> String;
}Imlementing a trait on a type
1
2
3
4
5
6
7
8
9
10
11
12pub struct NewsArticle
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
......
}
}Default Implementations
traits can have default implementations
Traits as parameters
We have implemented the
Summary
trait on theNewsArticle
andTweet
types.We can define anotify
function that calls the summarize method on itsitem
parameter,which is of some type that implements theSummary
trait.1
2
3pub fn notify(item : &impl Summary) {
println!("Breaking news! {}", item.summarize());
}Trait Bound Syntax
Code in item6 is straightforward but it is actually a syntax sugar for a longer form called trait bound.
1
2
3pub fn notify<T : Summary>(item : &T) {
println!("Breaking news! {}", item.summarize());
}Specifying Multiple Trait Bounds with + Syntax
1
2
3pub fn notify(item :&(impl Summary + Display)) {
.....
}1
2
3pub fn notify<T : Summary + Display> (item: &T) {
......
}Clearer Trait bounds with where clauses
1
2
3
4fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{Returning types that implement traits
1
fn returns_summarizable() -> impl Summary {...}
Attention, you can only use
impl Trait
if you‘re returning a sigle type.Using Trait Bounds to Conditionally Implement Methods
We can also conditinally implement a trait for any type that implements another trait.Implementations of a trait on any type that satisfies the trait bounds are called blanket implementations.
Validating references with lifetimes
Preventing dangling references with lifetimes
The Borrow checker
Generic Lifetimes in functions
Lifetime Annotation Syntax
Lifetime Annotations in function signatures
1
2
3
4
5
6
7for longest<’a>(x: &'a str,y: &'a str) -> &'a str{
if x,len() > y.len() {
x
} else {
y
}
}The function signature now tells Rust that for some lifetime
'a
, the function takes two parameters, both of which are string slices that live at least as long as lifetime'a
. The function signature also tells Rust that the string slice returned from the function will live at least as long as lifetime'a
. In practice, it means that the lifetime of the reference returned by thelongest
function is the same as the smaller of the lifetimes of the references passed in. These constraints are what we want Rust to enforce. Remember, when we specify the lifetime parameters in this function signature, we’re not changing the lifetimes of any values passed in or returned. Rather, we’re specifying that the borrow checker should reject any values that don’t adhere to these constraints. Note that thelongest
function doesn’t need to know exactly how longx
andy
will live, only that some scope can be substituted for'a
that will satisfy this signature.Thinking in terms of lifetimes
Lifetime annotations in struct dedinitions
We have only defined structs to hold owned types.It is possible for structs to hold references, but in that case we would need to add a lifetime annotation on every reference in the struct’s definition.
1
2
3
4
5
6
7
8
9
10
11struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}This annotation means an instance of ImportantExcerpt can not outlive the reference it holds in its part field.
Lifetime Elision
Lifetimes on function or method parameters are called input lifetimes, and lifetimes on return values are called output lifetimes.
Three rules for lifetime:
- Each parameter that is a reference gets its own lifetime parameter
- If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
- There are multiple input lifetime parameters, but one of them is
&selft
or&mut self
because this is a method, the lifetime ofself
is assigned to all output lifetime parameters.
Lifetime Annotations in Method Definitions
Lifetime names for struct fields always need to be declared after the
impl
keyword and then used after the struct’s name, because those lifetimes are part of the struct’s type1
2
3
4
5impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}The static lifetime
Static reference can live for the entire duration of the program
let s: &'static str = "I have a static lifetime"'
Generic Type parameters, Trait Bounds, and lifetimes Together
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest_with_an_announcement(
string1.as_str(),
string2,
"Today is someone's birthday!",
);
println!("The longest string is {}", result);
}
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
Writing Automated Tests
How to Write Tests?
Checking resutls with the
assert!
MacroWe give the
!assert
Marco an argument that avaluates to a Boolean,if the value is true, assert! does nothing an the test passes,if the value is false, the assert! Macro calls the panic! macro.Testing equality with the
assert_eq!
Andassert_ne!
marcosAdding custom failure messages
Checking for panics with
should_panic
To our test function, this attribute makes a test pass if the code inside the function panics, thet test will fial if the code inside the function doesn’t panic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess { value }
}
}
mod tests {
use super::*;
fn greater_than_100() {
Guess::new(200);
}
}
Test orgnization
The
#[cfg(test)]
annotation on the tests module tells Rust to compile and run the test code only when you runcargo test
, not when you runcargo build
.
Function Language features:iterators and closures
Closures:aninymous functions that can capture their environment
To define a closure, we start with a pair of vertical pipes
|
,after the parameters,we place curly brackets that hold the body of the closure,which is optinal if the closure body is a sigle expression.Closures don’t require you to annotate the types of the parameters or the return value like functions do.The compiler is reliably able to infer the types if the paramaters and the return value, similar to how it’s able to infer types of most variables.
1
2
3
4fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;Once user calling a closure, compiler refers the type of closure paramaters, and types are then locked into the closure,and we get a type error if we try to use a difference type with the same closure.
Cloures have an additional capability that functions don’t have: they can capture their environment and access variables from the scope in which they’re defined.
1
2
3
4
5
6
7
8
9
10
11fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool {
z == x
}
let y = 4;
assert!(equal_to_x(y));
}Closures can capture value from their environment in three ways,which directly map to the three ways a function can take a parameter:taking ownership, borrowing mutably, and borrowing immutably.These are encoded in three
Fn
traits as follows:FnOnce
:consumes the variables it captures from its enclosing scope, known as the closure’s environment.To consume the captured variables, the closure must take ownership of these variables and move them into the closure when it is defined.The once part of the name represents the fact that the closure can’t take ownership of the same variables more than once, so it can be called only once.FnMut
:can change the environment because it mutably borrows values.Fn
:borrows values from the environment immutably
When you create a closure, Rust infers which trait to use based on how the closure uses the values from the environment. All closures implement
FnOnce
because they can all be called at least once. Closures that don’t move the captured variables also implementFnMut
, and closures that don’t need mutable access to the captured variables also implementFn
. In Listing 13-12, theequal_to_x
closure borrowsx
immutably (soequal_to_x
has theFn
trait) because the body of the closure only needs to read the value inx
.If you want to force the closure to take ownership of the values it uses in the environment, you can use the
move
keyword before the parameter list. This technique is mostly useful when passing a closure to a new thread to move the data so it’s owned by the new thread.1
2
3
4
5
6
7
8
9
10
11
12fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x;
println!("can't use x here: {:?}", x);
// move this line, the code can be compiled
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}
Processing a series of items with iterators
Iterators are lazy
The Iterator trait an next method
1
2
3
4
5
6
7pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}The valuies we get from the calls to
next
are immutable references to the values in the vector.Theiter
method produces an iterator over immutable references.If we want to create an iterator that takes ownership of vector and returns owned values, we can callinto_iter
instead ofiter
.Similarly, if we want to iterate over mutable references, we can calliter_mut
instead ofiter
.Methods that consume the iterator
1
2
3
4
5fn iterator_sum() {
let v1 = vec![1,2,3];
let v1_iter = v1.iter();
let total:i32 = v1.iter.sum();
}Methods that produce other iterators
1
2let v1: Vec<i32> = vec![1,2,3]
let v2: Vec<_> = v1.iter().map(|x| x+1).collect();We use collect method to consume the iterator because iterators are lazy, we collect the results of iterating over the iterator that’s returned from the call to
map
into a vector.Using closures that capture their environment
filter
iterator adaptor:Thefilter
method on an iterator takes a closure that takes each item from the iterator and returns a boolean.If the closure returns true, the value will be included in the iterator produced by filter, if the closure returns false, the value won’t be included in the resulting iterator.shoes.into_iter().filter(|s| s.size == shoe_size).collect()
Comparing performance:Loops and iterators
Closures and iterators are Rust features inspired by functional programming language ideas. They contribute to Rust’s capability to clearly express high-level ideas at low-level performance. The implementations of closures and iterators are such that runtime performance is not affected. This is part of Rust’s goal to strive to provide zero-cost abstractions.
More about Cargo and Crates.io (Not covered)
Smart Pointers
In Rust, which uses the concept of ownership and borrowing, an additional difference between references and smart pointers is that the reference are pointers that only borrow data; in contrast, in many cases, smart pointers own the data they point to.
Using Box
to point to data on the heap Boxes allow you to store data on the heap rather than the stack, what remains on the stack is the pointer to the heap data.
- Enabling recursive types with boxes
Treating smart pointers like regular references with the Deref trait
Implementing the
Deref
trait allows you to customize the behavior of the dereference operator,*
(as opposed to the multiplication or glob operator). By implementingDeref
in such a way that a smart pointer can be treated like a regular reference, you can write code that operates on references and use that code with smart pointers too.Without the
Deref
trait, the compiler can only dereference&
references.Thederef
method gives the compiller the ability to take value of any type that implementsDeref
and call thederef
method to get a&
reference that it knows how to dereference.Implicit deref coercions with functions and methods
Deref coercion is a convenience that Rust performs on arguments to functions and methods. Deref coercion works only on types that implement the
Deref
trait. Deref coercion converts such a type into a reference to another type. For example, deref coercion can convert&String
to&str
becauseString
implements theDeref
trait such that it returns&str
. Deref coercion happens automatically when we pass a reference to a particular type’s value as an argument to a function or method that doesn’t match the parameter type in the function or method definition. A sequence of calls to thederef
method converts the type we provided into the type the parameter needs.Deref coercion was added to Rust so that programmers writing function and method calls don’t need to add as many explicit references and dereferences with
&
and*
. The deref coercion feature also lets us write more code that can work for either references or smart pointers.1
2
3fn hello(name: &str) {
println!("Hello, {}!", name);
}1
2
3
4fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}with rust deref coercions, the code is equal to
1
2
3
4fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}How deref coercion interacts with mutability
rust does deref coercion when it finds types and trait implementions in three cases:
- From
&T
to&U
whenT: Deref<Target=U>
- From
&mut T
to&mut U
whenT: DerefMut<Target=U>
- From
&mut T
to&U
whenT: Deref<Target=U>
- From
Running code on cleanup with the drop trait
The second trait important to the smart pointer pattern is
Drop
, which lets you customize what happens when a value is about to go out of scope. You can provide an implementation for theDrop
trait on any type, and the code you specify can be used to release resources like files or network connections. We’re introducingDrop
in the context of smart pointers because the functionality of theDrop
trait is almost always used when implementing a smart pointer. For example, when aBox<T>
is dropped it will deallocate the space on the heap that the box points to.Programmer do not have to free memory in code when finishing using an instance of a data type.In rust, when a value goes out of scope,the compiler will insert ‘free memory’ code automatically.
Most of time, programmers do not have to drop memory of one instance before the scope ending, if have to, rust provides a method
std::mem::drop
to do this.Rc
, the reference counted smart pointer To enable multiple ownership, Rust has a type called
Rc<T>
, which is an abbreviation for reference counting. TheRc<T>
type keeps track of the number of references to a value to determine whether or not the value is still in use. If there are zero references to a value, the value can be cleaned up without any references becoming invalid.We use the
Rc<T>
type when we want to allocate some data on the heap for multiple parts of our program to read and we can’t determine at compile time which part will finish using the data last. If we knew which part would finish last, we could just make that part the data’s owner, and the normal ownership rules enforced at compile time would take effect.1
2
3
4
5
6
7
8
9
10
11
12
13enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}RefCell
and the interior mutability pattern ……..(TODO)
reference:https://doc.rust-lang.org/book/ch15-05-interior-mutability.html