go语言基础学习13章节详细总结欢迎一起入门学习
golang学习笔记
- 开篇: 初次学习,理解尚浅。暂且做些笔记,日后慢慢充实后,再加以更正或添加~
- 全部已上传至gitee平台,观看效果更佳,欢迎点赞:https://gitee.com/huangdonglin/go-learning-notes/blob/master/go%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/golang%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0(7).md
第一章:启蒙与环境安装
安装链接(非官网):https://studygolang.com/dl
终端输入:
go env
, 查看镜像资源更改一个软件下载源:
- windows:
go env -w GOPROXY=https://goproxy.cn,direct
- linux:
export GOPROXY=https://goproxy.cn
- windows:
设置
GO111MODULE
为打开:go env -w GO111MODULE=on
- GOPATH: 后序后讲到, 从前的包管理工具
- GOMODULE: 现在的包管理工具
安装工具:
go get -g -v golang.org/x/tools/cmd/goimports
安装 go语言编辑器 : GoLand
- https://www.jetbrains.com/go/download/other.html
- 下载 2020.1.1版本
- B站的视频:Goland安装与配置
点击settings进行配置相关的环境。
- GOROOT: 点击会自动出现你所安装的go,选择即可(注意!, GoLand应该是不识别中文的, 之前默认安装在C盘的,我估计是这个原因,打死找不到 go安装位置。。,之后又再d盘纯英文路径安装一个go,才显示~)
- GPPATH:后续会讲到, 对目录结构有要求。 (随便配置,比方建立一个文件夹叫gopath, 里面再建一个文件夹叫做 src即可)
- GoModules: 配置 这个是主流的
GO111MODULE
, 目前的go依赖管理。选择对应的Vgo,然后 Environment填: https://goproxy.cn,direct 更换下载源。
创建项目:
设置 proxy: Settings-> Go -> Go Modules (vgo),勾选 Enable Go Modules (vgo) integration 以启用 Go Modules,并在 envirment 输入框中输入 :
https://goproxy.cn,direct
- 设置Settings-> Go -> GOPATH,勾选上 Index entire GOPATH 以索引整个 GOPATH。 还可以单独为该项目建立一个 gopath。(使用go Modules 就不需要配置这个了我觉得~)
- Go, Go Modules, Dep, App Engine : 选择 GO Modules
- Location: 设置项目目录。
- Go, Go Modules, Dep, App Engine : 选择 GO Modules
小问题
// fmt.Println(a...: 123) 这个 a... 其实是 Println 参数的名字// 是goland显示的。 可以在设置里关掉.// 1. 在设置中搜索 parameter hint(参数提示), 2020.1.1 版本中是在 Inlay Hints 下的 Go,看到 Show parameter hint 把对钩点掉,然后应用就可以了。
使用vscode, 需要安装一堆插件(自动提示安装)
- 命令行 go run +文件名执行
- go mod init +模块名字 生成 mod 文件
- (另外) go build 编译文件
第二章:基本语法
1. 定义变量的方法:
package main// 函数外也可以定义 变量,但只能使用 var定义
// 另外这是属于 包内变量(作用域是包内部), 没有全局变量的说法
var aa = 33
// 还可以这种写法
var (bb int = 1cc string = "123"dd float32 = 2.01
)func variable() {var a, d int = 1, 2// var a, d = 1, 2 还可以根据后面赋值的类型赋值,不必声明,编译器可推测变量类型// var a, b, c , s = 3, 4, true, "def"// p := 10 还可以代替 var 关键字var b intvar s string = "123"fmt.Println(a, b, s)fmt.Printf("%d %q\n", a, s) // 格式打印, 且定义的变量必须使用!fmt.Println(aa)
}
2. 内建变量类型
- bool, string
- (u)int, (u)int8, (u)int16, (u)int32, (u)int64, uinputer
- byte, rune(字符型,应对多国应用,长度32位,4字节,unicode)
- float32, float64, complex64, complex128(虚数,实部虚部各128位)
- 类型是强制转换的
var a, b = int = 3, 4
var c int = math.Sqrt(a * a + b * b) 错误
var c int = int(math.Sqrt(float64(a*a + b * b))) 正确
- 另外 float类型 不准,每个语言都存在,二进制无法准确表示十进制
3. 常量定义
func consts() {const filename string = "123.txt"// 显示指明类型const a, b float32 = 3, 4const b float32 = 4// 这里必须加上 float64var c int = int(math.Sqrt(float64(a*a + b*b)))fmt.Println(a, b, filename, c)
}func consts() {const filename string = "123.txt"const a, b = 3, 4// 这样声明的话, 不需要, a, b是文本, 就自己会当成float来用~var c int = int(math.Sqrt(a*a + b*b))fmt.Println(a, b, filename, c)
}// 枚举类型
func enums() {const(cpp = 0 // app = iota 表示自增值,下面的都不需要复值了。 _ 可以跳过一个值,你放说java不要了,它的位置写为 _java = 1python = 2golang = 3)// 利用 iota 实现复杂的const (b = 1 << (10 * iota)kbmbgbpb)// 结果按照公式进行1 1024 1048576 1073741824 1099511627776
}
4. 条件语句
if else 语句
if 条件{... } else if {... } else {... }// 支持条件内赋值, 变量的作用域就在 if else语句中 if a, b := 2, 3; a == 1{fmt.Println(a) } else {fmt.Println(b)fmt.Println("cannot print", nil) }
switch 语句
// switch语句不需要每句后面加上break, 每个case后面都有break, 除非使用 fallthrough var result int var op = 1 switch op { case 1:result = 1// 中断程序执行,报错 panicpanic("error") case 2:result = 2 case 3:result = 3 default:result = 4 }
for 语句
sum := 0 for a := 1; i <= 100; i++ {sum += i }
// 复杂的读取文件,并逐行打印
func forT(filename string) {file, err := os.Open(filename)// 判断是否异常if err != nil {panic(err)}scanner := bufio.NewScanner(file)// 这里只有这个 结束条件。 直接省略掉 ; (起始条件,递增条件都没有), 充当while 使用for scanner.Scan() {fmt.Println(scanner.Text())}
// 还可以省略 递增条件. 相当于死循环for {fmt.Println("abc")}
5. 函数
// 1. 返回单值func eval(a, b int, op string) int {}// 2. 返回两个int值func div(a, b int, op string) (int, int) {return a / b, a % b}// 3. 还可以给返回值取名字:func div(a, b int, op string) (q, r int) {return a / b, a % b}// 当执行函数时, 可自动生成q, r := div(13, 3)// 4. 当定义出来后,直接自动返回func div(a, b int, op string) (q, r int) {q := a / br := a % breturn // 自动返回 q, r: 不建议这样, 当代码比较长,可读性太差}// 5. 按照上面的例子,在其它函数中调用// 这是错误的,因为这是返回的两个值不能像python那样 return div(a, b)[0] 表示返回第一个值return div(a, b) // 在这样也是错误的! 哈哈。。 go语言中定义两个变量,必须都要使用起来,编译错误q, r := div(a, b)return q// 正确写法, _ 表示不需要这个值q, _ := div(a, b)return q// 6. 一般来讲,多返回值不是乱用的, 第二个返回值,一般是返回的错误,例如file, err := os.Open(filename)func div(a, b int, op string) (int, error) {if a == 1 {return 1, nil} else {return 0, fmt.Errorf("err....") // 出错时,返回异常error}}aaa, err := div(2, 3, "123")fmt.Println(aaa, err)// 结果会被打印出来// 0 err....123// 7. 函数作为形参// op 是个函数, 有两个int 类型参数, 返回值是 int 类型func apply(op func(int, int) int, a, b int) {return op(a, b)// 也可以获取原来的函数名p := reflect.ValueOf(op).Pointer() // 获取函数真正的指针opName := runtime.FuncForPc(p).Name() // 获取函数名}// 8. 匿名函数// apply 是个函数, apply(func (int, int) int, a, b int), 下面调用apply,中间函数参数传递是匿名函数,定义在内部apply(func(a int, b int) int {return a}, 3, 4))// 函数参数: go 语言中没有 默认参数,可选参数, 函数重载等 比较复杂的函数参数类型
// 有可变参数列表
func sum(numbers ...int) int{ // numbers 可以接受多个int 值s := 0 for i := range numbers {s += numsbers[i]}return s
}
// 执行
sum(1, 2, 3, 4, 5)
6. 指针
// C语言中
int a = 100;
// 第1行代码中 * 用来指明 p 是一个指针变量,第2行代码中 * 用来获取指针指向的数据。
int *p_a = &a;
*p_a = 200;// go 中
var a int = 2
var pa *int =&a
*pa = 3// go语言中的指针简单再: 不能进行运算。 C语言中,获取列表的头指针,可以一直不断的进行运算加下去。
函数中的 参数传递:go语言中只有值传递一种传递方式!
go语言中,值传递, 还可以进行指针传递。相当于引用的作用。
// 指针的作用 又 强于 引用。 一般来讲 java,Python,内建的基本类型是 值传递,比方说 : // Python中 a = 5, 传递到函数内,不会影响该 变量的值。这是值传递。 // list 列表等,容器对象 和 自定义类型对象,都是 引用传递// 而 go语言中的, 指针传递。 var a int func f(pa *int){} // 传递 &a 进去,就算是 int类型, 也可以函数内修改该变量 举个简单的例子: 交换两个数 var a, b int = 3, 4 // 值传递 是交换不了的, 只是将a, b 的值拷贝一份, 形参 也叫 a, b func swap(a, b int) {a, b = b, a } swap(a, b) // 指针传递: 可以正确交换 func swap(a, b *int) {*a, *b = *b, *a } swap(&a, &b)// 当 传递的是 object 时, 也是值传递。 var cache Cache func f(cache Cache){}// cache 是一个 Cache 类型的对象。 它保存的并不是真正内存中的数据,而是指向内存中数据的指针, // 所以,值传递完全没有问题, 相当于将 对象的 指针拷贝了一份, 操作的是同一个对象
那么看到一个对象, 到底该用值类型,还是指针类型传递呢? 在 对象封装 中会讲。
第三章: 内建容器
(一)数组
1. 数组
func array() {// 一位数组定义方式var arr1 [5]intarr2 := [3]int {1, 2, 3}arr3 := [...]int{2, 4, 6, 8 ,10}// 二维数组: 4行5列var grid [4][5]intfmt.Println(arr1)fmt.Println(arr2)fmt.Println(arr3)fmt.Println(grid)// 1.遍历for i:= 0; i < len(arr3); i++{fmt.Print(arr3[i], " ")}// 2.遍历: rangefmt.Print("\n")for i := range arr3 {fmt.Println(arr3[i], " ")}// 相当于 python中的 enumerate(), 遍历 索引和 值// 如果只想要值, 可以 for _, v := range arr3, 下划线代替for i, v := range arr3 {fmt.Println(i, v)}// 为什么要使用range, 1. 简洁美观。 2.
}
数组是值类型!
// arr [5]int 这样传递的是 值类型。 也就是传入的数组 必须是 5长度才行。 func printArray(arr [5]int) {arr[0] = 100for i, v := range arr {fmt.Println(i, v)} }printArray(arr1) // 对 printArray(arr2) // 错误// 因为是值传递, 相当于将数组整体 五个元素做一个拷贝,再函数内对数组进行更改,不会影响到外部!// 要想其他语言中可以改变原数组,就需要传递指针了 func printArray(arr *[5]int) {} // 数组内通过 arr[0] = 100 可以修改外部数组的值 printArray(&arr1)
go语言中一般不直接使用数组: 而是切片~
2. 切片(slice)
arr := []int {1, 2, 3, 4 ,5 ,6 ,7 ,8} // 注意和数组区别,不用加 ...
// 1. 这里切片和 python是一样的 s = [2, 3, 4, 5], 左闭右开
s := arr[2:6] // 2. 其他写法: arr[:6], arr[2:], arr[:] 和python一样// 3. slice 不是值类型, 它的内部有个数据结构, slice 是对arr 的一个视图,看到数组中指定的内容
// 将 数组的切片(slice) 传递到 函数中, 对其修改是可以改变外部数组的
func updateSlice(s []int) {s[0] = 100
}updateSlice(s)
fmt.Println(arr) // 会变为100// 4. 如果要对原数组进行操作,就可以使用 slice切片 即可
updateSlice(arr[:]) // 5. 可以对 切片进行切片。 得到的是以当前切片为准的 切片
arr := [...]int {1, 2, 3, 4 ,5 ,6 ,7 ,8}
s1 := arr[2:6] // s = [3, 4, 5, 6]
s2 := s1[3:5] // s = [6 ,7]
s3 := s1[3:] // s = [6]// 可以看到 上面你那个超额去取, 因为底层都是对 arr的视图, 当s2规定[3:5] 超过s1时,会取到隐藏的部分
// s3 取 s1[3:], 默认只能看到显示的 s1的部分,也就是只取到一个 6// 二维切片
var a [][]string
// 或者
b = make([][]string, 0)// 赋值
a = [][]string{[]string{"abc", "efg"}, []string{"abc", "mln"}}
fmt.Println(a)// 结果
[[abc efg] [abc mln]]
3. 切片的底层原理
slice
param: ptr 指向的是切片的开头元素
param: len 当前的切片长度, 当s[6] 大于该长度就会报错。
parma: cap(capacity), 指针对 ptr 到 最后有多长。// 需要注意:
// 1. slice 可以向后扩展,但是没法向前扩展, 当切片的第一个 元素大于 len 时,就会报错。s2 = s1[6:7] 就会报错,不会进行扩展
// 2. 可以打印出, 获取 len 和 cap 的值
fmt.Printf("s1= %v, len(s1)=%d, cap(s1)= %d\n", s1, len(s1), cap(s1))
// 可以得到结果
s1= [3 4 5 6], len(s1)=4, cap(s1)= 6
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X79rhUOh-1635157982890)(.\slice.png)]
4. 向slice 添加元素
arr := [...]int {1, 2, 3, 4 ,5 ,6 ,7 ,8}
s1 := arr[2:6]
fmt.Printf("s1= %v, len(s1)=%d, cap(s1)= %d\n", s1, len(s1), cap(s1))
s2 := append(s1, 10)
s3 := append(s2, 11)
s4 := append(s2, 12)
s5 := append(s4, 13)
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
fmt.Println(s4)
fmt.Println(s5)
fmt.Println(arr)// 得到结果为:
/*
s1= [3 4 5 6], len(s1)=4, cap(s1)= 6
[3 4 5 6]
[3 4 5 6 10]
[3 4 5 6 10 11]
[3 4 5 6 10 11 12]
[3 4 5 6 10 11 12 13]
[1 2 3 4 5 6 10 11]可以看到 s1 的最大结果为 cap = 6, 所以后面对切片的操作时, arr中,10, 11 覆盖了 7, 8。
而后面插入 12, 13 ,并不会影响到 arr 了。 总结: 当添加元素时,如果超越了 cap, 系统会重新分配更大的底层数组,并将原来的元素拷过去(参考上面的 底层结构)另外: 由于值传递, 必须接收 append 的返回值。 因为 append 时,可能底层数组改变里,len,cap等都可能改变, 需要重新接收新的返回
*/
5. slice 的其他操作:创建, delete ,和copy
- 创建 slice
// 1. 声明 变量的类型为 slice
var s []int // 默认为 nil, go中变量 定义不初始化也行// 2. 第二种方式: 建立了一个 array, 有声明了 s1 是 array的 slice
s1 := []int {2, 4, 6, 8} // 3. 建立定长的 slice
s2 := make([]int, 16, 32) // len = 16, cap = 32, 不值当cap,默认为 len长// 4. 可以进行append
for i := 0; i < 100; i++ {fmt.Printf("len(s)=%d, cap(s)= %d\n", len(s), cap(s))s = append(s, i * 2 + 1)
}// 每次对底层数组的扩充, 都是乘 2的。打印输出可以看到
len(s)=0, cap(s)= 0
len(s)=1, cap(s)= 1
len(s)=2, cap(s)= 2
len(s)=3, cap(s)= 4
len(s)=4, cap(s)= 4
len(s)=5, cap(s)= 8
len(s)=6, cap(s)= 8
len(s)=7, cap(s)= 8
len(s)=8, cap(s)= 8
len(s)=9, cap(s)= 16
len(s)=10, cap(s)= 16
len(s)=11, cap(s)= 16
len(s)=12, cap(s)= 16
len(s)=13, cap(s)= 16
len(s)=14, cap(s)= 16
len(s)=15, cap(s)= 16
len(s)=16, cap(s)= 16
len(s)=17, cap(s)= 32
len(s)=18, cap(s)= 32
len(s)=19, cap(s)= 32//5. copy: 将s1 复制到 s2
copy(s2, s1) // 6. delete
// 删除索引值 3的元素: s2[:3] + s2[4:]
// append第二个值是一个变长参数列表,也就是可以 一次添加多个值。当加入的是一个 slice, 后面加上 ... 即可
// 另外 删除一个元素,不影响 cap, 大小不变, len - 1
s2 = append(s2[:3], s2[4:]...) // 7. 删除 头的元素
front := s2[0]
s2 := [1:]// 8. 删除尾元素
tail := s2[len(s2 - 1)]
s2 := s2[:len(s2) - 1]
(二)Map
###1. 基础内容
// 1. 建立map
// 方法1.
m := map[string] string {"name": "xiaoming","course": "golang","age": "18",
}// 方法2. go 语言中 nil (相当于 Python的 None), 是可以参与运算的,相当于空 map 使用
m1 := make(map[string]int) // m1 == empty map 空map
var m2 map[string] int // m2 == nil 空// 2. 遍历map, 同样可以使用 下划线 _ 代替不想要的
// 注意: 每次遍历的得到的顺序不一定相同, 因为 map的 结构
for k, v := range m {fmt.Println(k, v)}// 3. 获取值
courseName := m["course"]
// 当获取不存在的值的时候,得到的就是空串, 对应数据类型的空值。 go语言变量不初始化,也能用courseName, exist := m["course"] // 第二个变量 exist 可以判断当前值是否存在,返回 bool值// 标准写法
if courseName, exist := m["course"]; exist {fmt.Println(courseName)
} else {fmt.Println("error")
}// 4. 删除元素
delete(m, "name")
2.map 键值对的数据类型
- map 使用hash表, 必须可以比较相等
- 除了 slice, map, function 的内建类型都可以key
- Struct类型 不包含上述字段, 也可以作为 key。(在编译的时候检查的)
3. 真题练习
- 力扣: 3. 无重复字符的最长子串
- 我自己的题解,当然没有 官方题解好~
import "fmt"
func lengthOfLongestSubstring(s string) int {charMap := map[byte] int {}i := 0rst := 0byteS := []byte(s) // 转换为字节 slicefor j := 0; j < len(byteS); j++ {if _, ok := charMap[byteS[j]]; !ok {charMap[byteS[j]] = 1 } else {// 当前元素以重复出现,先计算当前窗口大小if rst < j - i {rst = j - i}// 去除重复for _, ok := charMap[byteS[j]]; ok ; _, ok = charMap[byteS[j]]{delete(charMap, byteS[i]) i += 1}// 添加当前元素到窗口charMap[byteS[j]] = 1 }}// 最后还会有一个位置没有判断if rst < len(byteS) - i{return len(byteS) - i} else {return rst}
}// 哈哈, 属实脑洞大开, 去除重复那里,本该是个 while循环, 用for 来写, 就要搞懂 ; ; 三部分都是啥,让我给写对了
// 如果 s[j] 在 map 里,就一直循环, i++
for charMap[s[j]]; ok ; _, ok = charMap[s[j]]{}
// 还可以直接这样: 默认找不到元素是 返回 0
for charMap[s[j]] != 0 {}// 注意点:
// 1. 最开始是定义了 i,j变量,才能在整个代码中使用!, for则只能在相应的 {} 代码块中使用
// 2. charMap := map[byte] int {} 需要注意这个定义, go语言对字符的处理// 另外需要注意的是
###(三)字符和字符处理
1. 简单描述
- 根据前面的真题练习,可以发现,go语言对 字符的表示。
- 文章讲解 string, rune, byte
func strings() {s := "ABC我是初学者!" // UTF-8 编码, rune 类型fmt.Println(s, len(s)) // 打印长度为 19!// 1. 转为字节类型输出for _, b := range []byte(s) {fmt.Printf("%X ", b )}// 一字节的表示 打印如下: 中文为 3字节表示// 比如 E6 88 91 表示 "我" , 是 UTF-8编码// 41 42 43 E6 88 91 E6 98 AF E5 88 9D E5 AD A6 E8 80 85 21fmt.Println() // 2. 直接输出for i, ch := range s {fmt.Printf("(%d, %X)", i, ch)}// 结果: (0, 41)(1, 42)(2, 43)(3, 6211)(6, 662F)(9, 521D)(12, 5B66)(15, 8005)(18, 21)// ch 为rune 类型(等价于 int32)四字节整数。// 6211表示 "我", 是unicode编码。 且可以看到 下标递增三。// "我": 过程,将s(string) 字符 utf-8解码:E6 88 91, 转为 unicode 6211, 再放到 rune类型 四字节类型中保存fmt.Println()// 3. 调用utf8这个库fmt.Println(utf8.RuneCountInString(s))// 打印结果为 9// 拿到字节数组bytes := []byte(s)// 返回每个字符 和字符大小,英文1,中文3。 解码for len(bytes) > 0 {ch, size := utf8.DecodeRune(bytes)bytes = bytes[size:]fmt.Printf("%c ", ch)}// 结果 A B C 我 是 初 学 者 !fmt.Println()// 转为rune 类型for i, ch := range []rune(s) {fmt.Printf("(%d, %c)", i, ch)}// 结果 (0, A)(1, B)(2, C)(3, 我)(4, 是)(5, 初)(6, 学)(7, 者)(8, !)
}
- 总结:
- 使用 range 遍历pos, rune对
- 使用
utf8.RuneCountInString()
获得字符数量 - 使用 len获得字节长度
- 使用 []byte 获得字节
- 再来理一下上面的内容
package mainimport ("fmt""unicode/utf8"
)func main() {s := "ABC我刚开始学go!"var runeS []rune = []rune(s) // 打印 11,每个rune 是int32,4字节fmt.Println(len(runeS))var byteS []byte = []byte(s) // 打印 21,中文字符集占三个字节fmt.Println(len(byteS))// 返回每个字符 和字符大小,英文1,中文3。 解码for len(byteS) > 0 {ch, size := utf8.DecodeRune(byteS)byteS = byteS[size:]fmt.Printf("%c ", ch)}// 结果 A B C 我 是 初 学 者 !// 3. 调用utf8这个库, 可以得到想要的长度 11fmt.Println(utf8.RuneCountInString(s))
}// 注意: a := []byte(s) 或 a := []rune(s) 或者 s(字符串默认都是 rune类型)
// 他们 使用 a[2], s[2] 得到的都是数字, 字符串在go里的特殊表示
2. 真题:国际版的(支持中文)
// 将字节(byte) slice, 表示为 字符slice (rune)
func lengthOfLongestSubstring(s string) int {charMap := map[rune] int {}i := 0rst := 0runeS := []rune(s)for j, ch := range runeS { // 这里的ch 也是 数字, 或者直接 for j := 0; j < len(runsS); j++if _, ok := charMap[ch]; !ok {charMap[ch] = 1 } else {// 当前元素以重复出现,先计算当前窗口大小if rst < j - i {rst = j - i}// 去除重复for charMap[ch] != 0{delete(charMap, runeS[i]) i += 1}// 添加当前元素到窗口charMap[ch] = 1 }}// 最后还会有一个位置没有判断if rst < len(runeS) - i{return len(runeS) - i } else {return rst}
}
- 最后看下比较结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LKl44vPr-1635157982892)(./rune_byte_range.png)]
- 注意,只是作为性能参考, 并不准确, 我又试了几次,时间其实大差不差。
3. 字符串的其他操作
// strings 库下有很多操作
老师没细讲,后序再来记录
- 总结
- 字符串分开或者合起来: Fields, Split, Join
- Contains, index: 查找子串
- ToLower, ToUpper:
- Trim, TrimRight, TrimLeft
第四章: 结构体和方法“面向对象”
###1.基本内容
- go语言 面向对象的特点
- 仅仅支持封装, 不支持继承和多态
- go 语言没有class, 只有struct
- 定义
// 定义一个树的节点类型
type TreeNode struct {Left, Right *TreeNode // 指针类型Value int
}
- 创建
func main() {// 1. 定义var root1 TreeNoderoot2 := TreeNode{}// 初始化root1 = TreeNode{val: 3}root1.left = &root2 // 注意 指针,传递地址// 注意go语言中,不管是实例 还是 属性,都是 . 像 c++ 可能是 ->root1.right = &TreeNode{val: 4}fmt.Println(root1.val)// 2. 还可以定义一个 slicenodes := []TreeNode {// 省略掉 初始化数据{val: 3},{},{nil, &root1, 6},}fmt.Println(nodes)//结果: [{<nil> <nil> 3} {<nil> <nil> 0} {<nil> 0xc000004078 6}]
}
###2. 构造函数?
结构体是没有 构造函数的。
- 因为上面介绍有很多种初始化的方法。 但是我们有时候需要 自己去控制 它的构造
- 可以使用
工厂函数
,实际上就是普通函数,比如
// 工厂函数 func createNode(val int) *TreeNode{return &TreeNode{val: val} }// 学过c++ 的话可以看到这里有个典型的错误, TreeNode{val: val} 是在函数内构建的局部变量 // 返回局部变量的指针, 程序就挂了! // go 语言中不会挂~ 局部变量 也可以返回给别人用root1.left.right = createNode(9) // 这样也是可以的
3. 结构体的创建(堆区栈区)
- 既然上面讨论了,返回局部变量的地址,那么这个 TreeNode 是存在 哪里的呢?
- 例如c++, 局部变量是分配在栈上的, 函数一退出,局部变量就会立刻被销毁
- 如果要传递出去, 就需要在堆上进行分配, 需要手动释放。这是c++。
- java中一般对象都是在 堆中的,变量是在栈中的。
- go 语言中:
- 不需要知道~ 因为有垃圾回收器。 需要根据go语言的编辑器,和运行环境来决定的。
- 就像上面的例子, 当 编译器发现 TreeNode{val: val} 没有取地址并且返回时,觉得它不需要给外界用,可能在临时栈中进行分配。
- 当 取了一个地址(像上面的例子),并返回出去, 就会在堆上进行分配。堆上分配完后,就会参与垃圾回收。 当传递到外界的指针 “被扔掉” 不用了, 那么这个结构体对象,就会被回收了。
- 内存结构图
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ponh9NUs-1635157982893)(./struct.png)]
4.为结构体定义方法
// TreeNode 结构体
type TreeNode struct {left, right *TreeNodeval int
}// 给结构体定义方法
func (node TreeNode) print() { // (node TreeNode) 接收者fmt.Println(node.val)
}// 调用
root1.print()
有个接收者, 这里就是 node TreeNode。
go 语言的这个定义,其实和普通的函数方法差不多,只是一种语法, 实际上就是普通的函数,等价于:
func print(node TreeNode) { // 这里是传值,go 只有传值fmt.Println(node.val) }// 调用 print(root1)
需要注意,上面的写法都是传值的, 函数内部修改不会堆 外部对象造成影响,等于拷贝。
- 需要这样写:
func (node *TreeNode) print() { // 传指针fmt.Println(node.val) }// 调用: 这点是比较人性化的。 虽然上面变成了传指针,但是调用起来是不变的。实际调用会自动传递地址进去 root1.print()// 相同的,如果函数定义的就是传值, 你传递进去一个地址~ 也不会影响,会自动从地址把值取出拷贝。
总结: go很聪明,知道你函数调用时 是要值,还是要指针。
- nil 指针也可以调用 方法! 和其他语言不一样, 因为go有时候初始化的是空对象,有时候是 nil。但都可以使用。
type TreeNode struct {
left, right *TreeNode
val int
}
// 给结构体定义方法
func (node *TreeNode) print() { // (node TreeNode) 接收者
fmt.Println(node.val)
}
func (node *TreeNode) setValue(val int) {
if node == nil {
fmt.Println(“Setting val to nil node!”)
}
node.val = val
}
var pRoot *TreeNode
// nil 指针,调用方法
pRoot.setValue(200) // 到这里可以打印出来,说明能够调用方法,但是不能执行下面的 node.val = val 操作~
pRoot = &root1
pRoot.setValue(300)
pRoot.print()
* 对于值接收者,指针接收者的问题* 要改变内容,必须使用指针接收者
* 结构体过大也考虑使用指针接收者
* 一致性: 如果有指针接收者, 最好都是指针接收者。
* 值接收者是 go 特有的。 指针接收者,c++, java引用,python(self),也类似指针。### 5. 实现一个遍历二叉树的方法* 前序遍历: [力扣原题](https://leetcode-cn.com/problems/binary-tree-preorder-traversal/submissions/)```GO
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
// 递归解题
func preorderTraversal(root *TreeNode) (vals []int) { // 这里相当于定义了 vals 这个变量var preorder func(*TreeNode)preorder = func(node *TreeNode) {if node == nil {return}vals = append(vals, node.Val)preorder(node.Left)preorder(node.Right)}preorder(root)return
}// 另外需要注意, 初始化 slice
zxc := []int{}
var cvb []int
fmt.Println(zxc == nil, cvb == nil) // 结果 false, true
- 重点是nil 指针,调用方法, 如果将上面的实现为 TreeNode 的方法, 其他语言中,node == nil 时调用会出错的, go语言中 nil 可以调用函数,内部判断下即可。
// 写成TreeNode的 方法
func (node *TreeNode) traverse() {if node == nil {return}node.print()node.left.traverse()node.right.traverse()
}
###6. 包和封装
通过对函数的命名,来区分函数时可见或者 不可见等。 名字一般使用 CamelCase: 单词首字母大写连在一起。
针对包来说:
- 首字母大写:
public
- 首字母小写:
private
- 首字母大写:
包:
- 每个目录一个包: 包名不一定要和目录名一样, 但是每个目录只能有一个包
- main包 包含可执行入口, main函数。如果目录下有一个main函数,那么这个目录下只能有一个main包。
- 为结构定义的方法,必须放在同一个包内。
- 可以使不同文件
tree目录node.go //定义方法 和 节点结构体 package treetraverse.go // 为了清晰,还可以分开, 把遍历节点的方法单独放一个文件里。都可以找到entry // 目录entry.go // 将main函数移动到这里, package main : 一个目录下只能有一个包。所有要新建一个目录,取名 package main, main函数写进来。// entry.go 中import 语句, 是导入 tree 目录的。// entry.go 中import 语句, 是导入 tree 目录的。 // 创建变量时需要, tree.TreeNode var root tree.TreeNode
关于导包的一点东西(自己感觉得):
- 首先包名不一定 和包 所在目录名一致。
- 然后导包的时候,是导入的目录名
// 目录结构 01testingretriever.go // 包名是 package testing111, 定义了一个 type Retriever struct{}downloader.go // 导入包,并调用包里的内容import "01/testing" // 导入到路径名testing111.Retriever{} // 调用使用包名
另外: 再一个包中定义的结构体,结构体方法, 只能定义在同一包下,否则会报错:
invalid receiver *testing111.Retriever (type not defined in this package)
在 上面的testing 目录下再创建一个文件 , 同样 package testing111 里面可以成功定义方法,因为属于同一个包。
7. 扩展已有类型
在其他语言中有 继承的概念,可以扩展已有的类型
go语言中是没有继承得
go可以扩充
系统类型
还可以扩充别人的类型
有两种方式:下面的代码。依照上面定义的包结构(tree目录)
- 使用组合:
// (根据go语言习惯,包名叫 tree, 将TreeNode 改写为 Node, tree.TreeNode,叫起来太重复了)// 且首字母大写 表示Public的方法。所有方法名改为 首字母大写。type Node struct {Val intLeft *TreeNodeRight *TreeNode}// 目标: 让节点对象 支持中序遍历(Node 实现的是前序遍历)// 定义自己的节点type MyTreeNode struct {node *tree.Node // 保存节点类型的指针。}func (myNode *MyTreeNode) InOrder() {if myNode == nil || myNode.node == nil { // 自己为空,或者 包装的node为 空,都不进行下去。return}// 需要将 Node 包装成 myTreeNode,调用中序遍历方法,进行中序遍历left := myTreeNode{myNode.node.Left} right := myTreeNode{myNode.node.right} // myTreeNode{myNode.node.Left}.InOrder() 这里不能这样写,因为传递的是指针,必须要用一个变量接收left.InOrder()myNode.node.Print()right.InOrder()}// 报错信息展示: myTreeNode{myNode.node.Left}.InOrder()/*cannot call pointer method on myTreeNode literalcannot take the address of myTreeNode literal无法在 myTreeNode 文字上调用指针方法不能取 myTreeNode 文字的地址*/
- 通过别名
实现一个 队列。// 定义一个 Queue。 目录结构如下queue // 目录entry // 目录main.go // 程序入口queue.go // 定义结构体和 方法
queue.go
package queue// 定义一个 队列
type Queue []intfunc (q *Queue) Push(v int) {// go语言的特性,指针是可以被改变的。 q传进来的是一个指针,*q 去操作它指向的对象// 然后 q 指向的对象,再变为 指向 append 新产生的对象// *q 相当于 python 中的 self,表示对象本身但是 go 里面, self(q)指向的却被变为了新产生的对象~!*q = append(*q, v)
}// 先进先出队列
func (q *Queue) Pop() int {head := (*q)[0]*q = (*q)[1:]return head
}func (q *Queue) IsEmpty() bool {return len(*q) == 0
}
- main.go
package mainimport ("fmt""01/queue"
)func main() {q := queue.Queue{1}q.Push(2)q.Push(3)fmt.Println(q.Pop())fmt.Println(q.Pop())fmt.Println(q.IsEmpty())fmt.Println(q.Pop())fmt.Println(q.IsEmpty())
}
- 打印结果为:
1
2
false
3
true
// 符合预期
8. 使用内嵌扩展已有类型
- Embedding:
内嵌
// 前面组合的方式:
type MyTreeNode struct {node *tree.Node // 保存节点类型的指针。
}// 内嵌就是 去掉 node, 相当于是go的一个语法糖,省下一些代码的量,简洁一些
type MyTreeNode struct {*tree.Node // 保存节点类型的指针。
}// 内嵌的好处是,节省代码。 看下之前组合中 中序遍历里的代码
left := myTreeNode{myNode.node.Left}
right := myTreeNode{myNode.node.Left}// 使用内嵌后, myNode 就相当于是 有了原来Node 的所有属性,和方法,还有自身定义的属性和方法。
// 直接写, myNode.Left, 省去了 中间的node
left := myTreeNode{myNode.Left}
right := myTreeNode{myNode.right}
- 总结: 扩充系统类型或者别人的类型
- 定义别名: 最简单
- 使用组合: 最常用(从定义别名到 使用 组合之间不能无缝的进行转换,要注意后期维护)
- 使用内嵌:语法糖,省代码。(易读性差一点)
9.扩展和其他语言“继承”的区别
使用内嵌
其他语言中的重写,体现在go中, 也可以在 myTreeNode 中定义 TreeNode同名的 方法,提示是: shadowed。
直接调用 mynode.方法名,将会是调用重写后的方法。 但是隐藏的 mynode.TreeNode.方法名,将可以调用原来的方法。
java中有个体现多态的概念:
父类父类引用指向子类对象
(大概是上转型):父类对象不可以访问,子类方法。
而上转型对象,既可以访问父类方法(通过 super. 来调用),也可以访问子类继承或隐藏的成员变量,或者调用子类继承的方法(父类方法)或者子类重写的实例方法。进而,一个父类类型的引用指向一个子类的对象既可以使用子类强大的功能,又可以抽取父类的共性。
- go 语言是不能这样的~, 要实现类似的功能, go 语言是通过接口来的。
第五章:go语言的依赖管理(包管理)
1. 基本概念
- 依赖的概念:
- 依赖有很多意思
- 这里是指,大量使用第三方库,我们要把功能建立在别人已经实现的一些基础设施上。
- 比方说,在github上拉一个别人的库,将库的代码拉到代码树上,我们的代码和 这个库一起built,这样才能工作。 这是我们关心的重点(依赖)。
- 依赖管理经历了三个阶段: GOPATH, GOVENDOR,go mod(主流)。
2. GOPATH
就是go的安装路径
默认在
~/go(unix, linux)
,%USERPROFILE%\go (windows)
都是在用户目录下- GOPATH 的管理方式,就是不管理。当指定GOPATH,你需要任何东西,我都会到GOPATH下面去找。
- 问题: 造成所有项目所需要的,或者依赖的库等东西,都放到GOPATH目录下面。会变得越来越大。
实践操作GOPATH(切记先关掉 GO111MOODULE):
- 关掉:
整个系统更改: go env -w GO111MOODULE = "off"
,当前窗口:export GO111MOODULE = "off"
- GOPATH 还必须满足一些目录条件
// 创建一个目录叫做 gopathtest,来作为 GOPATH// 目录结构 gopathtestsrc // 这个目录必须有,且必须这个名字。src下面可以放各种代码// 1. go env -w GOPATH = "目录" 更改系统整个GOPATH // 2. export GPPATH="目录" 临时更改。仅限这个终端窗口// 拉去一个项目 go get -u go.uber.org/zap // 库名 zap // 再次看到, src 目录下就会出现 go.uber.org
- 关掉:
写代码测试:
zaptest.go
import "go.uber.org/zap"func main() {log, _ := zap.NewProduction()log.Warn("warning test")}
- 一般找包会从两个地方找:
- GOROOT: 就是安装go的位置下有个 src 下面找
- 从指定的 GPPATH下去找
- 一般找包会从两个地方找:
问题: 上面导图的 zap库,若有两个项目都需要使用这个库,但是所需版本不同,那么就做不到了
- 解决方案:在项目目录下,建立一个vendor目录,下面放上,zap库,就会优先使用这里的。
// 目录结构 gopathtestsrcgo.uber.orgzap // zap 库project1 // 项目1vendor go.uber.org // 放上一份project2 // 项目2vendor go.uber.org // 放上一份
- 不能总是所有依赖都 go get 下来后,再手工拷近 vendor里,所以有了一些第三方的依赖管理工具
- glide, dep,go dep 等等
- 真实的操作就不会去动 vendor目录, 而是去更改各种管理工具的配置文件,设置清楚后, 让管理工具去完成拷贝这个工作。
3. go mod使用
- 基本使用和介绍:
当开启 GO111MOODULE, 再次 :
go get -u go.uber.org/zap
下载这个包- 可以指定版本
go get -u go.uber.org/zap@v1.12.0
- 然后代码结构是这样的:
gomodetest // 创建项目名go.mod go.sum // 介绍可参看: https://my.oschina.net/renhc/blog/3171035zaptest.go // 测试代码
- go mod 内容:
module gomodetestgo 1.17require go.uber.org/zap v1.19.1require (go.uber.org/atomic v1.7.0 // indirectgo.uber.org/multierr v1.6.0 // indirect )
- zaptest 代码和上面不变
- 调用的版本啥的,就是指定的。
- zap 安装位置在go/pkg/mod:
C:\Users\Administrator\go\pkg\mod\go.uber.org\zap@v1.19.1
- 命令
go mod init +模块名字 生成 mod 文件
go mode tidy, 删除未使用的模块 或者 添加已使用但go.mod中不存在的模块
- 可以在网上查找,目前的认知只能对 go mod 这块做这些笔记,理解不够深刻。
- 如果要更换版本,直接 修改go mod,比方说使用 1.12.0 版本,直接改, 改完后 go mod tidy 就可以了。 打开对应的目录,会发现两个版本都在其中。
- 两种方式生成好 go mod。 一种自己 go get …, 一种是运行程序,自动根据imoprt,来帮你 get
- 可以指定版本
- 把旧项目迁移到 go mod 上去
- go mod init +模块名字(或者其他) 生成 mod 文件。
- go build ./…, 每次碰到 import, 就会去拉去,所需要的库,放到本地统一的一个缓存里面。
- 如果项目用到过 GOPATH讲过的一些 管理工具,那么 init 时,可能会根据它们的配置信息,去生成mod。
4. 总结 go mod
- 由go命令 统一管理, 用户不必关心目录结构, 不像之前(GOPATH),自己的项目(project1,project2,必须放到GOPATH src目录下),有了 vendor后,更加复杂, 自己的项目必须还有 vendor目录。
- 初始化:
go mod init
- 增加依赖:
go get
, 或者直接写代码, build 时候自动就会把依赖拉取加进去 - 更新依赖: go get , @v 指定版本, go mod tidy去除多余或者添加没有的。
- 迁移项目:
go mod init, go build ./...
5. 目录整理
- 之前有讲过,一个文件目录只能有一个包, 一个包只能有一个 main函数。
// 像下面这样的目录结构:
01arrays.gomaps.goslices.gofuncs.go// 01是我建的一个项目, 下面每一个是我学习各个部分基础知识,创建的文件。当执行 go build + 任何一个文件都可以编译通过,但是当执行 go build, 一次编译所有文件就会出错:
main redeclaration in this block : 很明显,每个文件下面都有一个 mian函数,是不对的。
- 没有好的解决方法。把每一个main函数,分列到不同的文件里,如下目录结构
// 将每个文件单独放到一个文件夹下,才能有单独的 main函数。
01 arrays arrays.gomapsmaps.goslicesslices.gofuncsfunc.go
- 现在需要执行:
go build ./...
就可以编译通过。把当前目录(空)以及所有子目录中的文件 build,编译。 - 这样做法确实有些繁琐, 但是就是这样的~, go语言官方的工具包 tools也是这样的。一个命令,放在一个文件夹下。更工程化,代码结构更清晰。
- (暂时不是很懂)编译后会有编译好的结果文件, 一般编译一半失败在本目录下。编译成功,放在 GOPATH下的bin目录。
- 一次编译多个包,不会有结果。可以通过
go install ./...
来生成。
- 一次编译多个包,不会有结果。可以通过
第六章:接口
// 先瞅一下,看完后面再回来看这个结构。 Python也有这个 “鸭子类型”, 感觉go这里就是 把“静态类型”变成 “动态类型”了。
// 这个接口, 不用去考虑 Traversal 指的是啥, 也不用管这个接口 有没有被实现(java中。 go中就不能这样想)只要满足了 Traverse()方法。 就可以是它。
// “给人的感觉是: 好像不需要在定义变量时给他确定类型,而是运行时动态确定一样的感觉~, ” getTraversal()传递啥类型过来, 满足接口定义的方法, 接口就可以是该类型。type Traversal interface {Traverse()
}func main() {traversal := getTraversal()traversal.Traverse()
}
1. 引出接口
- 实现main 函数 和 功能函数的解耦。 main函数只需要调用功能函数,而不必去实现功能
写一个文件:downloader.gopackage mainimport ("fmt""io/ioutil""net/http"
)// 定义一个请求链接的函数
func retrieve(url string) string {resp, err := http.Get(url)if err != nil {panic(err)}defer resp.Body.Close()bytes, _ := ioutil.ReadAll(resp.Body)// 转字符串输出return string(bytes)
}func main() {fmt.Println(retrieve("https://www.imooc.com"))
}
- 这里耦合度已经降低了,但是,main函数必须调用 retrieve() 函数, 增大了耦合性。
- (假设在写一个工程)下面假设这个 main()的功能是由一个部门完成的。 retrieve 是由另个部门完成的(infra)。
// 当前目录结构
downloader.go
infra // infra包下,有个方法是 解析url的urlretriever.go
urlretriever.go:
package infraimport ("io/ioutil""net/http" )// 为了引入接口,先将它定义为一个 结构体 type Retriever struct {}func (Retriever) Get(url string) string{ // 不需要给接收者起名字,因为用不到resp, err := http.Get(url)if err != nil {panic(err)}defer resp.Body.Close()bytes, _ := ioutil.ReadAll(resp.Body)// 转字符串输出return string(bytes) }
downloader.go :
package mainimport ("fmt""01/infra" )func main() {retriever := infra.Retriever{} // 构建一个对象,调用即可fmt.Println(retriever.Get("https://www.imooc.com")) }
新的问题:
retriever := infra.Retriever{}
这句话,并不一定一直都是调用 infra.Retriever{}, 它不属于一个逻辑,更像是一个配置,把它提取出来。dowloader.go 中这样写func getRetriever() infra.Retriever {return infra.Retriever{} }func main() {retriever := getRetriever() // 通过方法去获取结构体对象fmt.Println(retriever.Get("https://www.imooc.com")) }
新的问题: 这样写,还存在一个很强的耦合, retriever的类型不能换。 他一直保持是 infra.Retriever 类型。
retriever := getRetriever()
始终相当于是var retriever infra.Retriever = getRetriever()
是infra.Retriever类型的。 如果我们另外还有一个团队(testing),他们也有一个 解析url的功能。那么,要想调用这个 testing.Retriever,就很麻烦了,需要从头到尾的去改 getRetriever方法为 testing.Retriever类型// 新的目录结构, 加入 testing部门 downloader.go infra // infra包下,有个方法是 解析url的urlretriever.go testingretriever.go
retriever.go:
package maintype Retriever struct{}func (Retriever) Get(url string) string {return "fask content" // 和infra部门实现内容不一样 }
那么最终修改: (引出接口概念)
package mainimport (// "01/infra""fmt""01/testing"
)// 返回值为接口类型,必须带有 Get方法
func getRetriever() retriever { // 返回类型 retrieverreturn testing.Retriever{}
}// 定义接口
type retriever interface {// GetGet(string) string
}func main() {var r retriever = getRetriever()fmt.Println(r.Get("https://www.imooc.com"))
} // 此时完全解耦, 我们只需要 更改 getRetriever() 方法里的 返回值为 infra.Retriever{},就可以调用 infar里的go了,其他都不用动。
- 接口是个抽象的概念, 不管他是个什么东西。 上面 对它的定义就是, 只要它能够调用 GET 方法就可以。
- 所以可以将它赋值为
testing.Retriever{}
或者infra.Retriever{}
- 如果我们将接口里的方法 定义为 GET1, 那么再次运行就会报错:
testing.Retriever does not implement retriever (missing Get1 method)
- go 中的接口和 java是不一样的。 学过 java 可能会疑问,testing.Retriever{} 对象,并没有实现接口呀? 怎么能拿来用呢? 这就是go语言特殊的地方,也比较先进: 鸭子类型(duck typing)。
- 所以可以将它赋值为
2. duck typing(鸭子类型)
之前就说过,go 没有像 面向对象的语言那样。它只支持封装, 像继承,和多态所要完成的事儿,在go中都是 使用接口代替来完成。所以go语言中的接口,要比其他语言中灵活会很多。
鸭子类型:简单讲: “像鸭子走路,像鸭子叫(长得像鸭子),那么他就被认为是鸭子”。不同人对事物的认知不一样。它是不是一个“鸭子”,要从我的观点去看,满足我的需要,我就认为它就是“鸭子”。 编程里: 只要你能提供,我接口里所定义的那些能力(方法),我就认为你是“鸭子”吗满足我的需要, 我可以赋值为 你的类型, 跟你的这些能力互动。
另外go语言属于是 结构化类型系统, 类似 duck typing ,因为 duck typing当时提的时候,一定要动态绑定!(参考Python解释型语言 鸭子类型),但go 语言是编译时就绑定了~ 只能说类似。 看到这里,可以再去理解开篇的那段话和代码了。
Python中的 duck typing
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SG8CZ2og-1635157982895)(.\py_duck.png)]
- **C++**中的 duck typing
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nND91X9P-1635157982896)(./c++_duck.png)]
- java 没有 duck typing,只有类似的代码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-if6uPutl-1635157982898)(./java_duck.png)]
- go 中的 duck typing
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ol3j3bmA-1635157982898)(./go_duck.png)]
###3. 接口的定义和实现
- go的接口是由使用者定义的。
- 前面的例子中, download , retriever 前者是使用者,后者是定义者, download 使用 retriever的 Get方法。 download 定义接口,接口具有 Get方法。
- 接口的实现是隐式的。只要实现接口里定义的方法,该类型(定义者),就算实现了接口
// 目录结构
retrievermock mockretriever.go //定义者main.go // 使用者
- main.go:调用者
package mainimport ("01/retriever/mock""fmt"
)type Retriever interface {Get(url string) string
}func download(r Retriever) string {return r.Get("www.imooc.com")
}func main() {var r Retrieverr = mock.Retriever{"this is me"}fmt.Println(download(r))
}
- mockretriever.go
package mocktype Retriever struct{Contents string
}// 实现get方法。(因为有了使用者,定义者默认算实现了 Get接口)
func (r Retriever) Get(url string) string {return r.Contents
}
###4.接口的值类型
- 按照上面的例子, r, 代表的是值类型。在其他语言中,可能只是一个引用,或者是一个指针。但是go语言中所有都是值类型。 r 内部不是有指针,这么简单,还有其他东西。
var r Retriever
fmt.Printf("%T, %v\n", r, r)
r = mock.Retriever{"this is me"}
fmt.Printf("%T, %v\n", r, r)// 打印结果
<nil>, <nil>
mock.Retriever, {this is me} // r 内部有个类型,还有个值// 将 函数Get 改为接收指针
func (r *Retriever) Get(url string) string {return r.Contents
}var r Retriever
fmt.Printf("%T, %v\n", r, r)
r = &mock.Retriever{"this is me"} // 取地址
fmt.Printf("%T, %v\n", r, r)// 打印结果
<nil>, <nil>
*mock.Retriever, &{this is me} // 都是指针值传递 是 mock.Retriever{"this is me"} 生成了一个对象,然后拷贝到 r肚子里。
指针传递 是将对象的指针放到 r 肚子里。
查看 r 的类型
- 方法一
// switch 可以获取 r的 类型switch v := r.(type) {case *mock.Retriever:fmt.Println(v.Contents)}
- 方法二: Type assertion (类型断言)
// Type assertion (类型断言)mockRetriever, ok := r.(*mock.Retriever) // 当为值时,就是r.(mock.Retriever)// ok用来判断 类型判断是否正确fmt.Println(ok, mockRetriever.Contents)
接口变量(比如r)
- 存在实现者的类型, 实现者的值(或者是实现者的指针, 指向一个实现着)
- 接口变量自带指针
- 接口变量同样采用值传递, 几乎不需要使用接口的指针(因为接口变量肚子里有个指针)。
- 指针接收者实现只能以指针方式使用; 值接受者都可以
任何类型:
interface{}
q := []interface{} {1, "ahah", '1'}// q就变成了,类似Python中的列表。 可以接收任何类型。
5. 接口的组合
- 第一种:
// 组合现有的两个接口
type RetrieverPoster interface {Retriever // 实现GetPoster // 实现Post
}s := &mock.Retriever{"this is me"} // mock.Retriever 中再定义一个 Post方法func session(s RetrieverPoster){s.Get()s.Post()
}
- 第二种:
// 直接定义接口
type Rf interface { Post(url string) stringGet()
}
- 像官网的 io.go 文件中,就提供了很多组合的接口: 可读,可写, 可读写等等。
// 部分代码
type Seeker interface {Seek(offset int64, whence int) (int64, error)
}// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {ReaderWriter
}// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {ReaderCloser
}// WriteCloser is the interface that groups the basic Write and Close methods.
type WriteCloser interface {WriterCloser
}
6. 常用系统接口
stringer: 相当于java的 tostring
fmt/print.go 下面有个 stringer。type Stringer interface {String() string } // 我们自己的对象,只要实现了方法(String()),就可以实现接口了。 fmt.Println(对象) // 就会调用了。
Reader
io/io.go 下面有个 Reader, Writertype Reader interface { // 实现读取文件的功能Read(p []byte) (n int, err error) }type Writer interface { // 实现写文件的功能: byte slice, string 都能作为 Reader, WriterWrite(p []byte) (n int, err error) }抽象,任何实现和 Read, Write 方法的对象,都等于实现了这两个接口,可以进行读写操作。
Writer
第七章:函数和闭包(函数式编程)
###1. 基本概念
- 函数式编程 VS 函数指针
- 函数是一等公民: 参数,变量, 返回值都可以是函数
- 在c++ 里面只有函数指针。 在java里面,函数只是名字,没办法将名字传送给别人
- python里是可以将函数作为参数传递的。
- “正统的” 函数式编程
- 不可变性, 不能有状态, 只有常量和函数。选择语句,循环语句都不能用
- 函数只有一个参数
- go, python都不是真正的函数式编程~ 用法更灵活
package mainimport "fmt"// 实现累加
func adder() func(int) int {sum := 0var a func(i int) int = func (i int) int {sum += ireturn sum}return a
}func main() {a := adder()for i := 0; i < 10; i++ {fmt.Println(a(i))}
}// 里面的函数只能写成这样
1. var a func(i int) int = func (i int) int , 定义一个变量接收
2. 更简洁的使用 匿名函数, 省去掉名字
func adder() func(int) int {sum := 0return func (i int) int {sum += ireturn sum}
}
上面的例子就是演示了一个闭包的概念(Python中都学习过相关的概念)
- 函数内部的叫做局部变量
- 在函数 a 外部的,但在函数adder 内部的,并且函数a也定义在函数adder内部,那么sum叫做 自由变量。(Python中有闭包名字空间)
Python中的闭包
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hLAivqUt-1635157982899)(./py_Closure.png)]
c++ 中的闭包(比较新的语法,c++ 14以后)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gqe5ou6e-1635157982900)(./c++_Closure.png.png)]
java中的定义要严格一些: 使用final 关键字,定义一个class。holder,里面就一个 value
java中的函数不是一等公民,不支持函数作为参数和返回值,所以使用一个对象进行模拟一个函数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tvLjX2Oz-1635157982901)(./java_Closure.png)]
例子1:斐波那契
- 实现斐波那契(fibonacci)
package mainimport "fmt"func fibonacci() func() int {a, b := 0, 1return func() int {a, b = b, a+breturn a}
}func main() {f := fibonacci()for i := 0; i < 10; i++ {fmt.Println(f())}
}
例子2: 为函数实现接口(续fibonacci)。
- 类似文件打印一样(上面提到的系统接口中的 io.Reader), 为fibonacci 实现打印接口,打印 fibonacci数。(有难度,理解了很久,要仔细多次看视频讲解,反复回头看,理解串联起来,涉及知识挺多的)
package mainimport ("bufio""fmt""io""strings"
)func fibonacci() intGen {a, b := 0, 1return func() int {a, b = b, a+breturn a}
}type intGen func() int // 函数类型, 是一个类型就能实现接口~~ go语言灵活的地方// 其实就是一种语法糖,前面讲过。
// (g intGen) , 就是 g.Read() 调用。 不写这种,就是 Read(g) 调用。没有很特殊,就是普通函数
// 所以 即使是函数对象,也可以实现接口(实现方法, 方法被使用者使用,等于实现接口)
func (g intGen) Read(p []byte) (n int, err error) {// n: 写了多少字节, error异常// next 调用 g,读取下一个 斐波那契数next := g()// 控制 大于 10000停止if next > 10000 {return 0, io.EOF}// 调用Sprintf,将对应 字节数,计算出来,转字符串并加入换行,存 p里s := fmt.Sprintf("%d\n", next)// 通过 NewReader 来代理写入 p的操作return strings.NewReader(s).Read(p) // strings.NewReader(s) 返回一个 Reader对象, 他调用Read() 方法,将s写入p
}func printFileContents(reader io.Reader) {scanner := bufio.NewScanner(reader) // 点进源码可以发现,这里将 io.Reader又进行包装为 Scanner对象for scanner.Scan() { // 循环判断, Scan() Text() 都是 Scanner 对象提供的方法,进行输出fmt.Println(scanner.Text())}
}func main() {f := fibonacci()// printFileContents 函数,将 参数 reader 定义为 io.Reader 接口类型。 所以需要 f实现了 Reader方法// 然后 printFileContents 函数 内部不断的操作 调用 Read方法,写入数据,然后读取。// 且 Read 内部实现了一个 每次将 一个 斐波那契数加入到 p 里面的功能。printFileContents(f)// 总体过程, 1. 将 f 对象作为参数传递给 printFileContents() 函数// 2. 内部使用 io.Reader 接口类型变量接收 f, f 实现了 Reader方法// 3. 内部不断的调用 next := go(), 来写入下一个 斐波那契数 供打印。// 难点。 1. 对 io.Reader 接口实现的理解, 2. 为一个函数类型 实现 Reade 方法(间接就是实现了io.Reader 接口)
}
例子3:使用函数来遍历二叉树
package mainimport "fmt"// 定义节点
type TreeNode struct {left, right *TreeNodeval int
}// 中序遍历
func (node *TreeNode) TraverseFunc(f func(*TreeNode)){ // 传递一个函数if node == nil {return }node.left.TraverseFunc(f)f(node)node.right.TraverseFunc(f)
}func main() {root1 := TreeNode{val: 3}// 对 nodeCnt 进行累加计数nodeCnt := 0// 传递匿名函数实现累加计数, 中序遍历统计节点个数root1.TraverseFunc(func (node * TreeNode){nodeCnt ++})fmt.Println(nodeCnt)
}
- go语言的闭包更为自然(Python有闭包空间,暂时不清楚go的变量管理, Python需要使用nonlocal声明, go不需要)
- go没有匿名Lambda表达式, 但是有匿名函数。匿名函数功能就够用了,要求简洁,不必要增加语法,实现 Lambda 表达式。
第八章: 资源管理与出错处理
- 资源管理
- 比方说打开文件需要关闭
- 连接数据库,需要释放
- 都是成对出现的,但是和 出错处理一混合,就变得比较复杂了。如果程序中途异常,如何保证打开的链接被关闭掉呢, 这就是 资源管理与 出错处理
1. defer调用
确保调用在函数结束时发生
defer列表 并且满足先进后出(就是defer 语句出现的顺序,先出现的,后执行)
参数在defer语句时计算
func tryDefer() {defer fmt.Println(1) // 先输出2, 后输出1fmt.Println(2) }func tryDefer() {defer fmt.Println(1)defer fmt.Println(2)fmt.Println(3) } // 保证先进后出。 所以打印结果是 3 2 1func tryDefer() {defer fmt.Println(1)defer fmt.Println(2)fmt.Println(3)return // panic("error") 这都不影响,输出结果依然为 3 2 1 即使后面程序进行中断, 依然不影响预期执行fmt.Println(4) }
实战进行 一个 文件的写操作(创建一个文件,写入fibonacci数列)
- go语言的
资源管理
: 创建文件 --> 关闭文件 . 写入缓存 --> Flush() 成对出现, 我们就可以在打开文件后面 直接 defer close()
package mainimport ("01/functional/fib""bufio""fmt""os" )func writeFile(filename string) {file, err := os.Create(filename)if err != nil {panic(err)}defer file.Close() // 完成时关闭文件// 直接写文件比较慢, 利用bufiowriter := bufio.NewWriter(file) // buffer 缓冲,先写入到内存中去,到一定大小时,一次性写入硬盘中defer writer.Flush() // 只写入buffer 缓冲,还需要导进去f := fib.Fibonacci()for i := 0; i < 20; i++ {fmt.Fprintln(writer, f())}}func main() {writeFile("fib.txt") }
- go语言的
参数在defer语句时计算:
func forDefer() {for i := 0; i < 100; i++ {defer fmt.Print(i, " ")if i == 30 {panic("error")}} }打印结果为: // 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 panic: error// 先输出30,再输出 29..28..0 满足先进后出。并且,不是打印 30个 30.说明 i 参数的值,是在程序执行 defer 语句时,已经计算好的。
一个超级有意思的例子:
package mainimport "fmt"func f1() int {x := 5defer func() {x += 1}()return x
}func f2() (x int){defer func() {x++}()return 5
}// 输出结果为 5, 6
- 分析不明白,但是根据第一个例子:改为传递指针
func f1() *int {x := 5defer func() {x += 1}()return &x
}a := f1()
fmt.Println(*a)// 结果是 6. 只能说是 值传递。 return 返回的是 数值5. x后面右 ++
- 简要分析: defer确实是在return之后执行的 第二个因为你是在返回的时候定义了x所以可以加的上。 而第一个就是 返回了一个数值 5。
2. 错误处理
go语言的错误处理,其实主要就是围绕着 error的。 比方说 file, err := os.Create(filename)。
- 对这个 err进行判断。做一些错误的处理。
- 举了一个例子,还是文件读写。
func writeFile(filename string) {file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666) // 0666 linux 文件权限, 加了这个os.O_EXCL,文件存在会打开不了。 人为制造一个错误异常if err != nil {// panic(err) 这里不能单纯的进行 panic, 中断程序。可以进行细致的错误处理// 1. 进行打印输出, 再返回。不让异常中断程序// fmt.Println("file already exists ", err)// return// 2. 打开OpenFile()函数源码会发现, If there is an error, it will be of type *PathError, err 是接口类型,实现Error()方法,这里报错将返回的是 *PathErrorif pathError, ok := err.(*os.PathError); !ok { // 类型断言,看是否会报这个类型的错panic(err) // 当送进来的不是 *os.PathError 类型,直接 panic} else {fmt.Println(pathError.Op, // 输出一些 关于pathError的信息pathError.Path,pathError.Err)}return}// 打印结果: open fib.txt The file exists. 对应上面的三个 打印。
- 还可以自定义error :
err = errors.New("this is a custom error")
, 也可以为期定义 Error方法, 实现接口。
3. 服务器统一出错处理
如何实现统一的错误处理逻辑
- 实现一个显示文件的 webserver, 访问上面生成的 fib.txt 文件
- 文件目录
errhandlingdeferfib.txt // fib文件filelistingweb.go // webserver, 访问 fib 文件
最初版本(实现逻辑)
// web.gopackage mainimport ("fmt""io/ioutil""net/http""os" )// 实现一个显示文件的 webserverfunc main() {http.HandleFunc("/list/", func (writer http.ResponseWriter,request *http.Request) {// 拿到list 后面具体的文件名,或者目录fileName := request.URL.Path[len("/list/"):] // /list/fib.txt, 通过切片,只要后面的。fib.txt// 打开文件fibPath := fmt.Sprintf("%s%s", "../defer/", fileName) // 拿到文件名,拼接路径file, err := os.Open(fibPath)if err != nil {panic(err)}defer file.Close()// 读取文件all, err := ioutil.ReadAll(file)if err != nil {panic(err)}// 内容写入到写入到 ResponseWriter 里去writer.Write(all)})err := http.ListenAndServe(":8888", nil)if err != nil {panic(err)} }
- 可以试着访问: http://localhost:8888/list/fib.txta, 将得到报错提示
- http: panic serving 127.0.0.1:60714: open …/defer/fib.txta: The system cannot find the file specified.
- os.Open 函数会报错, panic 中断程序,但又没有完全中断。 打印了一堆错误提示,然后如果你此时输入正确的 url, 将得到正确的提示。服务器并没有停止(一般panic会把整个程序中断掉,但是在 httpserver里,panic不会中断程序 )
- 可以试着访问: http://localhost:8888/list/fib.txta, 将得到报错提示
尝试处理错误:
- 将panic去掉,进行处理
file, err := os.Open(fibPath)if err != nil {http.Error(writer, err.Error(),http.StatusInternalServerError) // codereturn}// 最后执行错误的 url, 结果将在 浏览器中显示,而不会在后台打印一堆错误信息 // 浏览器显示内容 open ../defer/fib.txta: The system cannot find the file specified.
- 缺点: 这样会将内部的出错信息,展示给用户。
将这些出错信息,包装成一个外部的 err。
- 看下新的目录结构:
filelistinglistinghandeler.go // 将函数处理的逻辑提取出来web.go // mian 函
- 将 http.HandleFunc() 所需要的匿名处理函数拿出来,放到一个单独的文件中(handleler.go), 并且只实现主要的逻辑即可, 将错误全部返回,交由外部处理。
- handeler.go: 正常处理逻辑,遇到问题直接返回错误。
package listingimport ("fmt""io/ioutil""net/http""os" )func HandFileList(writer http.ResponseWriter,request *http.Request) error {// 拿到list 后面具体的文件名,或者目录fileName := request.URL.Path[len("/list/"):] // /list/fib.txt, 通过切片,只要后面的。fib.txt// 打开文件fibPath := fmt.Sprintf("%s%s", "../defer/", fileName)file, err := os.Open(fibPath)if err != nil {return err}defer file.Close()// 读取文件all, err := ioutil.ReadAll(file)if err != nil {return err}// 内容写入到写入到 ResponseWriter 里去writer.Write(all)return nil }
- web.go: 定义一个函数类型 appHandler。
- 再 定义一个 errWrapper() 函数。 将处理函数作为参数传递,内部进行错误的统一处理。
- 看似很复杂,其实是 不熟悉go语言的语义。大白话解释一下,就是: handeler.go 是定义了 正常的处理逻辑的函数。 然后我们再定义一个函数,这个函数返回值是一个函数,函数内部调用完成了 handeler.go 的 业务处理函数。并接受了 返回来的异常, 下面对异常几种进行处理和判断。
package mainimport ("01/errhandling/filelisting/listing""net/http""os" )// 定义一个函数类型, 返回 error type appHandler func(writer http.ResponseWriter,request *http.Request) error// 再定义一个函数,功能是返回 HandleFunc()参数需要的函数 func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) {return func(writer http.ResponseWriter, request *http.Request) {err := handler(writer, request)if err != nil {code := http.StatusOKswitch {// 如果是文件不存在的错误case os.IsNotExist(err):http.Error(writer, http.StatusText(http.StatusNotFound), http.StatusNotFound)code = http.StatusNotFound// 没权限case os.IsPermission(err):code = http.StatusForbidden// 默认default:code = http.StatusInternalServerError}http.Error(writer,http.StatusText(code), code)} } }
// 实现一个显示文件的 webserver
func main() {
http.HandleFunc("/list/", errWrapper(listing.HandFileList)) // 处理逻辑函数,抽象出来
err := http.ListenAndServe(":8888", nil)
if err != nil {
panic(err)
}
}
4. panic 和 recover
panic
- 停止当前函数的执行(我觉得有点像Python中的 raise)
- 一直向上返回, 执行每一层的defer
- 如果没有遇见recover,程序就退出
- panic(中文:恐慌,恨)。也就是程序遇到问题很恐慌,很愤怒,不知道该做什么。所以panic是一个很 重 的词。一般来讲尽量少用。
recover
- 仅在defer调用中使用
- 可以获取panic的值
- 如果无法处理,可以重新panic
package mainimport ("fmt" )func tryRecover() {defer func() {r := recover() // func recover() interface{} 。可以看到recover() 是个任何类型if err, ok := r.(error); ok {fmt.Println("Error occurred: ", err)} else {panic(r)}}() // 定义匿名函数,并且调用b := 0a := 5 / bfmt.Println(a) }func main() {tryRecover() }// 结果 Error occurred: runtime error: integer divide by zero // 程序并不会异常终止
如果直接panic(123), 不是异常类型,那么 ,还将panic出去 123
func tryRecover() {defer func() {r := recover() // func recover() interface{} 。可以看到recover() 是个任何类型if err, ok := r.(error); ok {fmt.Println("Error occurred: ", err)} else {panic(r)}}() // 定义匿名函数,并且调用// b := 0// a := 5 / b// fmt.Println(a)panic(123) }// r的值 就是 123
5. 服务器统一出错处理2
学习了上面的 panic 和 recover 之后, 我们再来看一下
- 假如上面写函数处理逻辑的 和 主函数 的不是同一个人,那么加入main中是这样的
http.HandleFunc("/", errWrapper(listing.HandFileList)) // 把原先的 /list/ 改为了 /
- 那么handeler.go中的 处理函数还是以 /list/ 进行处理
fileName := request.URL.Path[len("/list/"):]
- 当输入 http://localhost:8888/abc 时,就会报错终端输出一大堆错误信息!并且显示该网页无法正常访问~(这不是我们想要的) 因为 切片操作超过索引长度。展示部分报错信息
2021/10/07 17:17:34 http: panic serving [::1]:65487: runtime error: slice bounds out of range [6:4] goroutine 6 [running]: net/http.(*conn).serve.func1(0xc00005ab40)D:/software/go/go1.15/src/net/http/server.go:1801 +0x147 panic(0x66a120, 0xc0000aa080)D:/software/go/go1.15/src/runtime/panic.go:975 +0x3e9 01/errhandling/filelisting/listing.HandFileList(0x6d9200, 0xc0000ae0e0, 0xc0000a8000, 0x0, 0x0)
- 此时,服务器并没有挂掉。当输入正确的 地址,仍能成功访问。 这就是 httpserver 做了**保护 ** 。在
D:/software/go/go1.15/src/net/http/server.go:1801 +0x147
这里:
func (c *conn) serve(ctx context.Context) {c.remoteAddr = c.rwc.RemoteAddr().String()ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())defer func() {if err := recover(); err != nil && err != ErrAbortHandler {const size = 64 << 10buf := make([]byte, size)buf = buf[:runtime.Stack(buf, false)]c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)}if !c.hijacked() {c.close()c.setState(c.rwc, StateClosed)}}()
- 可以看到进行了一个 recover(), err := recover() 然后进行了一些处理。
那么我们的代码 func errWrapper,也要加上一个 defer, recover。异常终止程序时进行操作。
func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) {return func(writer http.ResponseWriter, request *http.Request) {// !!!加上defer 处理异常defer func() {if r != nil {r := recover()log.Printf("Panic: %v", r)http.Error(writer,http.StatusText(http.StatusInternalServerError), // 返回服务器出错状态码http.StatusInternalServerError)}}()err := handler(writer, request)if err != nil {code := http.StatusOKswitch {// 如果是文件不存在的错误case os.IsNotExist(err):http.Error(writer, http.StatusText(http.StatusNotFound), http.StatusNotFound)code = http.StatusNotFound// 没权限case os.IsPermission(err):code = http.StatusForbidden// 默认default:code = http.StatusInternalServerError}http.Error(writer,http.StatusText(code), code)} } }
- 这时我们再访问 http://localhost:8888/abc 时, 浏览器就会显示: Internal Server Error。后台终端也只会打印: 2021/10/07 17:38:51 Panic: runtime error: slice bounds out of range [6:4]
后序还有主要内容,看不太懂,超出基础了,都不了解这些方法啥的。先放弃了。
第九章 :测试
- 跳过先
第十章:Goroutine
goroutine
- 轻量级“线程”
- 非抢占式 多任务处理, 由协程主动交出控制权
- 编译器/ 解释器/ 虚拟机层面的多任务
- 多个协程可能在一个或多个线程上运行
goroutine 模型:
- goroutine 和 物理线程不是 一一对应的。 它是很多个, map 到一个物理线程上去。下面会提到,即使开了1000个 goroutine, 也只会根据物理机的核数,开启相应的线程。因为物理线程是归操作系统管的。开销比较大,不受控制。物理线程是抢占式的多任务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nkzj62Vm-1635157982902)(./goroutine.png)]
初见
package mainimport ("fmt""time" )func main() {for i := 0; i < 10; i++ {go func(i int) {for {fmt.Printf("Hello from goroutine %d\n", i) // 打印属于io操作,这句话会进行阻塞,交出控制权}}(i)}// 睡眠10秒。 主程序 和 开的协程是一起执行的。当主程序执行完毕 协程还未执行,main()函数结束,整个程序就结束了time.Sleep(time.Millisecond*100) }
- go 关键字默认启动一个协程(Coroutine)去执行任务
- 在学python中,我们实现过 相关协程的内容:
- 1.生成器协程调度器: 将生成器 封装成 协程(通过装饰器),通过协程调度器,调度执行任务
- 2.协程调度器,epoll: 基于事件驱动的tcp网络服务器,协程调度器。将套接字封装为 协程适配器, 实现协程调度器(eventloop), 事件驱动编程在调度器中应用, 协程tcp网络服务器实现。
协程 Coroutine:
- 轻量级线程
- 非抢占式多任务处理,由协程主动交出控制权。(自己决定)
- 编译器(go)/解释器/虚拟机层面的多任务。(go编译器层面具有自己的调度器,不依赖操作系统调度)
- 多个协程可能在一个或者多个线程上运行。
将上面的程序改为一个数组(不存在io等操作,就不会进行切换,让出控制权)
package mainimport ("fmt""runtime""time" )func main() {var a [10]intfor i := 0; i < 10; i++ {go func() {for {a[i]++runtime.Gosched()}}()}// 睡眠10秒。 主程序 和 开的协程是一起执行的。当主程序执行完毕 协程还未执行,main()函数结束,整个程序就结束了time.Sleep(time.Millisecond)fmt.Println(a) }
两个数据冲突的地方:使用 go run -race goroutine.go, 查看数据冲突
- main函数的执行也相当于是一个协程。 内部函数等于使用外面的闭包变量 i。很可能main中 i 进 行到10了,才被 函数中使用,就会超出数组最大长度。所以要将 i 传递到匿名函数内部。
- 最后main进行了打印数组,但是 可能其他协程还在往数组中写入数据~ 这是另一个冲突,这个冲突需要使用 channel 进行处理。
任何函数只需要加上 go ,就能送给调度器执行
- 不需要再定义时区分是否是异步函数(针对Python, async def)
- 调度器会在合适的点进行切换
- 使用 -race 来检测数据访问冲突
goroutine 可能的切换点(非抢占式)
- I/O, select
- channel
- 等待锁
- 函数调用(有时)
- runtime.Gosched()
- 上述只是参考,不能保证不切换,不能保证在其他地方不切换
连续开启1000个协程, go语言会开启大概七个线程,而调度器会根据你的机器的核数,只有相应的一部分线程处于运行活跃状态。(top命令 linux系统中查看)
第十一章: Channel
1. channel 双向通道
channel :
goroutine 与 goroutine 之间的双向通道。
- 创建方式:
var c chan int // c == nil a := make(chan int) // 可以写为 a := make(chan int, 3) 加缓存,指定空间
- channel 和 函数 是一样的,都是一等公民, 可以作为函数参数和返回值。
var channels [10]chan int, 定义 数组,每个元素是一个channel
- 另外作为函数返回值、
func createWorker(id int) chan int {// 函数内创建 channelc := make(chan int)// 必须去 创建一个 协程,去处理 channel, 如果不把 for 循环定义到 func 里, 程序就死循环了! 一直等待接收数据。go func() {for {fmt.Println(<- c)}}()// 返回 channelreturn c }// createWorker 函数的作用就是创建返回一个 channel。 他没有做任何事情,真正的处理是在内部的 func 里。// 还可以规定方向, 外部的人只能收: 箭头标志 func worker(id int) chan <- int {}
c := make(chan int, 3) 规定大小,创建缓冲区,对性能是有一定得帮助的。可以累积导入三个 数值,才去切换,被其他协程接收。
还可以 停止发送: close(a) a为channel。 这是发送发使用 close(). 接收方 还可以接收数据, 不过接收的都是 0 (a chan int)。
- 接收者可以进行判断, 发送者是否调用了 close() , 接收数据使用 n, ok := <- c (c chan int). 通过 ok 来判断是否 close()
- 更通过的方法, 可以直接使用 range 循环。
for n := range a { // range 会自动判断 是否发送方调用close()fmt.Println(n)}
channel 的理论基础: Communication Sequential Process(CSP)
go语言作者的话: Don’t communicate by sharing memory; share memory by communicating.
翻译: 不要通过共享内存来进行通信; 通过通信来共享内存。
2. 使用channel 等待任务结束
前面当中的例子, 主程序中, 最后使用 time.Sleep() 来进行等待, 是 mian函数外的 写成任务进行执行。这种方式不是很好, 我们需要一个专业的方式,去等待 channel 任务结束, 然后结束 main() 函数。
- 上面的例子, 我们可以 发送两个 channel过去, 一个发送任务,一个接收任务。
// 定义channel var in chan int var done chan bool// 1.发送者 in <- 1 in <- 2 <- done <- done// 接收者 var n int n <- in n <- indone <- true done <- true
这样的写法,就不需要加上 Sleep() 了, 因为 发送者发送 in 完毕后, 还需要接收 接收者 done 发送的(没有实际接收), 这就导致了,需要等待 接收者发送数据, 从而实现了 等待任务的 接收者 in的任务结束了!
* 有了理论基础,下面就可以去 编写小例子, 串起来实践下了。```go package mainimport ("fmt" )func doWork(id int,in chan int, done chan bool) {// range 读取 in channel 数据for n := range in {// 处理 in 接收的数据fmt.Printf("Worker %d received %c\n",id, n)// 反馈信息, 已经处理完了done <- true} }type worker struct {in chan intdone chan bool }// 创建工作 func createWorker(id int) worker {w := worker {in: make(chan int),done: make(chan bool),}go doWork(id, w.in, w.done)return w }// 例子 func chanDemo() {// 创建十个 工人var workers [10]workerfor i := 0; i < 10; i++ {workers[i] = createWorker(i)}// 给每个工人分配任务, 传入一个值for i, worker := range workers {worker.in <- 'A' + i}// 给每个工人分配任务, 再传入一个值for i, worker := range workers {worker.in <- 'a' + i}// wait for all oh them, 统一等待所有的 done 的返回, 使上面的发送方 可以并发的去执行// 因为上面 每个 doWork接收两次值, 这里需要 同样就收两次for _, worker := range workers {<- worker.done<- worker.done} }func main() {chanDemo() }// 这样写并不会正确执行!!, 原因在于 dowork 中处理了 一个数据之后, 会 <- done, 而 chanDemo 里没有去即使处理这个 , 那么 dowork 会一直 卡在 <- done , 等着别人去接收, 而 chanDome 没有接收,反而又去 发送一组数据到 in <- . 导致双方等待,死锁。解决方式可以是 1. doWork 中: go func() {done <- true} // 再次创建一个协程,执行这句。2. 或者:设置缓存! createWorker() 函数中: w := worker {in: make(chan int),done: make(chan bool),}可以设置缓存, 当 done <- true 时,被立即等待 别人去收 w := worker {in: make(chan int),done: make(chan bool, 2), // 可以缓存}3. 或者是 实际业务不会有太复杂的并发。 这里可以把接收 分开, 不要一次等待接收,两批任务的结果chanDome() 中:// 给每个工人分配任务, 传入一个值for i, worker := range workers {worker.in <- 'A' + i}// 发送一组后, 立马等待结果for _, worker := range workers {<- worker.done}// 给每个工人分配任务, 再传入一个值for i, worker := range workers {worker.in <- 'a' + i}// 发送一组后, 立马等待结果for _, worker := range workers {<- worker.done}
使用 sync.WaitGroup 进行等待
var wg sync.WaitGroup // 提供三个方法 wg.Add() // 接收int 类型: 任务数 用在接收者 wg.Done() // 任务处理结束标志 用在处理者 wg.Wait() // 等待所有任务结束 用在处理者
将上述代码的 done channel 去掉改用 WaitGroup
package mainimport ("fmt""sync" )func doWork(id int,in chan int, wg *sync.WaitGroup) {// range 读取 in channel 数据for n := range in {// 处理 in 接收的数据fmt.Printf("Worker %d received %c\n",id, n)// 反馈信息, 已经处理完了wg.Done()} }type worker struct {in chan int// 添加 WaitGroupwg *sync.WaitGroup // 使用指针, 调用同一份。 不能是单独的 }
// 创建工作
func createWorker(id int, wg *sync.WaitGroup) worker {
w := worker {
in: make(chan int),
wg: wg,
}
go doWork(id, w.in, w.wg)
return w
}// 例子
func chanDemo() {
// 创建 WaitGroup
var wg sync.WaitGroup
// 创建十个 工人
var workers [10]worker
for i := 0; i < 10; i++ {
workers[i] = createWorker(i, &wg)
}// 我们下面设置的 一共20个任务
wg.Add(20)// 给每个工人分配任务, 传入一个值
for i, worker := range workers {
worker.in <- ‘A’ + i
// 除了一次性Add(20), 当不知道总的 任务数时, 可以再这里,每创建一个 Add(1)
}// 给每个工人分配任务, 再传入一个值
for i, worker := range workers {
worker.in <- ‘a’ + i
}// 等待
wg.Wait()
}func main() {
chanDemo()
}
3. channel 遍历树, 和 函数式递归对比
上代码
package mainimport "fmt"type TreeNode struct {Left *TreeNodeRight *TreeNodeVal int }//1. 使用函数式编程,遍历树. 中序遍历 func (node *TreeNode) TraverseFunc(f func(*TreeNode)) {if node == nil {return}node.Left.TraverseFunc(f)f(node)node.Right.TraverseFunc(f) }// 2. 使用 channel进行 数的遍历 // 和之前的例子一样, TraverseWithChannel() 相当于是 createWorker 的作用。创建一个 channel 并返回 func (node *TreeNode) TraverseWithChannel() chan *TreeNode {out := make(chan *TreeNode)// 开启goroutine,写入node节点go func() {node.TraverseFunc(func(node *TreeNode) {out <- node})// 关闭close(out)}()return out }func main() {// 0. 先进行创建 树root := TreeNode{nil, nil, 1}root.Left = &TreeNode{nil, nil, 5}root.Right = &TreeNode{Val: 4}root.Right.Left = new(TreeNode)root.Left.Right = &TreeNode{nil, nil, 3}// 1. 调用 函数式遍历,f 匿名函数,统计节点个数count := 0root.TraverseFunc(func(node *TreeNode) {count++fmt.Println(node.Val)})fmt.Println("Tree has Node count: ", count)// 2. 通过channelc:= root.TraverseWithChannel()// 通过range 就可以遍历节点了, 统计元素值最大的maxNodeVal := 0for node := range c {if node.Val > maxNodeVal {maxNodeVal = node.Val}}fmt.Println("Max Node Value: ", maxNodeVal) }// 这里有些像 Python中的 yield。 out <- node , 之后切换, range 里拿到后, 执行再切换。 // go语言的channel 可以做更加复杂的事情, 不过这里就有些相当于 yield 的功能了
4. select 进行调度
老师讲的例子比较复杂, 还讲到了计时器, 是个很有用的场景例子。 我只是听了听后面的。
package mainimport ("fmt""math/rand""time" )// 创建channel func generator() chan int {out := make(chan int)i := 0go func() {for {// 随机sleep 1500毫秒time.Sleep(time.Duration(rand.Intn(1500)) *time.Millisecond)out <- ii++}}()return out }
func main() {
var c1, c2 = generator(), generator()
// 假如我们想从 c1,c2 收数据, 并且谁来的快 收谁,就需要使用 select 了
//n1 := <- c1
//n2 := <- c2// 就好像是做了一个非阻塞式的处理~ 立即返回结果
for {
select {
case n := <- c1:
fmt.Println("Receive from c1: ", n)
case n := <- c2:
fmt.Println("Receive from c2: ", n)
//default: // 不加 default 就会陷入阻塞,等待数据中
// fmt.Println(“No value received”)
}
}
}总结:
- Select 的使用
- 定时器的使用
- 在Select 中使用 nil Channel
5. 传统的同步机制
上面提到的复杂的例子中, 没有使用任何锁,都是通过channel进行通信共享数据的。这就是 CSP机制。
也有传统的机制, Mutex, Cond, WaitGroup也算的是
Mutex (互斥量)
package mainimport ("fmt""sync""time" )// 制作原子操作的 int类型。注意: 系统是有 atomic 的原子操作的。atomic.AddInt32() 等,保证"线程安全", // go中就是 在多个并发执行的 goroutine中 保证安全 type atomicInt struct {val intlock sync.Mutex }// 构建两个函数 func (a *atomicInt) increment() {// 用锁来保护起来a.lock.Lock()defer a.lock.Unlock()a.val++}func (a *atomicInt) get() int {// 加锁解锁a.lock.Lock()defer a.lock.Unlock()return a.val }func main() {var a atomicInta.increment()go func() {a.increment()}()time.Sleep(time.Millisecond)fmt.Println(a.get()) }
g 语言中很少使用传统的 同步机制, 尽量使用 channel 进行通信。 传统的同步机制都是 通过共享内存进行通信的, 需要加锁去保护。
第12章: 一个迷宫游戏,算法例子
有一个文件:
// maze.in 其中第一行是 文件的行列数。 第二行空格隔开。 // 0 表示路, 1表示墙。 随机给 起点(start) 和 终点(end)。 标记路径6 5 0 1 0 0 0 0 0 0 1 0 0 1 0 1 0 1 1 1 0 0 0 1 0 0 1 0 1 0 0 0
代码实现
package mainimport ("fmt""os" )// 1. 从文件读取并返回 二维数组 func readMaze(filename string) [][]int {// 打开文件file, err := os.Open(filename)if err != nil {panic(err)}// 读取二维数组的 行列var row, col intfmt.Fscanf(file, "%d %d", &row, &col) // 要加上取地址, 值传递改不了外界的maze := make([][]int, row) // 创建二维数组,实际上是一维数组, 元素又是一维数组for i := range maze {maze[i] = make([]int, col)for j := range maze[i] { // 读取索引fmt.Fscanf(file, "%d", &maze[i][j])}}return maze }// 定义坐标点的表示 type point struct {i, j int }// 定义 上左下右的 方向 var dirs = [4]point {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}// 实现 坐标相加的方法 func (p point) add(r point) point{ // 使用值类型, 返回新的return point{p.i + r.i, p.j + r.j} }// 判断 point 位置是否越界 func (p point) at(grid [][]int) (int, bool) {if p.i < 0 || p.i >= len(grid) {return 0, false}if p.j < 0 || p.j >= len(grid[p.i]) {return 0, false}return grid[p.i][p.j], true }func walk(maze [][]int, start, end point) [][]int{// 建立坐标路线steps := make([][]int, len(maze))for i := range steps {steps[i] = make([]int, len(maze[i]))}// 广度优先求解// 1. 建立队列Q := []point {start}for len(Q) > 0 {// 拿到队头元素cur := Q[0]Q = Q[1:]// 获取元素的位置curSteps, _ := cur.at(steps)// 走到终点了:if cur == end { break }// 上下左右走for _, dir := range dirs {// 获取新节点位置next := cur.add(dir)// 判断该位置是否合法, 越界或者撞墙val, ok := next.at(maze)if !ok || val == 1 { continue }// 判断这个点是否走过了val, ok = next.at(steps)if !ok || val != 0 { continue }// 等于原点也不行if next == start {continue}// 下面开始做事, 将下一元素 步数记录,然后添加进Q队列steps[next.i][next.j] =curSteps + 1Q = append(Q, next)}}return steps }func main() {// 获取二维 slicemaze := readMaze("maze/maze.in")// 打印//for _, row := range maze {// for _, num := range row {// fmt.Printf("%d ", num)// }// fmt.Println()//}// 走迷宫steps := walk(maze, point{0, 0}, point{2,2})// 打印结果for _, row := range steps {for _, num := range row {fmt.Printf("%4d", num) // %4d 最后输出占三位}fmt.Println()} }
第十三章: 标准库
http库
go语言作为一门面向网络,面向服务的, 高并发编程语言, 对 http 的封装是非常良好的。
可以作为服务器
也可以作为客户端: 爬虫,请求
- 使用http客户端发送请求
- 使用http.Client 控制请求头部等
- 使用httputil简化工作
package mainimport ("fmt""net/http""net/http/httputil" )func main() {request, err := http.NewRequest(http.MethodGet, "http://www.imooc.com", nil)request.Header.Add("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")// 自定义 Clientclient := http.Client{CheckRedirect: func(req *http.Request,via []*http.Request) error {fmt.Println("Redirect: ", req)return nil},}resp, err := client.Do(request)//resp, err := http.DefaultClient.Do(request)//resp, err := http.Get("http://www.imooc.com")if err != nil {panic(err)}defer resp.Body.Close()s, err := httputil.DumpResponse(resp, true) // 一次性解析出来if err != nil {panic(err)}fmt.Printf("%s\n", s) }
http 服务器性能分析
- inport _ “net/http/pprof” 这个不是在程序中的,在终端。 所以加个 _ 表示使用
- 访问 /debug/pprof
- 使用 go tool pprof
2. 其他标准库
bufio
log
encoding/json
regexp
time
- time.Sleep()
- 计算时间等
- 产生channel ,计时器等
strings/ math/ rand
如何查看标准库的文档
自己起一个服务器
- godoc -http:8888 (我启动不了)
https://studygolang.com/pkgdoc
创建一个队列: https://studygolang.com/pkgdoc
字符串转数字,数字转字符串: https://blog.csdn.net/weixin_34185560/article/details/91438265
字符串拼接(删除其中一个字符)
st := "12345678" st = st[:2] + st[3:8] fmt.Println(st)// 1245678
// 拼接
rst := []string {“1”, “2”, “3”}
fmt.Println(strings.Join(rst, “,”))// 1,2,3
- 一些小的总结
package mainimport "fmt"type a struct {b int
}type w struct {s *a
}func index(x int) { // 1. 定义参数可以不使用fmt.Println(123)
}func main() {index(2) // 定义参数可以不使用aa := a{1}w := w{&aa}w.s.b = 3 // 2. 结构体也是值传递, 需要使用指针传递,才是同一份fmt.Println(w.s) // 结果 &{3}fmt.Println(aa) // 结果 {3}
}
[][]int{
// 建立坐标路线
steps := make([][]int, len(maze))
for i := range steps {
steps[i] = make([]int, len(maze[i]))
}
// 广度优先求解
// 1. 建立队列
Q := []point {start}
for len(Q) > 0 {// 拿到队头元素cur := Q[0]Q = Q[1:]// 获取元素的位置curSteps, _ := cur.at(steps)// 走到终点了:if cur == end { break }// 上下左右走for _, dir := range dirs {// 获取新节点位置next := cur.add(dir)// 判断该位置是否合法, 越界或者撞墙val, ok := next.at(maze)if !ok || val == 1 { continue }// 判断这个点是否走过了val, ok = next.at(steps)if !ok || val != 0 { continue }// 等于原点也不行if next == start {continue}// 下面开始做事, 将下一元素 步数记录,然后添加进Q队列steps[next.i][next.j] =curSteps + 1Q = append(Q, next)}
}return steps
}
func main() {
// 获取二维 slice
maze := readMaze(“maze/maze.in”)
// 打印
//for _, row := range maze {
// for _, num := range row {
// fmt.Printf("%d ", num)
// }
// fmt.Println()
//}
// 走迷宫
steps := walk(maze, point{0, 0}, point{2,2})
// 打印结果
for _, row := range steps {for _, num := range row {fmt.Printf("%4d", num) // %4d 最后输出占三位}fmt.Println()
}
}
## 第十三章: 标准库### http库* go语言作为一门面向网络,面向服务的, 高并发编程语言, 对 http 的封装是非常良好的。* 可以作为服务器* 也可以作为客户端: 爬虫,请求* 使用http客户端发送请求
* 使用http.Client 控制请求头部等
* 使用httputil简化工作```go
package mainimport ("fmt""net/http""net/http/httputil"
)func main() {request, err := http.NewRequest(http.MethodGet, "http://www.imooc.com", nil)request.Header.Add("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")// 自定义 Clientclient := http.Client{CheckRedirect: func(req *http.Request,via []*http.Request) error {fmt.Println("Redirect: ", req)return nil},}resp, err := client.Do(request)//resp, err := http.DefaultClient.Do(request)//resp, err := http.Get("http://www.imooc.com")if err != nil {panic(err)}defer resp.Body.Close()s, err := httputil.DumpResponse(resp, true) // 一次性解析出来if err != nil {panic(err)}fmt.Printf("%s\n", s)
}
http 服务器性能分析
- inport _ “net/http/pprof” 这个不是在程序中的,在终端。 所以加个 _ 表示使用
- 访问 /debug/pprof
- 使用 go tool pprof
2. 其他标准库
bufio
log
encoding/json
regexp
time
- time.Sleep()
- 计算时间等
- 产生channel ,计时器等
strings/ math/ rand
如何查看标准库的文档
自己起一个服务器
- godoc -http:8888 (我启动不了)
https://studygolang.com/pkgdoc
创建一个队列: https://studygolang.com/pkgdoc
字符串转数字,数字转字符串: https://blog.csdn.net/weixin_34185560/article/details/91438265
字符串拼接(删除其中一个字符)
st := "12345678" st = st[:2] + st[3:8] fmt.Println(st)// 1245678
// 拼接
rst := []string {“1”, “2”, “3”}
fmt.Println(strings.Join(rst, “,”))// 1,2,3
- 一些小的总结
package mainimport "fmt"type a struct {b int
}type w struct {s *a
}func index(x int) { // 1. 定义参数可以不使用fmt.Println(123)
}func main() {index(2) // 定义参数可以不使用aa := a{1}w := w{&aa}w.s.b = 3 // 2. 结构体也是值传递, 需要使用指针传递,才是同一份fmt.Println(w.s) // 结果 &{3}fmt.Println(aa) // 结果 {3}
}
- fmt.Printf("%p") 可以打印内存地址。
go语言基础学习13章节详细总结欢迎一起入门学习相关推荐
- 新手入门学习SAP,SAP初级实施顾问入门学习难点重点及建议
1.SAP模块选择哪个比较好 答: SAP顾问的待遇在IT行业来说算是高等待遇了.金领行业嘛 至于模块的话,如果笼统的来说的话,FICO模块比较好,因为FICO工资待遇高,而且市场需求量大,一般好多其 ...
- PHP语言基础知识(超详细)
文章目录 前言 第一章 PHP语言学习介绍 1.1 PHP部署安装环境 1.2 PHP代码工具选择 第二章 PHP代码基本语法 2.1 PHP函数知识介绍 2.2 PHP常量变量介绍 2.2.1 PH ...
- C语言基础之13:文件输入/输出
Tips1: 函数:fopen().getc().putc().exit().fclose() fprintf().fscanf().fgets().fputs() rewind().fseek(). ...
- python语言基础与应用章节七_专技天下Python语言基础与应用章节测试答案
[单选] 价值工程中所述的"价值"是指作为某种产品所具有的功能与获得该功能的全部费用的比值.它是对象的(),是作为评价事物有效程度的一种尺度提出来的. [单选] 不必要功能就是不符 ...
- linux安装autossh详细教程,Linux系统入门学习:如何安装autossh
提问: 我打算在Linux上安装autossh,我应该怎么做呢? autossh 是一款开源工具,可以帮助管理SSH会话.自动重连和停止转发流量.autossh会假定目标主机已经设定无密码SSH登陆, ...
- 【Spring学习】IoC、DI、AOP入门学习
新建一个普通的java项目,添加spring功能(3.x),不要使用MyEclipse提供的jar包,自己手动添加需要的jar包(commons-logging.jar.spring.jar): Io ...
- Linux学习总结(61)——Ansible 快速入门学习
一.基本概述 ansible是一款开源自动化工具,基于python paramiko模块开发,分布式,无需客户端 官网网址:https://www.ansible.com 官方文档:https://d ...
- Docker学习总结(42)——Docker Compose 入门学习
一.Docker Compose介绍 Compose 是 Docker 公司推出的一个工具软件,可以管理多个 Docker 容器组成一个应用(称为一个 project,即项目).Compose 定位是 ...
- 原典法的学习训练程序详细介绍
原文:https://www.douban.com/group/topic/38822800/ 原典法的学习训练程序详细介绍: 原典英语学习法简略概括包括两个方面:在学习过程上遵循人类语言获得的本原程 ...
最新文章
- oracle 被另一个用户锁定
- 用身体给可穿戴设备充电,网友:离人类被当作电池更近一步!
- 蛮力法求最大字段和时间复杂度_只需十个字,让你掌握推拿手法的秘诀
- python基础教程(第二版)
- 音视频技术开发周刊 | 208
- 关于SimpleDateFormat时间转换总是显示1970年的问题
- mybatis枚举自动转换(通用转换处理器实现)
- 大数据计算服务MaxCompute行业开发应用案例
- 【leetcode】Max Points on a Line(hard)☆
- 强连通分量(Tarjan)模板
- 如何重启 Windows 10 子系统(WSL) ubuntu
- istio搭建testbed 补充(指令汇集)
- 《传统相声开场小唱》
- oracle wire protocol,WMB 8.0.0.2 在linux下面怎么配置连接ORACLE数据库
- 如何写好一个打动投资人的计划书
- win7虚拟机使用nat模式配置静态IP上网
- 【题解】百度2020校招Web前端工程师笔试卷(第二批):单选题、多选题
- HTML5——如何在网页中加入图片和超链接。
- Interactron | 体现自适应的目标检测器
- OpenStack云平台的管理
热门文章
- 什么是vuex?vuex的工作流程?
- Android-33源码分析: 系统启动流程
- PHP语言之数组 educoder答案
- C# Color颜色RGB对照表、颜色选择器
- 高速摄影测量 照相机定标
- 一个人住230平米的黎贝卡是败家娘们吗?
- AVALON-MM接口
- 聚英国际|【燎原计划启动周第三期AMA】大陆节点数据中心揭秘!
- 代码执行覆盖率工具Emma、cobertura简单比较
- 写定时任务发送邮件报错(Could not connect to SMTP host:smtp.exmail.qq.com,port:465)