# learngo **Repository Path**: lihaowen2017/learngo ## Basic Information - **Project Name**: learngo - **Description**: go语言学习笔记,及珍爱网爬虫案例 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 2 - **Created**: 2019-12-05 - **Last Updated**: 2023-03-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Go语言学习 ### Go语言最适合的应用场景 为大数据、微服务、并发而生的通用编程语言 ### Go语言与其它语言的区别 没有"对象",没有继承多态,没有泛型,没有try/catch 有接口,函数式编程,CSP并发模型(goroutine+channel) ### 课程框架 #### 基本语法 变量;选择、循环;指针、数组、容器 #### 面向接口 结构体、duck typing的概念、组合的思想 #### 函数式编程 闭包的概念 #### 工程化 资源管理、错误处理、测试和文档、性能调优 #### 并发编程 goroutine 和 channel、理解调度器 #### Go搭建简单的分布式爬虫 爬取相亲网站资料 #### Go语言的安装与开发环境 自行百度 ## 内置函数积累 #### 文件读取内置函数 ```go // 读取文件的库函数返回两个值,([]byte, error) 文件内容和出错信息 contents, err := ioutil.ReadFile(filename) // reflect.ValueOf(op)反射获得函数真正的值,Pointer()获得函数的指针 reflect.ValueOf(op).Pointer() // 获得函数名 runtime.FuncForPC(p).Name() // 获得 math.Pow(float64(i), float64(i2)) ``` ## 坑 > 在src文件夹的源码中使用go install 安装 ``` go: golang.org/x/net@v0.0.0-20190620200207-3b0461eec859: Get https://proxy.golang.org/golang.org/x/net/@v/v0.0.0-20190620200207-3b0461eec859.mod: dial tcp 172.217.24.17:443: connectex: A connection attempt fail ed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond. ``` > go env -w GOPROXY=https://goproxy.cn copy是值拷贝,等号是指针拷贝,copy必须要先创建目标数组的内存 ## Go基础语法 ### 变量定义 #### 要点 >变量类型写在变量名之后 >编译器可以推测变量类型 >没有char只有rune >原生支持复数类型 #### 使用var关键字 ```go var a, b, c bool // 不赋初值 var s1, s2 string = "hello", "world" // 可放在函数内, 可放在包内(感觉像全局变量其实不是) // 使用var()集中定义变量 ``` #### 编译器自动决定类型 ```go var a, b, c, s = 3, 4, true, "def" // 推断变量类型 ``` #### 使用:=定义变量 ```go // 该用法只能在函数内变量定义使用而不能在包内变量定义使用 func variableShorter() { a, b, c, s := 3, 4, true, "def" // 第一次定义变量必须使用:= 来定义 b = 5 // 后续的变量赋值可以直接使用 = fmt.Println(a, b, c, s) } ``` ### 内建变量类型 ```go // bool, string // (u)int, (u)int8, (u)int16, (u)int32, (u)int64, uintptr // u是带符号的,int一类是根据操作系统位数来确定长度,一类是自定义位数,uintptr指针 // byte, rune(字符型等价于char类型;32位4字节) // float32, float64, complex64, complex128 浮点数;复数(实部虚部各一半) ``` ### 强制类型转换 ```go // 类型转换是强制的 func triangle() { var a, b int = 3, 4 var c int // c为整形的 c = int(math.Sqrt(float64(a * a + b * b))) // 类型转换是强制的, Sqrt必须接受float类型 fmt.Println(c) } ``` ### 常量的定义 ```go // 同var变量一样可以写在函数内也可以写在函数外。 func consts() { const filename = "abc.txt" // const 数值可以作为各种类型使用,替换 const a, b = 3, 4 var c int c = int(math.Sqrt(a*a + b*b)) // 此处无需将a,b转换为float即可直接使用 fmt.Println(filename, c) } const file = "123.txt" const( file1 = "123" file2 = "1234" ) ``` ### 枚举型常量 #### 普通枚举型常量 ```go func enum() { const( cpp = 0 java = 1 python = 2 golang = 3 ) // go的枚举类型,直接赋值(普通枚举类型) // iota作为自增值表达式的种子来定义枚举类型 // go的枚举类型,自增型 const( b = 1 << (10 * iota) kb mb gb tb pb ) fmt.Println(b, kb, mb, gb, tb, pb) fmt.Println(cpp, java, python, golang) // 1 1024 1048576 1073741824 1099511627776 1125899906842624 // 0 1 2 3 } ``` ### 程序控制 > for, if 后面的条件没有括号 > if 条件里也可以定义变量 > 没有while > switch不需要break,也可以直接switch多个条件 #### if 条件查询 >if的条件里可以赋值 >if的条件里赋值的变量作用域就在这个if语句里 ##### if条件语句的第一种写法 ```go contents, err := ioutil.ReadFile(filename) if err != nil { fmt.Println(err) } else { fmt.Printf("%s\n", contents) } ``` ##### if 条件语句的第二种写法 ```go // 与第一种不同,第二种写法将变量定义在if块中 if contents, err := ioutil.ReadFile(filename); err != nil { fmt.Println(err) } else { fmt.Printf("%s\n", contents) } ``` #### switch ```go // switch后加表达式 func eval(a, b int, op string) int { var result int switch op { // switch会自动break,除非使用fallthrough case "+": result = a + b case "-": result = a - b case "*": result = a * b case "/": result = a / b default: panic("unsupported operator:" + op) } return result } ``` ```go // switch后不加表达式 // switch中case的值 不允许是浮点型 func grade(score int) string { g := "" switch { case score < 0 || score > 100: panic(fmt.Sprintf("Wrong score: %d", score)) // 抛出异常 case score < 60: g = "F" case score < 80: g = "C" case score < 90: g = "B" case score <= 100: g = "A" } return g } ``` #### for 循环 ```go // for的条件里不需要括号 // for的条件里可以省略初始条件,结束条件,递增表达式 func forTest() { sum := 0 // 初始条件;结束条件;递增表达式 for i := 1; i <= 100; i++ { sum += i } fmt.Println(sum) } ``` ```go // 省略初始条件,相当于while func converToBin(n int) string { result := "" if n == 0 { result = "0" } else { // 起始条件,终止条件,递增表达式 // 省略初始条件 for ; n > 0; n /= 2{ lsb := n % 2 // 最低位 result = strconv.Itoa(lsb) + result // strconv.Itoa整数转字符串 } } return result } ``` ```go // 省略初始条件和递增条件,也相当于while func printFile(filename string) { file, err := os.Open(filename) if err != nil { panic(err) } scanner := bufio.NewScanner(file) // 省略初始条件和递增表达式,类似于while循环,go语言中没有while循环 for scanner.Scan() { fmt.Println(scanner.Text()) } } ``` ```go // 死循环 func forever() { // 省略起始条件,终止条件,递增表达式,死循环 for { fmt.Println("abc") } } ``` ### 函数 > 函数定义与变量名定义类似 函数名在前返回值类型在后 > func eval(a,b int, op string) int > 函数返回多个值时可以起名字 > 仅用于非常简单的函数 > 对于调用者而言没有区别,仅提示在接受返回值时名字可以使用什么 > 函数可以作为参数 > 函数没有默认参数,可选参数 > 可以使用可变参数列表 #### 函数多返回值 ```go // 13 / 3 = 4 ... 1 // 函数多返回值 func div(a, b int) (q, r int) { //return a / b, a % b q = a / b r = a % b return } ``` #### 嵌套函数 ```go // 嵌套函数 func apply(op func(int, int) int, a, b int) int { // reflect.ValueOf(op)反射获得函数真正的值,Pointer()获得函数的指针 p := reflect.ValueOf(op).Pointer() opName := runtime.FuncForPC(p).Name() // 获得函数名 fmt.Printf("Calling function %s with args " + "(%d, %d)\n", opName, a, b) return op(a, b) } ``` #### 可变参数列表 ```go // 可变参数列表 func sum(numbers ...int) int { s := 0 for i := range numbers { s += numbers[i] } return s } ``` #### 匿名函数作为函数参数 ```go fmt.Println(apply( // 匿名函数 Calling function main.main.func1 with args (3, 4) // main 包名, main 主函数的main, func1 匿名函数的系统命名 func(i int, i2 int) int { return int(math.Pow(float64(i), float64(i2))) }, 3, 4)) ``` ### 指针 > 指针不能运算 ```go var a int = 2 var pa *int = &a *pa = 3 fmt.Println(a) ``` #### 参数传递 > 值传递?引用传递? ```c++ void pass_by_val(int a) { a++; } void pass_by_ref(int& a) { a++; } int main() { int a = 3; // 值传递,main函数的a的值没有变,而是将main中的a拷贝一份传递给pass_by_val // 拷贝中的a增1,main函数的a未改变 // 值传递相当于做了一份copy pass_by_val(a); printf("After pass_by_val: %d\n", a); // 3 // 引用同一个变量 // 引用传递 pass_by_ref(a); printf("After pass_by_ref: %d\n", a); // 4 } ``` >Go语言只有值传递一种方式 #### 交换两个值 ```go // 值传递,错误方法 func swapVal(a, b int) { b, a = a, b } // 利用指针进行引用传递 func swapPtr(a, b *int) { *b, *a = *a, *b } // 利用返回值来实现交换 func swapReturn(a, b int) (int, int) { return b, a } func main() { a, b := 3, 4 swapVal(a, b) // 无法交换,值传递 swapPtr(&a, &b) // 传入两个地址,成功交换 // 外部使用变量接返回值 a, b = swapReturn(a, b) fmt.Println(a, b) } ``` ### 数组,切片和容器 #### 数组定义 ```go // 数组数量写在类型的前面 var arr1 [5]int arr2 := [3]int{1, 3, 5} arr3 := [...]int{2, 4, 6, 8, 10} // 编译器自动识别数组数量 var grid [4][5]int // 4行5列的int二维数组 ```` #### 数组的遍历 ```go // 使用len获取数组长度进行遍历 for i := 0; i < len(arr3); i++ { fmt.Println(arr3[i]) } ``` ```go for i := range arr3 { // range关键字获得数组的下标, 取到元素 fmt.Println(arr3[i]) } for i, v := range arr3 { // range关键字获得数组的下标, range关键字可以获得下标和元素值 fmt.Println(i, v) } for _, v := range arr3 { // 只获取值不要下标 fmt.Println(v) } ``` ##### 获取最大的下标及元素 ```go func max(numbers ...int) (int, int){ maxi := -1 maxValue := -1 for i, v := range numbers { if v > maxValue { maxi, maxValue = i, v } } return maxi, maxValue } ``` ##### 为什么要用range > 意义明确,美观 > c++:没有类似能力 > Java/Python:只能for each value, 不能同时获取 i, v #### 数组是值类型 > [10]int 和 [20]int 是不同类型,长度不同的数组go语言会认为是不同类型 > 调用func f(arr [10]int) 会拷贝数组 > 在go语言中一般不直接使用数组 ### 切片(Slice) > 切片作为数组的视图 > 切片作为一个独立类型进行操作 > 切片上操作能够看作直接对数组的引用进行操作 ```go func updateSlice(s []int) { // slice是原本数组的视图,即为对数组部分的直接操作 // []里无数字是切片,切片相当于数组的视图 // 函数传递进来一个切片 s[0] = 100 } func printArraySlice(arr []int) { // 修改数组的第一个元素 arr[0] = 100 for i, v := range arr { fmt.Println(i, v) } } ``` ####slice的实现 > 切片的组成分为三部分,首元素ptr指针,切片长度len,切片对应数组的长度减去首指针的下标即为cap capacity 容量 > 切片[起始位置:终止位置] 终止位置不能超过cap > [下标] 下标不能超过len #### slice的扩展 ```go func extendingSlice() { arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7} fmt.Println(arr) s1 := arr[2:6] // [2 3 4 5] s2 := s1[3:5] // [5 6] [s1[3], s1[4]] // cap capacity 容量 fmt.Printf("s1=%v, len(s1)=%d, cap(s1)=%d\n",s1, len(s1), cap(s1)) fmt.Printf("s2=%v, len(s2)=%d, cap(s2)=%d\n",s2, len(s2), cap(s2)) } ``` > s1的值为[2 3 4 5], s2的值为[5 6] > slice可以向后扩展,不可以向前扩展 > s[i]不可以超越len(s),向后扩展不可以超越底层数组cap(s) #### 向Slice添加元素 > 添加元素时如果超越cap,系统会重新分配更大的底层数组 > 由于值传递的关系,必须接收append的返回值 > s = append(s, val) ```go func appendSlice() { arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7} s1 := arr[2:6] s2 := s1[3:5] s3 := append(s2, 10) s4 := append(s3, 11) // view 一个新arr len超过caps,系统会自动分配一个新数组,caps翻倍 s5 := append(s4, 12) // view 一个新arr fmt.Println(s3, s4, s5, arr) } ``` #### 创建slice ```go func createSlice() { var s [] int // 定义一个slice, Zero value for slice is nil for i := 0; i < 100; i++ { printSlice(s) // len超过caps,系统会自动分配一个新数组,caps翻倍 s = append(s, 2 * i + 1) } fmt.Println(s) s1 := []int{2, 4, 6, 8} // 建立array,建立slice去view这个array printSlice(s1) s2 := make([]int, 16) // 定义slice长度固定,值不固定 s3 := make([]int, 10, 32) // 定义slice长度为10,cap为32 printSlice(s2) printSlice(s3) } ``` #### 复制slice ```go func copySlice() { s1 := []int{2, 4, 6, 8} // 建立array,建立slice去view这个array printSlice(s1) s2 := make([]int, 16) // 定义slice长度固定,值不固定 printSlice(s2) copy(s2, s1) printSlice(s2) } ``` #### 删除slice的元素 > go语言中Slice没有delete内置函数,使用拼接来实现删除 ```go func deleteSlice() { s1 := []int{2, 4, 6, 8, 0, 0, 0} // 建立array,建立slice去view这个array s1 = append(s1[:2], s1[3:]...) // 利用append进行删除,s1[3:]... 下标3之后的元素 printSlice(s1) fmt.Println("Popping from front") front := s1[0] s1 = s1[1:] fmt.Println(front) fmt.Println(s1) fmt.Println("Popping from back") tail := s1[len(s1) - 1] s1 = s1[:len(s1) - 1] fmt.Println(tail) fmt.Println(s1) } ``` ### Map #### 创建Map ```go m := map[string]string { "name": "lihaowen", "course": "golang", "site": "qianfeng", "quality": "notbad", } m2 := make(map[string]int) // 空map定义 m2 == empty map var m3 map[string]int // m3 == nil ``` #### 获取元素 > 获取元素:m[key] > 判断key是否存在,使用两个值来接,courseName, ok := m["course"],利用第二个值来判断 > 获取不存在的key时会打印ZeroValue ```go func getValues(m map[string]string) { courseName, ok := m["course"] fmt.Println(courseName, ok) if causeName, ok := m["cause"]; ok { // 不存在的key时会打印ZeroValue-这里是空串 fmt.Println(causeName, ok) } else { fmt.Println("key does not exist") } } ``` #### 删除元素 > delete内建函数直接删除 ```go func deleteMap(m map[string]string) { name, ok := m["name"] fmt.Println(name, ok) delete(m, "name") name, ok = m["name"] fmt.Println(name, ok) } ``` #### Map的遍历 > 使用range遍历key,或者遍历key,value对 > 不保证遍历顺序,如需顺序,需手动对key排序(将key放到slice中进行排序再拿出来) > 使用len获得元素个数 ```go func traverseMap(m map[string]string) { for k, v := range m { // 单取 k,v 与数组遍历一致 fmt.Println(k, v) } } ``` #### map的key > map使用哈希表,必须可以比较相等 > 除了slice,map,function的内建类型都可以作为key > Struct类型不包含上述字段,也可作为key #### 例:寻找最长不含有重复字符的子串 ```go // 对于每一个字母x // lastOccured[x]不存在,或者无需操作 // lastOccured[x] >= start -> 更新start更新lastOccurred[x], 更新maxLength func lengthOfNonRepeatingSubStr(s string) int { lastOccured := make(map[byte]int) start := 0 maxLength := 0 for i, ch := range []byte(s) { fmt.Println(i) if LastI, ok := lastOccured[ch]; ok && LastI >= start { start = lastOccured[ch] + 1 fmt.Println("start", start) } if i - start+1 > maxLength { maxLength = i - start + 1 fmt.Println("max",maxLength) } lastOccured[ch] = i } return maxLength } ``` ### 字符和字符串的处理 ```go func main() { s := "Yes我爱慕课网!" fmt.Println("byte长度",len(s)) //fmt.Printf("%X\n", []byte(s)) for _, b := range []byte(s) { fmt.Printf("%X ", b) } fmt.Println() for i, ch := range s { // ch is a rune fmt.Printf("(%d %X)", i, ch) } fmt.Println() fmt.Println(utf8.RuneCountInString(s)) // 获得字符数量 bytes := []byte(s) for len(bytes) > 0 { ch, size := utf8.DecodeRune(bytes) // byte解码为rune bytes = bytes[size:] fmt.Printf("%c ", ch) } fmt.Println() for i, ch := range []rune(s) { // 转为rune类型的切片 fmt.Printf("(%d %c)", i, ch) } fmt.Println() // strings 包对字符串的操作 s1 := "1,2,3,4,5" res := strings.Split(s1,",") // 切片 fmt.Println(res[1]) } ``` #### rune相当于go的char > 使用range遍历pos,rune对 > 使用utf8.RuneCountInString获得字符数量 > 使用len获得字节长度 > 使用[]byte获得字节 #### 其他字符串操作 > Fields, Split, Join > Contains,Index > ToLower, ToUpper > Trim, TrimRight, TrimLeft ### 面向对象 > go语言仅支持封装,不支持继承和多态 > go语言没有class只有struct #### 结构体和方法 ##### 结构的创建 > 不论地址还是结构本身,一律使用 . 来访问成员 ##### 为结构定义方法 > 显示定义和命名方法接收者 > 使用指针作为方法的接收者 > 只有使用指针才可以改变结构内容 > nil指针也可以调用方法 ##### 值接收者和指针接收者 > 要改变内容必须使用指针接收者 > 结构过大也考虑使用指针接收者 > 一致性:如有指针接收者,最好都是指针接收者 > 值接收者是go语言特有 > 值/指针接收者均可接受值/指针 ```go type treeNode struct{ value int left, right *treeNode } func (node *treeNode) traverse() { // 中序遍历 if node == nil { return } node.left.traverse() node.print() node.right.traverse() } func createNode(value int) *treeNode { // 使用自定义工厂函数 // 注意返回了局部变量地址 return &treeNode{value: value} } func (node treeNode) print() { // 函数名前定义接收者 fmt.Print(node.value," ") } func (node *treeNode) setValue(value int) { if node == nil { fmt.Println("Setting value to nil node. Ignored!") return // node是nil不可能拿到value必须return } node.value = value } func main() { // 树结构的定义 var root treeNode root = treeNode{value: 3} root.left = &treeNode{} root.right = &treeNode{5, nil, nil} root.right.left = new(treeNode) root.left.right = createNode(2) nodes := []treeNode { {value: 3}, {}, {6, nil, &root}, } fmt.Println(nodes) fmt.Println(root) root.right.left.setValue(4) root.right.left.print() //root.left.right.print() pRoot := &root pRoot.print() pRoot.setValue(200) pRoot.print() var nRoot *treeNode nRoot.setValue(200) // nil指针也可以调用方法 nRoot = &root nRoot.setValue(300) nRoot.print() root.traverse() } ``` #### 封装 > 名字一般使用CamelCase > 首字母大写:public > 首字母小写:private ##### 包 > 每个目录一个包 > main包包含可执行入口 > 为结构定义的方法必须放在同一个包内 > 可以是不同的文件 ##### 扩展已有类型 > 使用别名 > 使用组合 ```go // 别名 package queue type Queue []int //已有类型 func (q *Queue) Push(v int) { *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 } ``` ##### go get获取第三方库 > go get 获取第三方库 > 使用gopm来获取无法下载的包 > > example: > > ```shell > gopm get -g -v -u golang.org/x/tools/cmd/goimports > ``` ##### GOPATH下目录结构 > go build 来编译 > go install产生pkg文件和可执行文件 > 编译时必须package main,只有main包才能编译成二进制 > go run 直接编译运行 ### 接口 > 接口存在,接口的实现者 > 接口类似于python 中ABC,抽象基类,实现者必须重写该方法, > 感觉python的魔法方法,只不过可以自定义 > 而实现这个接口的方法的结构即满足鸭子类型 > 接口类似描述鸭子的性状 ```go type Retriever interface { // 接口声明 Get(url string) string } type Poster interface { Post(url string, form map[string]string) string } ``` ```go // 接口实现者 type Retriever struct { // 接口实现者 Contents string } func (r *Retriever) Post(url string, form map[string]string) string { r.Contents = form["contents"] return "ok" } func (r *Retriever) Get(url string) string { return r.Contents ``` #### Go语言的duck typing >拥有python的灵活性,又具有Java的类型检查 #### 接口的定义和实现 > 接口变量自带指针 > 接口变量同样采用值传递,几乎不需要使用接口的指针 > 指针接收者实现只能以指针方式使用;值接收者均可 #### 查看接口变量 > 表示任何类型:interface{} > Type Assertion ```go if realRetriever, ok := r.(*real.Retriever); ok { fmt.Println(realRetriever.TimeOut) } else { fmt.Println(" not a real retriever") } ``` > Type Switch ```go func inspect(r Retriever) { // interface类型判断 fmt.Printf("%T %v\n", r, r) // %T 是类型,%v, %T是字节码,%c是rune字符 switch v := r.(type) { case mock.Retriever: fmt.Println("Contents:", v.Contents) case *real.Retriever: fmt.Println("UserAgent:", v.UserAgent) } } ``` #### 接口值的类型 > type Queue []int 只支持int类型 > type Queue []interface{} // 接口不限定类型 #### 接口的组合 > go中没有继承与多态 > 接口的组合,多种接口组合为一个接口,使使用者拥有多态特性 ```go type RetrieverPoster interface { Retriever Poster } func session(s RetrieverPoster) string { s.Post(url, map[string]string{ "contents": "another faked imooc.com", }) return s.Get(url) } ``` ```go // 接口实现者 type Retriever struct { // 接口实现者 Contents string } func (r *Retriever) Post(url string, form map[string]string) string { r.Contents = form["contents"] return "ok" } func (r *Retriever) Get(url string) string { return r.Contents ``` #### 常用系统接口 > 类似于魔法方法 ```go // reader func printFileContents(reader io.Reader) { scanner := bufio.NewScanner(reader) for scanner.Scan() { fmt.Println(scanner.Text()) } } ``` ```go // 自定义输出 func (r *Retriever) String() string { return fmt.Sprintf("Retriever: {Contents=%s}", r.Contents) } ``` ### 函数式编程 #### 函数式编程与函数指针 > 函数是一等公民:参数,变量,返回值,都可以是函数 > 高阶函数 > 函数 --> 闭包 #### “正统”函数式编程 > 不可变性:不能有状态,只有常量和函数 > 函数只能有一个参数 > 一般不遵循该规定 #### 函数与闭包 > 闭包 函数体:局部变量+自由变量 > 自由变量会找到所有的连接关系,连接所有的变量,整体构成一个闭包 ##### 闭包的应用 > 斐波那契数列 ```go func fibonacci() intGen { a, b := 0, 1 return func() int { a, b = b, a + b return a } } type intGen func() int // 类型就能实现接口 func (g intGen) Read(p []byte) (n int, err error) { next := g() if next > 10000 { return 0, io.EOF // 文件读完 } s := fmt.Sprintf("%d\n", next) return strings.NewReader(s).Read(p) // 代理实现Read接口 } func printFileContents(reader io.Reader) { scanner := bufio.NewScanner(reader) for scanner.Scan() { fmt.Println(scanner.Text()) } } func main() { f := fibonacci() printFileContents(f) } ``` ### 资源管理与出错处理 #### defer调用 > 确保调用在函数结束时发生 > 参数在defer语句时计算 > defer列表为后进先出 #### 调用defer的时机 > Open/Close > Lock/UnLock > PrintHeader/PrintFooter ```go func writeFile(filename string) { file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666) err = errors.New("this is a custom error") if err != nil{ if pathError, ok := err.(*os.PathError); !ok { panic(err) } else { fmt.Printf("%s, %s, %s\n", pathError.Op, pathError.Path, pathError.Err) } //fmt.Println("Error:",err) return } defer file.Close() // 函数执行后关闭文件 writer := bufio.NewWriter(file) defer writer.Flush() // 后进栈执行先与file.Close() f := fib.Fibonacci() for i := 0; i < 20; i++ { fmt.Fprintln(writer, f()) } } ``` #### 错误处理概念 > 记录错误类型不让程序中断,类似于python try...catch ##### 服务器的错误处理 > 服务器错误不能让用户看到,应该返回状态码 ```go // handler package filelist import ( "io/ioutil" "net/http" "os" ) func HandleFileList (writer http.ResponseWriter, request *http.Request) error { path := request.URL.Path[len("/list/"):] // /list/fib.txt file, err := os.Open(path) if err != nil { // 记录错误信息 //http.Error(writer, // err.Error(), // http.StatusInternalServerError) return err } defer file.Close() all, err := ioutil.ReadAll(file) if err != nil { return err } writer.Write(all) return nil } ``` ```go // web package main import ( "learngo/errhandling/fileserver/filelist" "log" "net/http" "os" ) type appHandler func(writer http.ResponseWriter, request *http.Request) error // 函数式编程, func errWrapper (handler appHandler) func(http.ResponseWriter, *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { err := handler(writer, request) if err != nil { //log.Warn("Error handling request: %s", err.Error()) // 不是标准库 log.Printf("Error occurred" + "handling request: %s", err.Error()) code := http.StatusOK // 文件不同的错误类型 switch { case os.IsNotExist(err): // 找不到资源 code = http.StatusNotFound case os.IsPermission(err): code = http.StatusForbidden // 没有权限,错误检测,在linux中文件权限改为500,访问时就没有权限 default: code = http.StatusInternalServerError } http.Error( writer, // 向谁来汇报err http.StatusText(code), // error, 写错误码是为了不暴露内部错误的具体原因 code) } } } func main() { http.HandleFunc("/list/", errWrapper(filelist.HandleFileList)) err := http.ListenAndServe(":9999", nil) if err != nil { panic(err) } } ``` ##### panic和recover ##### panic > 停止当前函数执行 > 一直向上返回,执行每一层的defer > 如果没有遇见recover,程序退出 ##### recover > 仅在defer调用中使用 > 获取panic的值 > 如果无法处理,可重新panic ##### error和panic > 意料之中的:使用error。如:文件打不开 > 意料之外的:使用panic。如:数组越界 ##### 错误处理综合示例 > defer + panic +recover > Type Assertion > 函数式编程的应用 ### 测试 > testing.T的使用 > 运行测试 > 表格驱动测试 > 代码覆盖 > 性能优化工具 > http测试 > 文档以及示例代码,示例代码要写//Output: // 期望输出的值,可以当测试用 #### 传统测试和表格驱动测试 ##### 传统测试 ``` @Test public void testAdd() { asserEquals(3, add(1, 2)); asserEquals(2, add(0, 2)); asserEquals(0, add(0, 0)); asserEquals(0, add(-1, 1)); assertEquals(Integer.MIN_VALUE, add(1, Integer.MAX_VALUE)); } ``` > 测试数据和测试逻辑混在一起 > 出错信息不明确 > 一旦一个数据出错测试全部结束 ##### 表格驱动式测试 ```go tests := []struct { a, b, c int32 }{ {1, 2, 3}, {0, 2, 2}, {0, 0, 0}, {-1, 1, 0}, {math.MaxInt32, 1, math.MaxInt32}, // 最大的int32 + 1 后溢出变为最小的int32 } for _, test := range tests { if actual := add(test.a, test.b); actual != test.c {} } ``` > 分离的测试数据和测试逻辑 > 明确的出错信息 > 可以部分失败 > go语言的语法使得我们更易实践表格驱动测试 ```go import "testing" // TestXxx的命名规范,Test后首字母大写 func TestSubstr(t *testing.T) { // 功能测试 tests := []struct{ s string ans int } { // Normal cases {"abcabcbb", 3}, {"pwwkew",3}, // Edge cases {"",0}, {"b", 1}, {"bbbbbbbbbb",1}, {"abcabcabcd",4}, //末尾字符增加 // Chinese support {"这里是我家",5}, {"这里是我家!",6}, {"一二三二一",3}, {"yes,是的",6}, } for _, tt := range tests { actual := lengthOfNonRepeatingSubStr(tt.s) if actual != tt.ans { t.Errorf("got %d for input %s; " + "expected %d", actual, tt.s, tt.ans) } } } ``` #### 代码覆盖率 > 命令行中输入 > go test -coverprofile=c.out > go tool cover // 观察命令 #### 性能测试 > test.B的使用 > 例:使用pprof优化性能 ```go func BenchmarkSubstr(b *testing.B) { // 性能测试 s := "yes,是的是的是的" ans := 6 for i := 0; i < b.N; i++ { // 系统自动确定运行遍数 actual := lengthOfNonRepeatingSubStr(s) if actual != ans { b.Errorf("got %d for input %s; " + "expected %d", actual, s, ans) } } } ``` > go test -bench . #### 使用pprof进行性能调优 > go test -bench . -cpuprofile cpu.out // 查看代码运行性能 > go tool pprof cpu.out // 打开cpu.out文件 > web // 查看各部分耗时的文件 > 结论: > 算法优化时,大数据时使用slice,小数据时使用map较好(寻找最长子串为例) > 检验性能时不能计算准备数据的时间 ```go s := "yes,是的是的是的" for i := 0; i < 13; i++ { // 准备输入数据的时间省略 s = s + s } b.Logf("len(s) = %d", len(s)) ans := 6 b.ResetTimer() // 准备输入数据的时间需要被省略 ``` > slice定义应该放在包外或函数外,在进行性能验证时不会出现大量的垃圾回收 #### http测试 > 通过使用假的Request/Response > 速度快,相当于单元测试 ```go func TestErrWrapper(t *testing.T){ // 仅仅测试了errWrapper函数 for _, tt := range tests { f := errWrapper(tt.h) response := httptest.NewRecorder() // return &ResponseRecorder {HeaderMap, Body, Code} request := httptest.NewRequest(http.MethodGet, "https://www.baidu.com", nil) f(response, request) verifyResponse(response.Result(), tt.code, tt.message, t) } } ``` > 通过起服务器 > 速度慢但整体性高 ```go func TestErrWrapperInServer(t *testing.T) { // 在server中进行测试 for _, tt := range tests { f := errWrapper(tt.h) server := httptest.NewServer(http.HandlerFunc(f)) // http.HandlerFunc 可以转成server的接口 resp, _ := http.Get(server.URL) verifyResponse(resp, tt.code, tt.message, t) } } ``` ### go语言文档 > go doc 包名 > godoc -http=localhost:6060 > 用注释可以写文档 > 在测试中加入Example > 使用go doc / godoc来查看/生成文档 #### go语言示例代码 ```go package queue import "fmt" func ExampleQueue_Pop() { q := 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()) // Output: //1 //2 //false //3 //true } ``` > 命令行 go test ## go语言的并发编程 > 其它语言中大的并发编程优先选择开线程来执行,然而对于大量并发时,是采取异步io来实现 > go 语言是由类协程来实现高并发 > 线程是属于抢占式多任务处理,在任何时候都有可能被系统中断暂停 ### goroutine #### 协程 Coroutine > 轻量级“线程” > 非抢占式(自主控制)多任务处理,由协程主动交出控制权 > 编译器/解释器/虚拟机层面的多任务 > 多个协程可能在一个或多个线程上运行 > go run -race goroutine.go 检查数据冲突 > Subroutines are special cases of more general program components, called coroutines. In contrast to the unsymmetric. > 子程序是协程的一个特例 #### goroutine的定义 > 任何函数只需加上go就能送给调度器运行 > 不需要在定义时,区分是否是异步函数 > 调度器在合适的点进行切换 > 使用 -race来检测数据访问冲突 #### goroutine可能的切换点(参考,不是肯定) > I/O, select > channel > 等待锁 > 函数调用(有时) > runtime.Gosched() 手动交出协程控制权 ### channel(goroutine之间的双向通道) > Don't communicate by sharing memory; share memory by communicating #### 基础理论 Communication Sequential Process (CSP) > channel 是goroutine之间的双向通道,如果数据发出而没有人接受会产生deadlock > channel也是一等公民,也能作为参数和返回值 > 类型是chan ```go func chanDemo() { //var c chan int // var 定义了c是channel int类型 c == nil 无法运用 c := make(chan int) // make 定义的channel可以直接运用 c <- 1 // 1 输入给channel // all goroutines are asleep - deadlock! 发送1时出现死锁 c <- 2 // 2 输入给channel n := <-c // channel 输出给 n fmt.Println(n) } func main() { chanDemo() } ``` > channel 作为参数 ```go func worker(id int, c chan int) { for { //n := <-c // channel 输出给 n fmt.Printf("Worker %d received %c\n", id, <-c) // c% 是字符类型 } } func chanDemo() { //var c chan int // var 定义了c是channel int类型 c == nil 无法运用 var channels [10]chan int // 建立容量为10的数组,类型是chan,10个chan分发给10个worker for i := 0; i < 10; i++ { channels[i] = make(chan int) // make 定义的channel可以直接运用, 相当于参数 go worker(i, channels[i]) } for i := 0; i < 10; i++ { channels[i] <- 'a' + i } for i := 0; i < 10; i++ { channels[i] <- 'A' + i } time.Sleep(time.Millisecond) } func main() { chanDemo() } ``` > channel作为返回值 ```go func createWorker(id int) chan int { // chan 作为返回值 // 函数是创建worker, 创建 go func c := make(chan int) go func() { // 在 go func 中对c操作 // worker在这里 for { //n := <-c // channel 输出给 n fmt.Printf("Worker %d received %c\n", id, <-c) // c% 是字符类型 } }() return c } func chanDemo() { //var c chan int // var 定义了c是channel int类型 c == nil 无法运用 var channels [10]chan int // 建立容量为10的数组,类型是chan,10个chan分发给10个worker for i := 0; i < 10; i++ { channels[i] = createWorker(i) } for i := 0; i < 10; i++ { channels[i] <- 'a' + i } for i := 0; i < 10; i++ { channels[i] <- 'A' + i } time.Sleep(time.Millisecond) } func main() { chanDemo() } ``` > 建立缓冲区,没有人读取数据时也不会出现死锁 ```go func bufferedChannel() { c := make(chan int, 3) // 建立缓冲区有利于提升性能 go worker(0, c) // 没有接收者时,传入三个数据也不会出现死锁 c <- 'a' c <- 'b' c <- 'c' c <- 'd' time.Sleep(time.Millisecond) } ``` > 关闭channel;发送方发送关闭信号时,接受方接受到空串或0时进行关闭 ```go func worker (id int, c chan int) { //for { // // 接收方接收到发送方的close进行中断 // n, ok := <- c // if !ok { // break // } // //n := <-c // channel 输出给 n // fmt.Printf("Worker %d received %d\n", id, n) // c% 是字符类型 //} for n := range c { fmt.Printf("Worker %d received %d\n", id, n) } } func channelClose() { // 数据有明确的结尾,需要关闭channel,数据发送方来发送给数据接收方的 c := make(chan int, 3) // 建立缓冲区有利于提升性能 go worker(0, c) c <- 'a' c <- 'b' c <- 'c' c <- 'd' close(c) // 发完a,b,c,d后接收方仍然会收1ms的空串或者0 time.Sleep(time.Millisecond) } ``` #### 通过通信来共享内存 ##### 例一:使用Channel来等待goroutine结束(以及WaitGroup的使用) > 使用Channel来等待goroutine结束 > goroutine结束时通知主程序同时结束 ```go func doWork (id int, w worker) { for n := range w.in { fmt.Printf("Worker %d received %c\n", id, n) go func() { w.done <- true // 作为终止规则,当外层for循环终止时,这个goroutine也终止,此时不再给done传入true // 最下方for循环跳出 // 如果没有done时则in会不断接受空串而造成死锁 // 因为有两组数据写入,如果可能一组数据写完后退出了最下面的主控 // 只收到了一个空值(退出信号)就所有的退出了而另外一组数据还未处理完毕 }() } } type worker struct { in chan int done chan bool } func createWorker(id int) worker { w := worker{ in: make(chan int), done: make(chan bool), } go doWork(id, w) return w } func chanDemo() { var workers [10]worker for 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 } // wit for all of them for _, worker := range workers{ // 退出主程序的退出所有 n:= <-worker.done m:= <-worker.done fmt.Println(n, m) } } ``` > WaitGroup使用 ```go func doWork (id int, c chan int, w worker) { for n := range c { fmt.Printf("Worker %d received %c\n", id, n) go func() { w.done() }() } } type worker struct { in chan int //wg *sync.WaitGroup done func() } func createWorker(id int, wg *sync.WaitGroup) worker { w := worker{ in: make(chan int), done: func() { wg.Done() }, } go doWork(id, w.in, w) return w } func chanDemo() { var workers [10]worker var wg sync.WaitGroup wg.Add(20) // add多少个任务 for i := 0; i < 10; i++ { workers[i] = createWorker(i, &wg) } for i, worker := range workers { worker.in <- 'a' + i } for i, worker := range workers { worker.in <- 'A' + i } wg.Wait() } ``` ##### 例二:使用Channel来实现树的遍历 > tree文件夹下 ```go func (node *Node) TraverseWithChannel() chan *Node { // 利用channel来遍历树 out := make(chan *Node) go func() { node.TraverseFunc(func(node *Node) { out <- node }) close(out) }() return out } c := root.TraverseWithChannel() maxNode := 0 for node := range c { if node.Value > maxNode { maxNode = node.Value } } fmt.Println("Max node value:", maxNode) ``` ##### 例三:使用Select来进行调度 > Select 的使用 加入default是非阻塞式的获取值 > 定时器的使用 > 在Select中使用nil channel,在数据未准备好时将channel置成nil channel ##### 传统的同步机制 > WaitGroup > Mutex ```go / 互斥锁的例子 type atomicInt struct { value int lock sync.Mutex } func (a *atomicInt) increment() { fmt.Println("safe increment") func (){ // 整块的保护 a.lock.Lock() defer a.lock.Unlock() a.value++ }() } func (a *atomicInt) get() int { a.lock.Lock() defer a.lock.Unlock() return a.value } func main() { var a atomicInt a.increment() go func() { a.increment() // 写 }() time.Sleep(time.Millisecond) fmt.Println(a.get()) // 读,在不加锁的时候有数据冲突 } ``` > Cond ### Go语言标准库 #### http > 使用http客户端发送请求 > 使用http.Client控制请求头部等 > 使用httputil简化工作 ##### http服务器的性能分析 > 在Web服务器 import _ "net/http/pprof" > 访问/debug/pprof/ http://localhost:9999/debug/pprof/ > 使用go tool pprof分析性能 go tool pprof http://localhost:9999/debug/pprof/profile 不断刷新页面观察30秒cpu性能 (pprof) web > go tool pprof http://localhost:9999/debug/pprof/heap 观察内存性能 #### 其他标准库 > bufio // 加缓存区的读写提升读写性能,将文件写入缓存区再一次性写到硬盘上 > log > encoding/json // 结构体直接decode或encode json > regexp // 正则表达式 > time // 可以产生一些channel,和select配合可以完成超时,或定时任务等 > strings/math/rand ##### 看标准库的文档 > godoc -http=localhost:8888 > https://studygolang.com/pkgdoc #### 例:广度优先算法走迷宫 > 用循环创建二维slice > 使用slice来实现队列 > 用Facanf读取文件 > 对Point的抽象 ##### 广度优先算法 > 上左下右 逆时针遍历 > 起始至终止逐步进行广度,终止至起始计算路径 > 结束条件,1. 终点; 2. 全是死路,队列为空即终止 ### 实战项目 > 分布式爬虫 #### 那些go语言没有的元素 > 类,继承,多态,重载 > > try / catch / finally > > 泛型 > > 构造函数 / 析构函数 > > 操作符重载 > > assert ##### 类,继承,多态,重载 > go 语言拥有不同的世界观 > > 在面向对象,也流行变继承为组合的思维 > > 面向对象的元素容易被滥用 > > go语言为组合提供了便捷的支持 ##### try/ catch/ finally > 太多错误被当做异常 > > 错误:意料之中的 > > 正确的使用try/catch处理错误,导致代码混乱 > > try/catch在产品代码中并不能减小开发人员负担 > > defer/ panic / recover 模式 构造函数/析构函数/RAII > 大型项目中很少使用构造函数,多使用工厂函数 > > 工厂函数:返回一个对象,给对象注入依赖 > > 值类型的构造有结构体初始化语法实现 > > RAII技巧性太强,隐藏意图(我不知道是个啥,C++的) > > 析构函数与垃圾回收不匹配 ##### 泛型(c++的) > 泛型作为模板类型:实际想实现duck typing go已经有了 > > 泛型约束参数类型: > > 本身非常复杂:类型通配符,covariance等问题 > > go语言本身自带强类型的slice,map,channel > > go使用type assertion甚至 gogenerate来实现自己的泛型 > > 泛型支持是作者唯一态度不强硬的点 ### 课程总结 #### 基本语法 > 变量 > > 选择、循环 > > 指针、数组、容器 #### 面向接口 >结构体 > >duck typing的概念 > >组合的思想 #### 函数式编程 > 闭包概念 #### 工程化 > 资源管理、错误处理 > > 测试(表格驱动测试)和文档 > > 性能调优 #### 并发编程 > goroutine(非抢占式多任务)和channel > > 理解调度器 #### 标准库(项目使用到的) > fmt,log > > errors > > io,bufio > > time > > net/http,net/rpc, jsonrpc > > html/template > > charset, encoding, unicode, utf8 > > strings, bytes, strconv > > regexp > > flag > > math > > os > > pprof > > runtime > > reflect(项目未涉及) > > Testing #### 实战项目 > 分布式爬虫系统