char3: go中常见的复杂数据结构
Array
属性
- 定长, 超出报错
- 类型统一
- for range循环
- 初始化, 数组每个元素值为所选类型的零值
1
2var x [5]int // 长度为5的数组
var y = [5]int{1, 2, 3, 4, 5} // 有初始值的数组
切片
- 容量和长度的概念
1
2len()
cap() - 初始化使用make初始化数组
1
2
3arr :=[...]string{"a", "b"}
s1 := arr[0: 1]
s2 := arr[1]1
2// 数组类型, 数组长度, 数组容量
sli = make([]int, 10, 20) - 可扩展
- append: 数组尾部插入数组
1
2sli = make([]int, 0, 3)
sli = append(sli, 100)
- 切片也为引用类型
Hash 我觉得有点像js的对象
- 展示类型为键值对
- key唯一
- 查找速度快
- 在go中体现为map数据结构
map
map是go中的hash结构
- 使用make创建一个map
1
2
3
4
5
6
7// 1
var idMap map[string]int
// 2
idMap = make(map([string]int))
// 3
idMap := map[string]int {
"warning" : 1} - 修改和新增: 和js对象一样
- 删除一个key-value——-delete(map, key)
- 判定一个key是否在对象中: 使用双赋值map
1
2// p是一个布尔值, true证明joe在idMap中,反之, 则不在
id, p := idMap["joe"] - len()获取map的key数量
- 遍历使用for-range
Structs
- 类似class但是并不是class, 有自己的属性, 自己的方法, 也可以根据这个结构体构造实例
- 定义:
1
2
3
4
5
6
7
8
9
10type struct Person {
name string
addr string
phone string
}
var p1 Person
// 访问变量
p1.name = "joe"
x = p1.addr - 初始化一个结构体
1
2
3
4// 1. use New keyword
p1 := new(Person)
// 2. 使用构造函数
p2 := Person(name: "joe", addr: "s", phone: "yooooh")
charper4: RPC JSON file_io
- go提供了rpc协议的一些基础包
- net/http : web communication protocol
- net : TCP/IP and socket programming
json: json比struct更为标准化和通用化
1
2// go struct
p1 := Person(name: "joe", addr: "a st.")json的特性
- 支持所有编码方式
- 有好的可读性
- 可以混如不同的类型
- 使用json.marshalling来对struct与json进行格式转换
- json.marshal额, 就是反过来
- file
- 访问方式为线性访问, 而不是随机访问
- 占用线程
- 分片操作
基本操作步骤:
- 打开文件
- 读取byte
- 写入byte
- 关闭文件(解锁)
- seek
文件io涉及的包: ioutil
1 | // 文件读取操作 |
- io
- os.Open() 打开一个文件, 返回一个文件的描述
- os.Close() 关闭一个文件
- os.Read() 读取一个文件, 以字节流的形式, 可以控制流的大小
- os.Write() 写一个文件, 以字节流的形式, 可以控制流的大小
1
2
3
4
5
6
7
8
9
10// opening and reading
f, err := os.Open("test.txt")
barr := make([]byte, 10)
nb, err := f.Read(barr)
f.Close()
// creating and writing
f, err := os.Create("output.txt")
barr := []byte{1, 2, 3, 4}
nb, err := f.Write(barr)
nb, err := f.WriteString("Hi")
function: 最主要的是函数名和注释吧, 给足够的信息让后来人尽可能的读懂
定义: func
1
2
3
4func add(a, b int) int {
result := a + b
return result
}在go中, 传入的参数都会被复制一份, 与原数据独立开来。
- 但是我们需要的话, 我们可以通过指针类型(*, &)传入, 然后对指针地址内的值进行操作, 这样会触发函数内部修改外部函数的效应.
1 | func foo(y *int) { |
- 入参为数组或者slice,
1
2
3
4
5
6
7
8
9// 数组
func foo(x [3]int) int {
return x[0]
}
func main() {
a := [3]int{1, 2, 3}
fmt.Print(foo(a))
}1
2
3
4
5
6
7
8
9func foo(sli []int) {
sli[0] = sli[0] + 1
}
func main() {
a := []int{1, 2, 3}
foo(a)
fmt.Print(a)
}
复杂度:
- 超大函数分解: 要求语义化和可读性
- 控制流复杂度: 减少嵌套的控制流, 切分函数, 函数分层可以让你的控制流复杂度降低
写好函数
- 可读性
函数组织方式
- 函数名要有意义
- 单一职责
- 更少的参数
函数类型
first-class funciton
- 函数在go中其实也是一种类型
- 函数可以赋值给变量, 这样可以以变量名调用函数
- 可以当作变量在函数中充当入参和出参
go中, 也存在类似js的变量作用域, 叫词法作用域
闭包
- function + its environment
- 当函数完成后, 还可以访问他的environment的能力(数据持久化)
1
2
3
4
5
6
7
8
9
10// 函数赋值
var funcVar func(int) int
func incFn(x int) int{
return x + 1
}
func main() {
funcVar = incFn
fmt.Print(funcVar(1))
}1
2
3
4// 函数作为参数
func applyIt (afunc func (int) int, val int) int{
return afunc(val)
}1
2
3
4
5
6
7
8
9
10// 匿名函数
func applyIt (afunc func (int) int, val int) int{
return afunc(val)
}
func main() {
v := applyIt(func (x int) int {
return x + 1
}, 2)
fmt.Print(v)
}
1 | // 返回一个函数 |
可变函数参数
使用…收集不确定数量的函数参数, 然后把这个参数当作slice处理
1
2
3
4
5
6
7
8
9
10func getMax(vals ...int) int {
maxV := -1
for _, v := range vals {
if v > maxV {
maxV = v
}
}
return maxV
}使用…展开slice
1
2
3
4
5func main() {
fmt.PrintIn(getMax(1, 2, 3, 4, 5))
vslice := []int{1, 2, 3, 4, 5}
fmt.PrintIn(getMax(vslice...))
}
异步函数-defer
- 普通模式, 在同步代码调用后, 执行def异步代码
1
2
3
4func testDef() {
defer fmt.Print("Defer")
fmt.Print("Hello")
} - 参数判定
在go中的def后面执行的代码段相当于一个匿名函数.所以,对于原语类型, 则为其分配一个新的内存控件,对于引用类型, 则通过引用的方式使用.1
2
3
4
5
6
7
8
9
10
11// 原语类型
func main() {
i := 1
defer fmt.Println(i + 1) // 2
i = i + 3 // 4
fmt.Println(i)
fmt.Println("Hello")
}
// 4
// Hello
// 2
1 | // 引用类型 |
Class and encapsulation
- class的基本概念
- 模板和实例的关系
- 私有变量的概念
- 链接变量和func
1
2
3
4
5
6
7
8
9
10type MyInt int
func (mi MyInt) Double () int {
return int(mi * 2)
}
func main() {
v := MyInt(3)
// 注意, 这里触发了隐式传值
fmt.Println(v.Double())
} - 自定义Struct
1
2
3
4
5
6
7
8
9
10
11
12
13
14type Point struct {
x float64
y float64
}
func (p Point) DistToOrig () {
t := math.Pow(p.x, 2) + math.Pow(p.y, 2)
return math.Sqrt(x)
}
func main() {
p1 := Point(3, 4)
fmt.Println(p1.DisToOrig())
}
封装
- 封装公共方法/也可以说是go中的模块化机制(外部可调用方法, 方法内部存私有变量)
(类似js中的export/import)
如果方法或者变量使用大写, 那么会被go自动导出.
1 | // data.go |
1 | package main |
- 把变化封装进struct中
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// data.go file
package data
type Point struct {
x float64
y float64
}
func (p *Point) InitMe(xn, yn float64) {
p.x = xn
p.y = yn
}
func (p *Point) Scale(v float64) {
p.x = p.x * v
p.y = p.y * v
}
func (p *Point) PrintMe() {
fmt.Println(p.x, p.y)
}
// and we can use it
// go.go file
func main() {
var p data.Point
p.Init(3, 4)
p.Scale(2)
p.PrintMe()
}
通过这样的方式, 模拟了class的思想,并且把数据隐藏了起来, 只能通过方法访问和修改.
记住, 在你往struct上挂方法的时候, 一定要使用*StructName。因为我们需要的是struct本身, 而不是他的copy。如果不传入指针, 那么对其的修改实际上是修改copy而不是struct.
class抽象相关概念
多态
一个函数在不同的上下文中, 起不同的作用。例如,计算面积的函数1
2
3
4
5func Area() float64
// Rect
area = base * height
// Triangle
area = 0.5 * base * height继承
go没有继承, 取而代之是组合.重写
对父类定义的方法重写.
接口类型
定义
- 接口是一系列方法的集合
- 包含接口名称,入参, 返回值
- 接口是实现而不是接口定义
满足一个接口的条件是
- 包含接口类型所定义的所有接口, 并且入参和出餐符合接口定义
- 除此之外, 可以有别的方法(接口)
go通过接口类型来完成多态.
举个例子
1 | type Shape2D interface { |
接口类型和组合比较
复杂Struct定义了data和方法.
接口只对我们需要的东西暴露出来.
举个例子: 接口类型的定义和使用
1 | package main |
使用*调用接口(此时还未实例化接口)
就是我以struct的指针赋予接口实例, 然后使用接口实例调用对应接口.此时, 在接口中,我们值为nil(零值)
1 | func (d *Dog) Speak() { |
使用接口
- 函数可变入参类型
举个栗子:我有个函数fn, 这个函数可以传入x类型也可以传入Y类型
- 定义一个接口类型z
- 该接口类型满足fn(x)
- 该接口类型满足fn(y)
- 实例化并挂载fn1 在fn1中去判定入参类型然后做什么事
- 扩展方法
1
2
3
4
5
6
7// 明确一下, 接口类型也是类型, 我们可以通过这个方式往接口扩展原有方法
func FitInYard(s Shape2D) bool {
if(s.Area() > 100 && s.Perimeter() > 100) {
return true
}
return false
}
空接口
- 空接口类型不包含任何方法
- 空的接口类型满足任何Struct
- 使用空接口扩展的方法可以接受任意类型的参数
举个栗子1
2
3func PrintMe(val interface{}) { // 这样可以让这个函数接受任何类型的参数
fmt.Println(val)
}
类型断言
在上面, 我们提出了, 如果我们使用空接口作为类型, 那么,我们可以让类型系统失效.可是我们想控制这个失效的范围, 那么就出现了类型断言.
1 | func DrawShape(s Shape2D) bool { |
1 | // 使用switch优化类型断言 |
错误处理
go程序中,如果出现错误时, 会选择返回error接口类型来指示错误.
如果没有错误则返回nil指示一切正常.
1 | type error interface{ |
常见错误处理
1 | func readFile() { |