# GO_李文周博客学习 **Repository Path**: java131313/go-li-wenzhous-blog-learning ## Basic Information - **Project Name**: GO_李文周博客学习 - **Description**: Golang基础入门 原博客地址:https://www.liwenzhou.com/ - **Primary Language**: Go - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 6 - **Created**: 2024-06-02 - **Last Updated**: 2024-06-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 参考书目 [李文周视频课程](https://www.bilibili.com/video/BV17Q4y1P7n9) [李文周博客](https://www.liwenzhou.com/) [Go 语言设计与实现](https://draveness.me/golang/) [同学的gitee仓库](https://gitee.com/gtxy27/go) [Golang标准库中文文档](http://doc.golang.ltd/) [gowebexamples | go web示例](https://gowebexamples.com/) ## 一、安装与基本使用 ### 1)搭建开发环境 1. 第一步:下载并安装 [Go官网下载地址 ](https://golang.org/dl/) [Go官方镜像站(推荐)](https://golang.google.cn/dl/) 2. 第二步:`go version`命令查看当前 go 版本号 3. 第三步:配置GOPATH `go env` 查看 go 环境变量配置 GOPATH 是一个环境变量,表明go项目的存放路径(go 1.8开始会为GOPATH设置默认目录;go 1.14开始使用go mod管理项目,可以不将代码写到 GOPATH 指定目录了) GOPATH 目录下包含三个文件夹: - bin:用来存放编译后生成的可在执行文件 - pkg:用来存放编译后生成的可在归档文件 - src:用来存放源码文件 ,实际开发中会以域名或github用户名组织代码文件 ![组织结构](./public/gopath%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84.png) ### 2)go mod 1. 设置GOPROXY GOPROXY —— 设置网络代理,使用 go mod 管理 go 项目第三方依赖时下载路径, 默认为`GOPROXY=https://proxy.golang.org,`,修改国内代理:`go env -w GOPROXY=https://goproxy.cn,direct` 2. 初始化项目 使用 `go mod init 项目名`初始化项目,会在执行目录生成go.mod文件(包含go版本,依赖等) 3. 新建入口文件 ```go package main // 将此文件归属到 main import "fmt" // 引入 fmt 包 func main() { // 输出 fmt.Println("hello world") } ``` 4. 编译 运行 使用`go build`编译整个项目: - 在当前目录下执行`go build`,表示将源代码编译成可执行文件: - 或者在其他目录执行以下命令`go build 文件路径`,编译器会去 GOPATH 的src目录下查找你要编译的hello项目 - 编译得到的可执行文件会保存在执行编译命令的当前目录下,运行即可 - 可以使用 -o 参数来指定编译后得到的可执行文件的名字。`go build -o hello.exe` 使用`go run`临时编译项目: - 编译单个文件即可 :`go run main.go` 使用`go install`临时编译项目: - `go install main.go` 先编译得到可执行文件,再将可执行文件拷贝到GOPATH/bin中(可以全局使用) 跨平台编译:省略 ### 3) 变量和常量 1. 标识符 标识符就是程序员定义的具有特殊意义的词,如变量名、常量名、函数名等等。 Go语言中标识符由字母数字和_(下划线)组成,并且只能以字母和_开头。 举几个例子:abc, _, _123, a123等 2. 关键字 关键字是指编程语言中预先定义好的具有特殊含义的标识符。 关键字和保留字都不建议用作变量名。 Go语言中有25个关键字和37个保留字,如:go、func、var、return、map、interface、impor、...等 3. 变量 利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据。 - 变量类型: 变量(Variable)的功能是存储数据。不同的变量保存的数据类型不同,常见变量的数据类型有:整型、浮点型、布尔型、结构体类型等。 - 变量声明: Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用。 标准声明: `var 变量名 变量类型` 批量声明: `var (变量名 变量类型 变量名 变量类型 ...)` ```go // 标准声明 var name string var age int //批量声明多个变量 var ( // 不初始化变量会被赋默认值 a string //"" b int //0 c bool //false ) ``` - 变量初始化: Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil。 ***类型推导(初始化时省略数据类型)***: 初始化时可以将变量的类型省略,这时编译器会根据值来推导变量的类型完成初始化,如:`var name = "Q1mi"` ***短变量声明(常用,但不能声明全局变量)***: 在函数内部(局部变量),可以使用更简略的 `:=` 方式声明并初始化变量。`name := "tom" age:= 18` ***匿名变量*** 在批量声明变量时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 `name,age := "tom",_` ***注意事项:*** 函数外的每个语句都必须以关键字开始(var、const、func等) := 只能在函数体内声明变量,且这种方式不支持声明全局变量。 匿名变量时一个特殊的变量,_ 多用于占位,表示忽略值。 同一个作用域中不能声明同名变量。 3. 常量和iota 常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值,常量在定义的时候必须赋值。定义常量的关键字`const`,定义语法类似变量。 批量声明常量时,如果省略了值则表示和上一行的值相同。 ```go const pi = 3.1415 const ( pi = 3.1415 e = 2.7182 n = 12 n1 //12 ) ``` - iota iota是go语言中的*常量计数器*,只能在常量的表达式中使用。 iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。 ```go const ( a1 = iota //0 a2 //1 _ a3 //3 a4 = 100 //100 a5 //100 a6, a7 = iota, iota+1 //6 7 ) ``` ### 4) 基本数据类型 Go语言中除了基本数据类型:整型、浮点型、布尔型、字符串外,还有数组、切片、结构体、函数、map、通道(channel)等复杂数据类型 ![数据类型](./public/数据类型.png) 1. 整形 按长度分为:int8、int16、int32、int64,对应的无符号整型:uint8、uint16、uint32、uint64。 - 特殊整型(平台不同,数据类型不同) 类型 描述 uint 32位操作系统上就是uint32,64位操作系统上就是uint64 int 32位操作系统上就是int32,64位操作系统上就是int64 uintptr 无符号整型,用于存放一个指针 注意: 在使用int和 uint类型时,不能假定它是32位或64位的整型,而是考虑int和uint可能在不同平台上的差异。 获取对象的长度的内建len()函数返回的长度可以根据不同平台的字节长度进行变化。 - 数字字面量 Go 1.13版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字,如: v := 0b00101101 //代表二进制的 101101,相当于十进制的 45。 v := 0o377 //代表八进制的 377,相当于十进制的 255。 v := 0x1p-2 //代表十六进制的 1 除以 2²,也就是 0.25。 v := 123_456 //允许用 _ 来分隔数字,比如说:表示 v 的值等于 123456。 我们可以借助fmt函数来将一个整数以不同进制形式展示。 ```go var i1 = 101 fmt.Printf("%d\n", i1) //十进制打印 fmt.Printf("%b\n", i1) //二进制打印 fmt.Printf("%o\n", i1) //八进制打印,八进制 以0开头 fmt.Printf("%x\n", i1) //十六进制打印,十六进制 以0x开头 //查看变量类型 fmt.Printf("变量类型:%T\n", i1) //变量类型:int // 声明int8类型变量 i8 := int8(9) fmt.Printf("变量类型:%T\n", i8) //变量类型:int8 ``` 2. 浮点数 Go语言支持两种浮点型数:*float32*和*float64*。这两种浮点型数据格式遵循IEEE 754标准: float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32。 float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64。 打印浮点数时,可以使用 fmt 包配合格式化输出 %f,代码如下: ```go // Float32最大值 // math.MaxFloat32 f1 := 1.35283 fmt.Printf("f1: %v, type:%T\n", f1, f1) //f1: 1.35283,type:float64,默认都是float64类型 f2 := float32(1.36) fmt.Printf("f2: %v, type:%T\n", f2, f2) //f2: 1.36, type:float32 // f1 = f2 //float32不能直接复制给float64 // golang中有已经定义好小数 fmt.Printf("%f\n", math.Pi) fmt.Printf("%.2f\n", math.Pi) ``` 3. 复数(了解) complex64和complex128,复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。 ```go var c1 complex64 c1 = 1 + 2i var c2 complex128 c2 = 2 + 3i fmt.Println(c1) fmt.Println(c2) ``` 4. 布尔值 Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)和false(假)两个值,布尔类型变量的默认值为false。 Go 语言中不允许将整型强制转换为布尔型,且布尔值无法参与数值运算,也无法与其他类型进行转换。 ```go var b bool //默认值为false fmt.Printf("value:%v type:%T\n", b, b) //value:false type:bool ``` 5. fmt占位符 ```go i := 101 fmt.Printf("i: %v\n", i) // i: 101 fmt.Printf("i: %b\n", i) // i: 1100101 fmt.Printf("i: %d\n", i) // i: 101 fmt.Printf("i: %o\n", i) // i: 145 fmt.Printf("i: %x\n", i) // i: 65 fmt.Printf("i: %T\n", i) // i: int s := "hello go !!!" fmt.Printf("s: %s\n", s) //s: hello go !!! fmt.Printf("s: %v\n", s) //s: hello go !!! fmt.Printf("s: %#v\n", s) //s: "hello go !!!" ``` 6. 字符串 Go 语言里的字符串的内部实现使用UTF-8编码。 字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCII码字符,例如:`s1 := "hello"` - 字符串转义符 Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。 | 转义符 |含义 | | ------- | ------ | | \r | 回车符(返回行首) | | \n | 换行符(直接跳到下一行的同列位置) | | \t | 制表符 | | \' | 单引号 | | \" | 双引号 | | \\ | 反斜杠 | ```go path := "C:\\Users\\Mrnianj\\go" fmt.Printf("path: %v\n", path) // path: C:\Users\Mrnianj\go ``` - 多行字符串(模板字符串) Go语言中要定义一个多行字符串时,必须使用反引号字符,反引号间换行将被作为字符串中的换行,但所有的转义字符均无效,文本将会原样输出。 ```go s1 := ` 第一行 第二行 第三行 ` ``` - 字符串的常用操作方法 | 方法 | 介绍 | | ----------- | ----------- | | Header | Title | | Paragraph | Text | | len(str) | 求长度 | | +或fmt.Sprintf | 拼接字符串 | | strings.Split | 分割 | | strings.contains | 判断是否包含 | | strings.HasPrefix,strings.HasSuffix | 前缀/后缀判断 | | strings.Index(),strings.LastIndex() | 子串出现的位置 | | strings.Join(a[]string, sep string) | join操作| ```go // 字符串常用方法 name := "理想" world := "world" // 1) 字符串拼接 ss := name + world fmt.Printf("ss: %v\n", ss) // ss: 理想world fmt.Println(name + world) // 理想world ss1 := fmt.Sprintf("%s%s", name, world) fmt.Printf("ss1: %v\n", ss1) // ss1: 理想world fmt.Printf("%s%s\n", name, world) //理想world // 2) 字符串分割 path := "C:\\Users\\Mrnianj\\go" ret := strings.Split(path, "\\") fmt.Printf("ret: %v\n", ret) // ret: [C: Users Mrnianj go] // 3) 判断是否包含 fmt.Println(strings.Contains(ss, "world")) // true // 4) 前后缀 fmt.Println(strings.HasPrefix(ss, "hello")) // false // 5) 判断字符串位置 fmt.Println(strings.Index(ss, "w")) // 6 fmt.Println(strings.LastIndex(ss, "l")) // 9 // 6) 拼接 fmt.Println(strings.Join(ret, "--")) // C:--Users--Mrnianj--go ``` - byte(=int8)和rune(=int32)类型 组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 定义字符用单引号包裹,定义语法:`c := 'o'` Go 语言的字符有以下两种: uint8类型,也叫 byte 型,代表了ASCII码的一个字符。 rune类型,代表一个 UTF-8字符,当需要处理中\日文\复合字符时,需要用到rune类型。rune类型实际是一个int32。 ```go var c1 byte = 'c' var c2 rune = 'c' fmt.Printf("c1: %v type:%T\n", c1, c1) // c1: 99 type:uint8 fmt.Printf("c2: %v type:%T\n", c2, c2) // c2: 99 type:int32 s := "hello 世界!!!" fmt.Println("s.len=", len(s)) // len 是球的byte字节的数量 for i := 0; i < len(s); i++ { // %c 字符 fmt.Printf("%c", s[i]) // hello ä¸ç!!! } fmt.Print("\n") for _, v := range s { // 从字符串中拿出具体的字符 fmt.Printf("%c", v) // hello 世界!!! } ``` - 修改字符串 修改字符串,需要先将其转换成`[]rune`或`[]byte`,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。 ```go // 修改字符串 str := "胡萝卜" // '胡' '萝' '卜' ss := []rune(str) // 将字符串强制变成一个rune切片 ss[0] = '白' // 将rune切片强制变成一个字符串 fmt.Printf("%v\n", string(ss)) // 白萝卜 c3 := "白" c4 := '白' fmt.Printf("c3: %v type:%T\n", c3, c3) // c3: 白 type:string fmt.Printf("c4: %v type:%T\n", c4, c4) // c4: 30333 type:int32 ``` 7. 类型转换 Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。 强制类型转换的基本语法如下:`T(表达式)`,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等。 ```go // 类型转换 n := 10 var f float64 f = float64(n) fmt.Printf("f: %v type:%T\n", f, f) // f: 10 type:float64 ``` ### 5) 流程控制 1. if ```go age := 18 if age > 14 { // 控制肯定语句1 } else if age > 18 { // 控制肯定语句2 } else { // 控制否定语句 } // 特殊 ,name 局部变量只存在与 if 作用域之内 if name := "lisi"; name == "lisi" { fmt.Printf("%v\n", name) } // fmt.Printf("%v\n", name) ``` 2. for ```go // 无限循环,可以通过return goto break panic语句强制退出 n := 0 for { n++ fmt.Printf("n: %v\n", n) if n >= 3 { break } } // for 循环 for i := 0; i < 5; i++ { fmt.Printf("i: %v\n", i) } // for range,键值循环f num := []int{1, 2, 3, 5} for _, v := range num { fmt.Printf("v: %v\n", v) } ``` - 跳出for循环 ```go // i=5时 跳出循环 for i := 0; i < 10; i++ { fmt.Printf("i: %v\n", i) if i >= 5 { break } } // i=5时 跳过此次循环 for i := 0; i < 10; i++ { if i == 5 { continue } fmt.Printf("i: %v\n", i) } ``` 3. switch ```go var n = 10 switch n { case 1: fmt.Print(1) case 2: fmt.Print(2) case 5: fmt.Print(5) default: fmt.Println("not is 1 or 2 or 5") } // 简化写法 switch n := 2; n { case 1, 3, 5, 7, 9: fmt.Println("奇数") case 2, 4, 6, 8: fmt.Println("偶数") default: fmt.Println(n) } // 变种写法: age := 18 switch { case age > 18: fmt.Println("成年了") case age > 0: fmt.Println("未成年") } // fallthrough 满足条件向下穿透一个case,go中是为了兼容c语言中的case设计的(不推荐使用) s := "a" switch { case s == "a": fmt.Println("s == a") fallthrough case s == "b": fmt.Println("s == b") default: fmt.Println(s) } ``` 4. goto ```go var flag = false for i := 0; i < 3; i++ { for j := 'A'; j < 'Z'; j++ { if j == 'C' { flag = true break } fmt.Printf("i=%d j=%c\n", i, j) } if flag { break } } /* 运行结果: i=0 j=A i=0 j=B */ // goto+label实现 for i := 0; i < 3; i++ { for j := 'A'; j < 'Z'; j++ { if j == 'C' { goto XX // 跳到指定标签 } fmt.Printf("i=%d j=%c\n", i, j) } } XX: // label标签 fmt.Println("over") /* 运行结果: i=0 j=A i=0 j=B over */ ``` 5. contine和break ```go // break推出指定标签对应的代码块,标签要求必须定义在for 、switch、和select代码块上 BREAKDEMO1: for i := 0; i < 3; i++ { for j := 'A'; j < 'Z'; j++ { if j == 'C' { break BREAKDEMO1 } fmt.Printf("i=%d j=%c\n", i, j) } } fmt.Println("over") /* 运行结果: i=0 j=A i=0 j=B ove */ // contine 结束当前循环,继续下次循环,仅限在for循环内使用 forloop1: for i := 0; i < 5; i++ { for j := 0; j < 3; j++ { if i == 2 && j == 2 { continue forloop1 } fmt.Printf("i=%d j=%d\n", i, j) } } /* 运行结果: ... i=1 j=0 i=1 j=1 i=1 j=2 i=2 j=0 i=2 j=1 i=3 j=0 ... */ ``` ### 6) 运算符 Go 语言内置的运算符有以下五类: 1. 算术运算符 ==> [ + - * / % ] 注意: ++(自增)和--(自减)在Go语言中是单独的语句,并不是运算符。 ```go var ( a = 5 b = 2 ) fmt.Println(a + b) // 7 fmt.Println(a - b) // 3 fmt.Println(a * b) // 10 fmt.Println(a / b) // 2 fmt.Println(a % b) // 1 // 单独的语句 a++ a-- ``` 2. 关系运算符 ==> [ == != > < >= <=> ] ```go // 关系运算符 fmt.Println(a == b) // false fmt.Println(a >= b) // true fmt.Println(a <= b) // false fmt.Println(a != b) // true fmt.Println(a > b) // true fmt.Println(a < b) // false ``` 3. 逻辑运算符 ==> [ && || ! ] ```go // 逻辑运算符 age := 20 // 若年龄大于18且小于60之间的 if age > 18 && age < 60 { fmt.Println("上班") } // 若年龄小于18或大于60 if age < 18 || age > 60 { fmt.Println("该吃吃 该喝喝") } // 取反 isMarried := false if !isMarried { fmt.Println("结婚了") } ``` 4. 位运算符 ==> [ & | ^ << >> ] ```go // 位运算符 针对二进制 // 5的二进制:101 // 2的二进制:10 // & 按位与(两位均为1才为1) fmt.Println(5 & 2) // 000 // | 按位或(两位有一个1就为1) fmt.Println(5 | 2) // 111 // ^ 按位异或(两位不一样则为1) fmt.Println(5 ^ 2) // 111 // << 将二进制左移指定位(将5的二进制为向右移2位) fmt.Println(5 << 2) // 10100 // >> 将二进制右移指定位 fmt.Println(5 >> 2) // 1 ``` 5. 赋值运算符 ==> [ =、+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^= ] ```go // 赋值运算符 var x = 10 x += 1 x -= 1 x *= 1 x /= 1 x %= 1 x <<= 2 x >>= 2 x ^= 2 x |= 2 x &= 2 ``` ### 7) 复合数据类型 1. 数组 - 数组是同一种数据类型元素的集合,从声明时就确定长度和数据类型,使用时可以修改数组成员,但是数组大小不可变化。 - 定义数组基本语法:`var 数组变量名 [元素数量]T` - 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会panic。 - 数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。 ```go // 1. 声明数组 var a1 [5]int var a2 [10]int // 数组的长度是数组类型的一部分 [5]int和[10]int是两种不同的类型 fmt.Printf("a1: %T\n", a1) // a1: [5]int fmt.Printf("a2: %T\n", a2) // a2: [10]int // 2. 数组初始化 var a3 = [3]int{1, 3, 5} fmt.Printf("a3: %v\n", a3) // 根据初始化值自动推算容量 a4 := [...]int{1, 2, 3, 4, 5, 6, 8, 9, 10} fmt.Println(len(a4)) // 根据索引初始化 a5 := [5]string{0: "a", 3: "c"} fmt.Printf("a5: %v\n", a5) // 3. 数组初始化 citys := [...]string{"北京", "上海", "广州", "深圳"} for i := 0; i < len(citys); i++ { fmt.Println("city:", citys[i]) } for _, v := range citys { fmt.Println("city:", v) } // 4. 多维数组 var al1 = [3][2]string{{"a", "b"}, {"A", "B"}, {"a1", "b2"}} var al2 = [3][2]int{{1, 3}, {5, 7}, {6, 8}} fmt.Printf("al1: %v\n", al1) // al1: [[a b] [A B] [a1 b2]] fmt.Printf("al2: %v\n", al2) // al2: [[1 3] [5 7] [6 8]] // 5. 数组是值类型 b := [...]int{1, 3, 7} b2 := b // 值传递 b2[0] = 100 fmt.Println(b, b2) // [1 3 7] [100 3 7] ``` - 注意: 数组支持 == 、!= 操作赋,因为内存总是被初始化过的 [n]*T表示指针数组,*[n]T表示数组指针 - 练习题 ```go // 1. 求数组[1,3,4,7,8]所有元素的和 arr := [...]int{1, 3, 4, 7, 8} var sum = 0 for _, v := range arr { sum += v } fmt.Printf("sum: %v\n", sum) // 2.从数组[1,3,5,7,8]中找出和为8的两个元素的下标分别为(0,3)和(1,2) arr2 := [...]int{1, 3, 5, 7, 8} for i1, v1 := range arr2 { for j := 0; j < len(arr2)-i1; j++ { if v1+arr2[i1+j] == 8 { fmt.Printf("(%d,%d)", i1, i1+j) } } } ``` 2. 切片 切片:相同元素的可变长度的序列,基于数组类型的一层封装,支持自动扩容,切片是一个引用类型,包括地址、长度和容量。 声明语法:`var name []T` 因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。 切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容,一般用于快速地操作一块数据集合。 切片的长度:切片拥有自己的长度和容量,我们可以通过使用内置的`len()`函数求长度,使用内置的`cap()`函数求切片的容量。 空切片:一个切片在未初始化之前默认为 nil,切片长度为 0 ,如 `var numbers []int` ```go // 定义切片 var s1 []int fmt.Printf("s1: %v type:%T\n", s1, s1) // s1: [] type:[]int // 未初始化的切片为空 nil fmt.Println(s1 == nil) // true // 初始化 var s2 = []string{"aa", "bb"} fmt.Printf("s2: %v type:%T\n", s2, s2) // s2: [aa bb] type:[]string // 切片的容量和长度 var s3 = []string{"aa", "bb", "世界很大"} fmt.Println("s2 len = ", len(s3)) fmt.Println("s2 cap = ", cap(s3)) // 数组定义切片,基于数组切割,左包含,右不包含【左闭右开】 arr := [...]int{1, 3, 5, 7, 9, 11, 13} s4 := arr[0:4] s5 := arr[0:] s6 := arr[:5] s7 := arr[:] fmt.Printf("s4: %v\n", s4) // s4: [1 3 5 7] fmt.Printf("s5: %v\n", s5) // s5: [1 3 5 7 9 11 13] fmt.Printf("s6: %v\n", s6) // s6: [1 3 5 7 9] fmt.Printf("s7: %v\n", s7) // s7: [1 3 5 7 9 11 13] // 切片的长度是指第一个元素到最后一个元素的元素数量 ,容量是指底层数组的容量 fmt.Printf("len:%d cap:%d \n", len(s4), cap(s4)) // len:4 cap:7 // 切片再切片 s8 := s6[:3] fmt.Println("s6:", s6, "s8:", s8) //s6: [1 3 5 7 9] s8: [1 3 5] // 切片是引用类型 s6[0] = 100 fmt.Println("s6:", s6, "s8:", s8) //s6: [100 3 5 7 9] s8: [100 3 5] ``` - make()函数构造切片 利用make()函数构造切片,可以在声明时指定切片长度和容量 切片本质就是一个框,框住了一块连续的内存 切片属于引用类型,真正的数据保存在底层数组中,因此切片不能直接进行比较,如用 == 比较两个切片 nil 值的切片没有底层数组,且此切片的长度和容量均为 0,但是一个长度和容量均为 0 的切片值不一定为 nil 判读一个切片是否为空,使用 len(name) == 0 来判断,不应该使用 s == nil 判断 ```go // 定义切片 s1 := make([]int, 5, 10) fmt.Printf("s1: %v len:%d cap:%d\n", s1, len(s1), cap(s1)) // s1: [0 0 0 0 0] len:5 cap:10 s2 := []int{} s3 := make([]int, 0) fmt.Printf("s2: %v len:%d cap:%d\n", s1, len(s2), cap(s2)) // s2: [0 0 0 0 0] len:0 cap:0 fmt.Printf("s3: %v len:%d cap:%d\n", s1, len(s3), cap(s3)) // s3: [0 0 0 0 0] len:0 cap:0 // 切片的赋值拷贝(引用拷贝) s4 := []int{1, 2, 3} s5 := s4 fmt.Println(s4, s5) // [1 2 3] [1 2 3] s4[0] = 100 fmt.Println(s4, s5) // [100 2 3] [100 2 3] // 切片遍历 // 索引遍历 for i := 0; i < len(s4); i++ { fmt.Printf("s4[i]: %v\n", s4[i]) } // for range遍历 for _, v := range s4 { fmt.Printf("v: %v\n", v) } ``` - 切片扩容策略 可以通过 查看`$GOROOT/src/runtime/slice.go`源码,具体结果如下: 早期比较简单,直接扩容二倍。 ![扩容策略](./public/%E5%88%87%E7%89%87%E6%89%A9%E5%AE%B9%E7%AD%96%E7%95%A5.png) ```go s := []string{"北", "上", "广"} // s[3] = "深" //错误的写法 会导致索引越界错误 // fmt.Printf("s: %v\n", s) // 调用append() 推荐使用原来的切片变量接收返回值(旧瓶装新酒) s = append(s, "深") fmt.Printf("s: %v\n", s) // ss: [北 上 广 深] // 追加多个元素 ss := []string{"AA", "BB"} s1 := append(s, ss...) fmt.Printf("s1: %v\n", s1) // s1: [北 上 广 深 AA BB] ``` - 切片拷贝和切片删除 ```go // 切片拷贝 a1 := []int{1, 3, 5, 7} a2 := a1 var a3 = make([]int, 10) copy(a3, a1) fmt.Printf("a1: %v\n", a1) // a1: [1 3 5 7] fmt.Printf("a2: %v\n", a2) // a2: [1 3 5 7] fmt.Printf("a3: %v\n", a3) // a3: [1 3 5 7 0 0 0 0 0 0] a1[0] = 100 fmt.Printf("a1: %v\n", a1) // a1: [100 3 5 7] fmt.Printf("a2: %v\n", a2) // a2: [100 3 5 7] fmt.Printf("a3: %v\n", a3) // a3: [1 3 5 7 0 0 0 0 0 0] // 删除切片 删除切片a1中索引为2的元素 a1 = append(a1[0:2], a1[3:]...) fmt.Printf("a1: %v len:%d cap:%d\n", a1, len(a1), cap(a1)) // a1: [100 3 7] len:3 cap:4 var ary = [...]int{1, 2, 3, 5} s1 := ary[:] s1 = append(s1[:1], s1[2:]...) // 修改了底层数组(切片不保存值,对应了一个底层数组,底层数组占用了一块连续的内存) fmt.Printf("s1: %v\n", s1) // s1: [1 3 5] fmt.Printf("ary: %v\n", ary) // ary: [1 3 5 5] ``` - 思考 面试题 ```go func main() { var a = make([]int, 5, 10) for i := 0; i < 10; i++ { a = append(a, i) } fmt.Println(a) // 输出结果为 } ``` 3. 指针 go中不存在c语言中的指针操作,只需要知道取值与取址,取地址操作符`&`和取值操作符`*`是一对互补的操作符 在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值。 ```go func main() { //指针取值 a := 10 b := &a // 取变量a的地址,将指针保存到b中 fmt.Printf("type of b:%T\n", b) c := *b // 指针取值(根据指针去内存取值) fmt.Printf("type of c:%T\n", c) fmt.Printf("value of c:%v\n", c) } ``` - 变量、指针地址、指针变量、取地址、取值的相互关系和特性如下: 对变量进行取地址(& )操作,可以获得这个变量的指针变量。 指针变量的值是指针地址。 对指针变量进行取值(* )操作,可以获得指针变量指向的原变量的值。 ```go // 1. &:取地址 n := 18 p := &n fmt.Println(&p) // 0xc000006028 fmt.Printf("p: %T\n", p) // p: *int // 2. *:取值 fmt.Println(*p) // 18 ``` - 指针传值: ```go // 值传递 func foo(n int) { n += 1000 fmt.Println("foo n:", n) } // 引用传递(交换两个值) func swap(m *int, n *int) { temp := *m *m = *n *n = temp } func main() { a := 100 b := 300 foo(a) fmt.Println("--main a:", a) swap(&a, &b) fmt.Println("a=", a, " b=", b) } ``` - new和make GO中对于引用类型的变量,使用时不仅要声明,还要为其分配内存空间,而值类型的声明不需要分配内存空间,因为在声明时已经分配好了内存空间。new()和make()是内建的两个函数,用来分配内存空间。 new 是一个内置函数,很少用,一般用来给基本数据类型申请内存。使用new函数得到一个类型的指针,且该指针对应的值为该类型的零值。签名:`func new(Type) *Type` ```go // new申请一个内存地址 var a1 *int fmt.Printf("a1: %v\n", a1) // a1: var a2 = new(int) fmt.Printf("a2: %v\n", a2) // a2: 0xc0000140e8 *a2 = 100 fmt.Printf("*a2: %v\n", *a2) // *a2: 100 ``` make 只用与slice、map和chan的内存创建,且返回的就是这三个类型本身,不是指针类型,签名:`func make(t Type,size ...IntegerType) Type` ```go // make 只用与slice、map和chan的内存创建,且返回的就是这三个类型本身,不是指针类型 var b map[string]int b = make(map[string]int, 10) b["沙河哪吒"] = 100 fmt.Printf("b: %v\n", b) // b: map[沙河哪吒:100] ``` 4. map map 是一种无序的基于 key-value 的数据结构,Go中map是引用数据类型,必须初始化才能使用。 定义语法:`map[KeyType]ValueType`, KeyType 表示键的类型,ValueType 表示键对应的值类型。 map类型的变量初始值为nil,需要使用make()来分配内存,语法为:`make(map[KeyType]ValueType,[cap])`,cap 容量不是必须的,但是在初始化应该指定。 - map基本语法 ```go // 定义map var m1 = make(map[string]int, 2) m1["id"] = 107829 m1["age"] = 19 // 取值 fmt.Printf("m1: %v\n", m1) // m1: map[age:19 id:107829] fmt.Printf("m1[\"id\"]: %v\n", m1["id"]) // m1["id"]: 107829 fmt.Printf("%v\n", m1["no"]) // 0 ,如果不存在,返回对应值类型的零值 v, ok := m1["age"] if ok { fmt.Printf("v: %v\n", v) // v: 19 } // 遍历 ,遍历的顺序与定义顺序无关 for k, v := range m1 { fmt.Printf("k: %v v:%v\n", k, v) } // 指定顺序遍历 // 删除 delete(m1, "age") fmt.Printf("m1: %v\n", m1) ``` - 切片与map ```go // 元素类型为map的切片 var s1 = make([]map[int]string, 3, 10) s1[0] = make(map[int]string, 1) s1[0][10] = "AA" fmt.Printf("s1: %v\n", s1) // s1: [map[10:AA] map[] map[]] // 值为切片类型的map m2 := make(map[string][]string, 2) m2["1班"] = []string{"tom", "jerry"} m2["2班"] = []string{"李四", "王五", "张三"} fmt.Printf("m2: %v\n", m2) // m2: map[1班:[tom jerry] 2班:[李四 王五 张三]] ``` - 对map顺序化输出 ```go rand.Seed(time.Now().UnixNano()) // 初始化随机数种子 var scoreMap = make(map[string]int, 200) for i := 0; i < 100; i++ { key := fmt.Sprintf("stu%02d", i) // 生成stu开头的字符串 value := rand.Intn(100) // 生成0~99的随机整数 scoreMap[key] = value } //取出map中的所有key存入切片keys var keys = make([]string, 0, 200) for key := range scoreMap { keys = append(keys, key) } //对切片进行排序 sort.Strings(keys) //按照排序后的key遍历map for _, key := range keys { fmt.Println(key, scoreMap[key]) } ``` ## 二、深入Go语言语法 ### 1)函数 函数的意义在于对代码的封装,将逻辑封装抽象为函数,可以优化代码结构,提高开发效率 1. 函数的定义 ```go // 1. 无参数无返回值函数 func f1() { fmt.Println("f1 over") } // 2. 无参数有返回值函数 func f2() (ret int) { return 10 // 显式返回 } // 3. 有参数有返回值函数定义 func f3(x int, y int) (ret int) { // return x + y ret = x + y return // 隐式返回 } // 4. 多个返回值函数定义 func f4() (int, string) { return 10, "AAA" } // 5. 多个形参类型一致时,可以将前面的类型省略 func f5(x, y int) {} // 6. 可变长参数(必须放在形参末尾,传入什么就是什么类型的切片) func f6(x string, y ...int) { fmt.Printf("x: %v\n", x) fmt.Printf("y: %v type:%T\n", y, y) } func main() { sum := f3(1, 2) fmt.Printf("sum: %v \n", sum) // sum: 3 _, s := f4() fmt.Printf("s: %v\n", s) // s: AAA f6("奇数", 1, 3, 5, 7) } ``` 2. defer defer 修饰的语句会延迟在函数结束时处理,多个defer语句逆序执行。 go中的return操作是非原子操作,分为两步,第一步:给返回值赋值,第二步:真正返回,defer语句在这两步之间执行 ```go 思考:下面几个函数的返回值是? func f1() int { n := 100 defer func() { n++ }() return n } func f2() (x int) { defer func() { x++ }() return 5 } func f3() (x int) { defer func(x int) { x++ }(x) return 5 } func f4() (x int) { n := 10 defer func() { n++ }() return n } ``` 面试题: ```go func calc(index string, a, b int) int { ret := a + b fmt.Println(index, a, b, ret) return ret } func main() { x := 1 y := 2 defer calc("AA", x, calc("A", x, y)) x = 10 defer calc("BB", x, calc("B", x, y)) y = 20 } // 问,上面代码的输出结果是?(提示:defer注册要延迟执行的函数时该函数所有的参数都需要确定其值) 个人理解: 压栈过程 AA --- 压栈时要确定参数,执行calc("B", x, y) ^ BB --- 压栈时要确定参数,执行calc("A", x, y) 弹栈过程 AA --- 弹栈,执行calc("AA", x, calc("A", x, y)) ^ BB --- 弹栈,执行calc("BB", x, calc("B", x, y)) ``` ### 2)函数进阶 1. 作用域 全局作用域、函数作用域、代码块作用域 ```go // 全局变量 var x = 100 func f1() { x := 10 /* 函数中查找变量的规则:先在函数内部查找,早不到去函数外部,一直查到全局 函数内部的变量只能在函数作用域内访问 */ x++ fmt.Println(x) } func main() { f1() // 语句块作用域 for i := 0; i < 5; i++ { } // fmt.Println(i) } ``` 2. 函数类型 函数也是一种数据类型,可以作为形参和返回值传递 ```go // 函数类型 func f1() {} func f2() int { return 10 } // 函数也可以作为参数类型 func f3(x func() int) { ret := x() fmt.Printf("f3 ret: %v\n", ret) } // 函数也可以作为返回值 func f4() func(int, int) int { ff := func(a, b int) int { return 10 } return ff } func main() { a := f1 fmt.Printf("a type:%T\n", a) // a type:func() b := f2 fmt.Printf("b type:%T\n", b) // b type:func() int f3(f2) // f3 ret: 10 num := f4()(1, 2) fmt.Printf("num: %v\n", num) // num: 10 } ``` 3. 匿名函数 ```go // 匿名函数 var sum = func(x, y int) { fmt.Printf("x+y: %v\n", x+y) } func main() { sum(1, 2) /* 函数内部是无法声明一个普通函数,因此多用匿名函数 且函数如果只需要执行一次,就可以简写为立即执行函数 */ func(x, y int) { fmt.Println(x + y) }(1, 9) } ``` 4. 闭包 本质: 将函数内部和外部连接起来的桥梁,闭包 = 函数+ 引用环境 ```go // 闭包 函数外部可以访问到函数内部的变量,突破了函数作用域的封锁 func f1() func(string) string { x := make(map[string]string, 3) x["name"] = "李四" x["age"] = "18" return func(key string) string { return x[key] } } func main() { name := f1()("name") fmt.Printf("name: %v\n", name) } ``` ```go func calc(base int) (func(int) int, func(int) int) { add := func(i int) int { base += i return base } sub := func(i int) int { base -= i return base } return add, sub } func main() { f1, f2 := calc(10) fmt.Println(f1(1), f2(2)) //11 9 fmt.Println(f1(3), f2(4)) //12 8 fmt.Println(f1(5), f2(6)) //13 7 } ``` ### 3) 内置函数 1. 内置函数 | 内置函数 | 介绍 | | ----- | ----- | | close | 主要用来关闭channel | | len | 用来求长度,比如string、array、slice、map、channel | | new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 | | make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice | | append | 用来追加元素到数组、slice中 | | panic和recover | 用来做错误处理 | 补充:GO 1.12中目前是没有异常机制的,使用panic和recover模式处理错误,`panic` 可以在任何地方触发,但 `recover` 只有在`defer`调用的函数中有效。 recover()必须搭配defer使用,defer一定要在可能引发panic语句之前定义 ```go func funcA() { fmt.Println("a") } func funcB() { // 1. 打开数据库连接 // 3.尝试修复 defer func() { err := recover() fmt.Printf("err: %v\n", err) }() // 2. 模拟出现错误 panic("error!!!") // 正常代码块 fmt.Println("b") } func funcC() { fmt.Println("c") } func main() { funcA() // a funcB() // err: error!!! funcC() // c } ``` 2. fmt标准库 - 输出函数 | 函数 | 表示 | | ---- | ---- | | Print | 直接输出内容 | | Printf | 支持格式化输出字符串 | | Println | 输出内容结尾会添加一个换行符 | - 格式化字符串 | 格式化字符串 | 值 | | --------- | --- | | %T | 查看类型 | | %c | 查看字符 | | %v | 查看值 | | %b | 查看二进制 | | %o | 查看八进制 | | %d | 查看十进制 | | %x | 查看十六进制 | | %s | 查看字符串 | | %p | 查看指针 | | %f | 查看浮点数 | | %t | 查看布尔值 | | %e | 科学计数法表示 | - 通用(转义)占位符 | 通用(转义)占位符 | 说明 | | ----- | ---- | | %v | 值的默认格式表示 | | %+v | 类似%v,但输出结构体时会添加字段名 | | %#v | 值的Go语法表示 | | %T | 打印值的类型 | | %% | 百分号 | ```go var str = "abc" var num = 12 var g = &num fmt.Printf("%s\n", str) // abc fmt.Printf("%d\n", num) // 12 fmt.Printf("%b\n", num) // 1100 fmt.Printf("%p\n", g) // 0xc000102058 // 整数=>字符 fmt.Printf("%q\n", 65) // 'A' // 浮点数和复数 pai := 3.1415926535 fmt.Printf("%b\n", pai) // 7074237751826244p-51 // 宽度(默认宽度 默认精度) fmt.Printf("%f\n", pai) // 3.141593 // 宽度(默认宽度 精度2) fmt.Printf("%.2f\n", pai) // 3.14 // 宽度(宽度7 精度3) fmt.Printf("%7.3f\n", pai) // 3.142 // 字符串 fmt.Printf("%q\n", "ABC") // "ABC" fmt.Printf("%5.3s\n", "ABCDEFG") // ABC fmt.Printf("%7.7s\n", "ABCDEFG") // ABCDEFG // 通用占位符 fmt.Printf("%%%d\n", num) // %12 fmt.Printf("%#v\n", str) // "abc" ``` - 获取输入 ```go // 获取输入 var s string // 1) Scan() fmt.Print("请输入:") fmt.Scan(&s) fmt.Printf("输入的是: %v\n", s) // 2) fmt.Scanf() 3) fmt.Scanln() var ( name string age int address string ) fmt.Print("请输入:") // fmt.Scanf("%s %d %s\n", &name, &age, &address) fmt.Scanln(&name, &age, &address) fmt.Println("输入的是:", name, age, address) ``` ### 4) 思考:分金币 面试题 [解析](https://www.bilibili.com/video/BV1fz4y1m7Pm?p=46) ```go /* 你有50枚金币,需要分配给以下几个人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。 分配规则如下: a. 名字中每包含1个'e'或'E'分1枚金币 b. 名字中每包含1个'i'或'I'分2枚金币 c. 名字中每包含1个'o'或'O'分3枚金币 d: 名字中每包含1个'u'或'U'分4枚金币 写一个程序,计算每个用户分到多少金币,以及最后剩余多少金币? 程序结构如下,请实现 ‘dispatchCoin’ 函数 */ var ( coins = 50 users = []string{ "Matthew", "Sarah", "Augustus", "Heidi", "Emilie", "Peter", "Giana", "Adriano", "Aaron", "Elizabeth", } distribution = make(map[string]int, len(users)) ) func dispatchCoin() int { } func main() { left := dispatchCoin() fmt.Println("剩下:", left) } ``` ### 5) 递归 ```go // 递归:自己调用自己 /* 1. 实现阶乘 5! = 5*4*3*2*1 4! = 4*3*2*1 3! = 3*2*1 */ func f(n int) int { if n <= 1 { return 1 } return n * f(n-1) } // 2. 走台阶 面试题 // 楼梯有n个台阶,上楼可以一步上1阶,也可以一步上2阶,一共有多少种上楼的方法? func taijie(n uint) uint { if n == 1 { return 1 } if n == 2 { return 2 } return taijie(n-1) + taijie(n-2) } func main() { n := f(5) fmt.Printf("n: %v\n", n) ret := taijie(5) fmt.Printf("ret: %v\n", ret) } ``` ### 6) 自定义类型与类型别名 ```go // 自定义类型 type myInt int64 // 类型别名(类似于byte rune) type twoInt = int func main() { var myNum myInt = 256 fmt.Printf("myNum: %v type:%T\n", myNum, myNum) // myNum: 256 type:main.myInt var twoNum twoInt = 256 fmt.Printf("twoNum: %v type:%T\n", twoNum, twoNum) // twoNum: 256 type:int var c rune // rune类型内置类型别名,是为了代码语义化 c = '为' fmt.Printf("c: %v type:%T\n", c, c) // c: 20026 type:int32 } ``` ### 7) 结构体 结构体是一种自定义数据类型,可以封装多个基本数据类型。 使用type和struct来定义结构体: ``` type 类型名 struct { 字段名 字段类型 字段名 字段类型 ... } ``` 匿名结构体: ``` var 变量名 struct { 字段名 字段类型 ... } ``` 结构体初始化: ```go // 结构体初始化 type person struct { name string age string } // 1. key - value初始化 var p = person{name: "tom", age: "22"} fmt.Printf("p: %v\n", p) // 2. 使用值列表的形式初始化 值列表和结构体定义顺序要一致 var p2 = person{"tom", "22"} fmt.Printf("p2: %v\n", p2) // 3. .的方式初始化 var p3 = person p3.name = "jerry" fmt.Printf("p2: %v\n", p2) ``` 结构体值传递与引用传递: ```go type person struct { name, age string } func f(x person) { x.name = "jerry" fmt.Printf("f x: %v\n", x) } func f2(x *person) { // (*x).name = "jerry" // 语法糖 x.name = "jerry" fmt.Printf("f2 x: %v\n", x) } func main() { var p = person{name: "tom", age: "22"} // 值拷贝 f(p) fmt.Printf("p: %v\n", p) // 引用传递 f2(&p) fmt.Printf("p: %v\n", p) } ``` new() ```go type person struct { name, age string } // go支持 new() 创建结构体实例,new() 返回的是结构体的地址,且支持直接对结构体指针使用.的方式访问成员 var p2 = new(person) fmt.Printf("type: %T\n", p2) p2.name = "王五" fmt.Printf("p2: %v\n", p2) ``` 结构体是一块连续的内存,深入了解:[Go语言结构体的内存对齐现象和对齐策略](https://www.liwenzhou.com/posts/Go/struct-memory-layout/) ```go type ints struct { x int8 // 1byte = 8bit y int8 z int8 } func main() { var is = ints{10, 42, 5} fmt.Printf("x: %p\n", &(is.x)) // x: 0xc000014098 fmt.Printf("y: %p\n", &(is.y)) // y: 0xc000014099 fmt.Printf("z: %p\n", &(is.z)) // z: 0xc00001409a } ``` ### 8) 模拟实现构造函数 GO提倡面向接口,暂时无法实现面向对象的特点,但是可以通过struct实现。 构造函数:返回一个结构体变量的函数 结构体是值类型,赋值的时候都是值拷贝 1. 定义方法 ```go type person struct { name string age int } // 构造函数(约定用new开头) // 返回的是结构体还是结构体指针 // 当结构体较大时,推荐返回结构体指针,减少内存开销 // func newPerson(name string, age int) *person { func newPerson(name string, age int) person { return person{name, age} } func main() { var p = newPerson("tom", 10) fmt.Printf("p: %v\n", p) } ``` 2. 方法和接收者 方法(作用于特定类型的函数),接收者表示调用该方法的具体类型变量,多用于类型名首字母小写表示,不推荐使用this 定义方法:`func (接收者) 方法名(参数列表) (返回参数){ //函数体 }` ```go // 结构体 type dog struct { name string color string } // 方法 func (d dog) say() { fmt.Println(d.name, "汪汪汪") } // 构造函数 func newDog(name string, color string) dog { return dog{name, color} } func main() { var d = newDog("柴犬", "yellow") d.say() } ``` - 指针接收者和值接收者 ```go var d = newDog("柴犬", "yellow") func newDog(name string, color string) dog { return dog{name, color} } var d = newDog("柴犬", "yellow") d.changeName() fmt.Printf("d: %v\n", d) // d: {柴犬 yellow} d.changeColor() fmt.Printf("d: %v\n", d) // d: {柴犬 black} ``` - 自定义类型添加方法 ```go // 自定义类型 type myInt uint16 // 给自定义类型添加方法 func (i myInt) say() { fmt.Println("type is a uint16") } func main() { var x myInt x.say() } ``` 3. 学生管理系统(crud) ```go // 自定义类型 type myStudents []student type student struct { id, age uint name, class, address string } func (s *myStudents) get() { fmt.Println(*s) } func (s *myStudents) add(newStu student) { *s = append(*s, newStu) } func (s *myStudents) deleteById(ids ...int) { for _, idsValue := range ids { for i, v := range *s { if uint(idsValue) == v.id { (*s) = append((*s)[:i], (*s)[i+1:]...) break } } } } func main() { var stus = make(myStudents, 0, 1) s1 := student{id: 1203, age: 18, name: "学生1", class: "1班", address: "xxxxxx"} s2 := student{id: 1211, age: 20, name: "学生2", class: "1班", address: "xxxxxx"} s3 := student{id: 1230, age: 18, name: "学生3", class: "1班", address: "xxxxxx"} stus.add(s1) stus.add(s2) stus.add(s3) stus.deleteById(1203) stus.get() // [{1211 20 学生2 1班 xxxxxx} {1230 18 学生3 1班 xxxxxx}] } ``` 4. 匿名结构体和结构体嵌套 匿名结构体 ```go // 匿名字段 —— 适用于字段少且简单的场景(不常见) type person struct { string int } func main() { p1 := person{"serson1", 19} fmt.Printf("p1: %v\n", p1) // p1: {serson1 19} fmt.Printf("p1: %v\n", p1.string) // p1: serson1 } ``` 结构体嵌套 ```go // 嵌套结构体 type person struct { name string age int } type student struct { person // 匿名嵌套结构体 id uint class string } func main() { s1 := student{person: person{name: "李四", age: 34}, id: 19035160237, class: "1班"} fmt.Printf("s1: %v\n", s1) // s1: {{李四 34} 19035160237 1班} // 匿名嵌套结构体可以直接访问 自己找不到字段,就会自动去嵌套的结构体查找。 fmt.Printf("s1 name: %v\n", s1.person.name) // s1 name: 李四 fmt.Printf("s1 name: %v\n", s1.name) // s1 name: 李四 } ``` ### 9) 模拟实现继承 ```go // 利用匿名结构体模拟实现继承 type animal struct { color string breed string } type dog struct { animal name string price int } func (d *dog) say() { fmt.Println(" my color is", d.color) } func main() { d1 := dog{animal: animal{"black", "边牧"}, name: "xxx", price: 1230} d1.say() // my color is black } ``` ### 10) 结构体与JSON ```go // 字段首字母小写,json包访问不到字段 // type person struct { // name string // age int // address string // } // 字段首字母大写,可以让json包访问数据 type person struct { Name string `json:"name"` Age int `json:"age"` Address string `json:"address"` } func main() { p1 := person{ Name: "周玲", Age: 18, Address: "xxxx", } // 序列化 data, err := json.Marshal(p1) if err != nil { fmt.Printf("err: %v\n", err) return } fmt.Printf("data: %v type:%T\n", data, data) fmt.Printf("data: %v\n", string(data)) // 反序列化 str := "{\"name\":\"李四\",\"class\":{\"id\":12037318,\"class\":\"1班\"}}" var p2 person json.Unmarshal([]byte(str), &p2) // 接收两个参数:byte字节切片和结构体指针 传指针是为了在函数内部修改p2 fmt.Printf("p2: %v\n", p2) } ``` ### 11) 接口与多态 1. 应用场景(只有在多个具体类型需要以相同的方式处理时才定义接口) 三角形、四边形、圆形都可以计算周长和面积,能不能将这几个当作图形来处理 微信支付、支付宝支付都是一种支付方式,能不能将这两种方式放到一个函数中处理 2. 接口 接口时一种特殊的类型,用来给变量、参数、返回值等设置类型 接口的定义: ``` type 接口名 interface { 方法名1(参数1,参数2...)(返回值1,返回值2...) ... } ``` 接口的实现:一个变量实现了接口中定义的所有方法,那么此变量就实现了这个借口,称为这个借口类型的变量 ```go // 任何品牌的车都会跑(一个方法要接收不同的结构体类型) // 接口 type car interface { drive() } // 结构体 type BYD struct { name string } type VW struct { name string } // 方法 func (b BYD) drive() { fmt.Println(b.name, "行驶中") } func (v VW) drive() { fmt.Println(v.name, "行驶中") } // 行驶方法 func carDrive(c car) { c.drive() } func main() { car1 := BYD{"比亚迪"} car2 := VW{"大众"} carDrive(car1) // 比亚迪 行驶中 carDrive(car2) // 大众 行驶中 } ``` 值接收者和指针接收者 两者的区别:使用值接收者实现接口,结构体类型和指针类型的变量都可以接收;反之只能存结构体指针类型的变量。 ```go // 接口 type animal interface { move() eat(foot string) } // 结构体 type cat struct { name string } type dog struct { name string } // 方法(使用指针接收者) func (d *dog) move() { fmt.Println(d.name, "行驶中") } func (d *dog) eat(foot string) { fmt.Println(d.name, "吃", foot) } func main() { var an animal d1 := dog{"汪汪"} an = &d1 an.eat("骨头") } ``` 接口和类型的关系 多个类型可以实现同一个接口;一个类型可以实现多个接口 ```go type xiyiji interface { xi hong } type xi interface { xi() } type hong interface { honggan() } // 结构体 type MD struct{} // 方法(使用指针接收者) func (m MD) xi() { fmt.Println("洗衣服") } func (m MD) honggan() { fmt.Println("烘干衣服") } func main() { var an xiyiji xiyiji1 := MD{} an = xiyiji1 an.xi() an.honggan() } ``` 空接口 定义空接口`interface{}` 空接口没有定义任何方法,因此任何类型都实现了空接口,空接口类型的变量可以存储任意类型的变量 ```go func showData(d interface{}) { fmt.Printf("d: %v\n", d) } func main() { m1 := make(map[string]interface{}, 10) m1["name"] = "人物1" m1["age"] = 10000 m1["merried"] = true m1["hobby"] = [...]string{"火锅", "寿司", "烧烤"} fmt.Printf("m1: %v\n", m1) showData("this is string") showData([]int{1, 3, 5, 7}) showData(m1) } ``` 空接口类型`interface{}`可以作为值也可以作为函数参数,作为函数参数时,判断参数类型使用类型断言机制,语法:`x.(T)` ```go func showData(d interface{}) { // v, ok := d.(string) // if !ok { // // } else { // fmt.Printf("v: %v\n", v) // } switch t := d.(type) { case int: fmt.Println("是一个整数", t) case string: fmt.Println("是一个字符串", t) case map[string]int: fmt.Println("是一个map", t) } } func main() { showData("this is string") // 是一个字符串 this is string showData(222) // 是一个整数 222 showData(map[string]int{"num": 11}) // 是一个map map[num:11] } ``` ### 12) package 包管理 1. import导包: 报的路径从 GOPAATH/src 后面的路径开始写。路径分隔符使用/ 想要被别的包调用的标识符都要首字母大写 导入包的时候可任意指定别名 导入包时候不想使用包颞部的标识符,则需要匿名导入 每个包导入时都会自动执行一个名为inti()的函数,此函数没有参数也没有返回值更不能手动调用 多个包中定义了init函数,执行顺序如下: ![init函数执行流程](./public/init.png) ```go import ( // 1、普通导包方式 "hello/6-init/lib1" // 2、匿名导包 // _ "hello/6-init/lib2" // 3、解构 导包 // . "hello/6-init/lib2" // 4、别名导包(推荐) mylib2 "hello/6-init/lib2" ) // 1)普通导包调用(包名.方法名) lib2.Lib1Test() // 2)匿名导包调用(包名.方法名),不调用也不会编译失败,会执行init方法 lib2.Lib1Test() // 3)解构导包(方法名) Lib2Test() // 4)别名导包调用(别名.方法名) mylib2.Lib2Test() ``` ## 三、Go语言语法高阶 ### 1)文件操作 打开文件:`fileObj, err := os.Open(fileSrc)` 关闭文件:`fileObj.Close()` 读取文件: ```go var fileSrc string = "./demo.txt" // 利用Read()读取文件 func readFromFile() { // 打开文件 fileObj, err := os.Open(fileSrc) // 错误处理 if err != nil { fmt.Println("err", err) return } // 关闭文件 defer fileObj.Close() for { // 读取文件 var tmp = make([]byte, 128) n, err := fileObj.Read(tmp[:]) // 读取到文件末尾 if err == io.EOF { return } // 错误处理 if err != nil { fmt.Println(err) return } // fmt.Println("此次读的字节数", n) fmt.Println(string(tmp[:n])) // 循环控制 if n < 128 { return } } } // 利用Bufio读取文件 func readFromFileByBufio() { fileObj, err := os.Open(fileSrc) if err != nil { fmt.Println(err) } defer fileObj.Close() reader := bufio.NewReader(fileObj) for { line, err := reader.ReadString('\n') if err == io.EOF { return } if err != nil { fmt.Println(err) } fmt.Printf("line: %v\n", line) } } // 利用 ioutil 读取文件 func readFromFileByIoutil() { ret, err := ioutil.ReadFile(fileSrc) if err != nil { fmt.Println(err) } fmt.Printf("ret: %v\n", string(ret)) } // 文件操作 func main() { // readFromFile() // readFromFileByBufio() readFromFileByIoutil() } ``` 写入文件: ```go var fileSrc string = "./demo.txt" // 利用OpenFile()写文件 func writeToFile() { // fileObj, err := os.Open(fileSrc) // fileObj, err := os.OpenFile(fileSrc, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) fileObj, err := os.OpenFile(fileSrc, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { fmt.Printf("err: %v\n", err) return } defer fileObj.Close() // Write fileObj.Write([]byte("aa bb")) // WriteString fileObj.WriteString("12122") } // 利用bufio.NewWriter()创建缓冲区写入 func writeToFileBybufio() { fileObj, err := os.OpenFile(fileSrc, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { fmt.Printf("err: %v\n", err) return } defer fileObj.Close() // 创建一个写的对象 wr := bufio.NewWriter(fileObj) wr.WriteString("hello say") // 写入缓存 wr.Flush() // 缓存中的内容写入文件 } // 利用ioutil.WriteFile()将字节切片写入(默认会覆盖写入) func writeToFileByioutil() { str := "abcdfff" err := ioutil.WriteFile(fileSrc, []byte(str), 0666) if err != nil { fmt.Printf("err: %v\n", err) return } } // 文件操作 func main() { // writeToFile() // writeToFileBybufio() writeToFileByioutil() } ``` 读取一个文件,然后在中间某个字符后插入字符: ```go // 在文件中间添加内容 const fileSrc = "./demo.txt" func f1() { // 打开文件 fileObj, err := os.OpenFile(fileSrc, os.O_RDWR, 0644) if err != nil { fmt.Println("openfile err:", err) return } // 创建临时文件 tmpFile, err := os.OpenFile("./sb.tmp", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644) if err != nil { fmt.Println("openfile err:", err) return } defer tmpFile.Close() // 读取文件写入临时文件 var ret [1]byte n, err := fileObj.Read(ret[:]) if err != nil { fmt.Printf("read file err: %v\n", err) return } // 源文件插入之前的文本写入临时文件 tmpFile.Write(ret[:n]) // 写入要插入的内容 var str = []byte{'^'} tmpFile.Write(str) // 源文件插入之后的文本写入临时文件 var ss [1024]byte for { n, err := fileObj.Read(ss[:]) if err == io.EOF { tmpFile.Write(ss[:n]) break } if err != nil { fmt.Println("read file err:", err) return } tmpFile.Write(ss[:n]) } // 关闭源文件 fileObj.Close() tmpFile.Close() // 重命名临时文件 os.Rename("./sb.tmp", "./sb.txt") } func f2() { fileObj, err := os.OpenFile("./demo.txt", os.O_RDWR, 0644) if err != nil { return } defer fileObj.Close() // func (f *File) Seek(offset int64, whence int) (ret int64, err error) // Seek设置下一次读/写的位置。offset为相对偏移量, // 而whence决定相对位置:0为相对文件开头,1为相对当前位置,2为相对文件结尾。 // 它返回新的偏移量(相对开头)和可能的错误。 fileObj.Seek(2, 0) // 从文件开头第二个字节插入光标 // 正常读取文件 var ss [1024]byte n, err := fileObj.Read(ss[:]) if err == io.EOF { fmt.Println("") } fmt.Printf("%v\n", string(ss[:n])) } func main() { // f1() f2() } ``` ### 2) 时间日期操作 ```go // 获取时间对象 now := time.Now() // 获取格式化日期 fmt.Printf("now: %v\n", now) fmt.Println("年", now.Year()) fmt.Println("月", now.Month()) fmt.Println("日", now.Day()) fmt.Println("时", now.Hour()) fmt.Println("分", now.Minute()) fmt.Println("秒", now.Second()) // 获取时间戳(19700101 8:00) timeStamp1 := now.Unix() timeStamp2 := now.UnixNano() //精确到纳秒 fmt.Printf("timeStamp1: %v\n", timeStamp1) fmt.Printf("timeStamp2: %v\n", timeStamp2) // 时间戳解析 timeStr := time.Unix(1660028148, 0) fmt.Printf("timeStr: %v\n", timeStr) // 时间间隔 // 计算一个小时之后的时间 fmt.Println(time.Second) // Golang Time包中定义的常量 oneHourTime := now.Add(24 * time.Hour) fmt.Printf("oneHourTime: %v\n", oneHourTime) // 求两个时间的间隔 d := now.Sub(oneHourTime) fmt.Printf("d: %v\n", d) // 定时器 timer := time.Tick(3 * time.Second) for t := range timer { // 每隔一秒打印一次 fmt.Printf("t: %v\n", t) } // 时间格式化 //其它多数语言使用Y-m-d H:M:S的格式化模板,Go中使用Go诞生的时间:2006-1-2 15:04 fmt.Println(now.Format("2006/01/02 15/04/05")) // 24小时制 fmt.Println(now.Format("2006-01-02 03:04:05 ")) // 12小时制 fmt.Println(now.Format("2006-01-02 15:04:05.000")) // 按照对应格式解析字符串 timeObj, err := time.Parse("2006-01-02", "2022-08-09") if err != nil { // 错误处理 return } fmt.Printf("timeObj: %v\n", timeObj) // 系统睡眠 time.Sleep(1000) time.Sleep(time.Second) ``` 实现查询时间差 ```go // 获取当前时间 nowTimeObj := time.Now() // 指定时区 loc, err := time.LoadLocation("Asia/Shanghai") if err != nil { return } // 获取另一个时间 futerTime, err := time.ParseInLocation("2006-01-02 15:04", "2022-08-09 20:00", loc) fmt.Printf("timeObj: %v\n", futerTime) fmt.Printf("nowTimeObj: %v\n", nowTimeObj) // 时间对象相减 td := nowTimeObj.Sub(futerTime) fmt.Printf("td: %v\n", td) ``` ### 3)反射(了解) 反射指在程序运行期间对程序本身进行访问和修改的能力。程序在编译期间,变量被转换为内存地址,变量名不会被编译器写入可执行部分。在运行程序期间,程序无法获取自身信息。 Go程序在运行期间使用reflect包访问程序的反射信息。 关于reflect包:在Go语言的反射机制中,任何接口值都由是一个**具体类型**和**具体类型的值**两部分组成。在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由`reflect.Type`和`reflect.Value`两部分组成,并且reflect包提供了`reflect.TypeOf`和`reflect.ValueOf`两个函数来获取任意对象的Value和Type。 1. TypeOf() 在Go语言中,使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。 ```go func reflectType(x interface{}) { v := reflect.TypeOf(x) fmt.Printf("type:%v\n", v) } func main() { var a float32 = 3.14 reflectType(a) // type:float32 var b int64 = 100 reflectType(b) // type:int64 } ``` 2. type name和type kind 在反射中关于类型还划分为两种:类型(Type)和种类(Kind) 因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind) Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空 ```go func reflectType(x interface{}) { t := reflect.TypeOf(x) fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind()) } func main() { var a *float32 // 指针 var b myInt // 自定义类型 var c rune // 类型别名 reflectType(a) // type: kind:ptr reflectType(b) // type:myInt kind:int64 reflectType(c) // type:int32 kind:int32 type person struct { name string age int } type book struct{ title string } var d = person{ name: "沙河", age: 18, } var e = book{title: "《xxxxx》"} reflectType(d) // type:person kind:struct reflectType(e) // type:book kind:struct } ``` 3. ValueOf() `reflect.ValueOf()`返回的是`reflect.Value`类型,其中包含了原始值的值信息。 `reflect.Value`与原始值之间可以互相转换 - 通过反射获取值 ```go func reflectValue(x interface{}) { v := reflect.ValueOf(x) k := v.Kind() switch k { case reflect.Int64: // v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换 fmt.Printf("type is int64, value is %d\n", int64(v.Int())) case reflect.Float32: // v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换 fmt.Printf("type is float32, value is %f\n", float32(v.Float())) case reflect.Float64: // v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换 fmt.Printf("type is float64, value is %f\n", float64(v.Float())) } } func main() { var a float32 = 3.14 var b int64 = 100 reflectValue(a) // type is float32, value is 3.140000 reflectValue(b) // type is int64, value is 100 // 将int类型的原始值转换为reflect.Value类型 c := reflect.ValueOf(10) fmt.Printf("type c :%T\n", c) // type c :reflect.Value } ``` - 通过反射设置值 ```go func reflectSetValue1(x interface{}) { v := reflect.ValueOf(x) if v.Kind() == reflect.Int64 { v.SetInt(200) //修改的是副本,reflect包会引发panic } } func reflectSetValue2(x interface{}) { v := reflect.ValueOf(x) // 反射中使用 Elem()方法获取指针对应的值 if v.Elem().Kind() == reflect.Int64 { v.Elem().SetInt(200) } } func main() { var a int64 = 100 // reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value reflectSetValue2(&a) fmt.Println(a) } ``` 4. isNil()和isValid() - `func (v Value) IsNil() bool`IsNil()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。 - `func (v Value) IsValid() bool`IsValid()返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。 IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效。 ```go func main() { var a *int // *int类型空指针 fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil()) // nil值 fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) b := struct{}{}// 实例化一个匿名结构体 // 尝试从结构体中查找"abc"字段 fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid()) // 尝试从结构体中查找"abc"方法 fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid()) c := map[string]int{} // map // 尝试从map中查找一个不存在的键 fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid()) } ``` 5. 结构体反射(难点) StructField类型 -- 用来描述结构体中的一个字段的信息。 ```go type student struct { Name string `json:"name"` Score int `json:"score"` } func main() { stu1 := student{ Name: "小王子", Score: 90, } t := reflect.TypeOf(stu1) fmt.Println(t.Name(), t.Kind()) // student struct // 通过for循环遍历结构体的所有字段信息 for i := 0; i < t.NumField(); i++ { field := t.Field(i) fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json")) } // 通过字段名获取指定结构体字段信息 if scoreField, ok := t.FieldByName("Score"); ok { fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json")) } } ``` ### 4)并发 1. 并发与并行 串行:对某项工作按步骤的去执行 并发:同一时间段内执行多个任务 并行:同一时刻执行多个任务 2. 进程、线程和协程 进程(process):程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。 线程(thread):操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位。 协程(coroutine):非操作系统提供而是由用户自行创建和控制的用户态‘线程’,比线程更轻量级。 3. 并发模型 常见的并发模型有这几种: 线程&锁模型 Actor模型 CSP模型 Fork&Join模型 ... Go语言中的并发程序主要是通过基于CSP(communicating sequential processes)的goroutine和channel来实现,当然也支持使用传统的多线程共享内存的并发方式。 Goroutine 是 Go 语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。 Goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建。 4. 使用goroutine 在Go语言中不需要去自己写进程、线程、协程,当需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个 goroutine 去执行这个函数就可以了。 goroutine结束时间:goroutine对应的函数执行结束/主函数执行完毕,主函数创建的gotroutine执行结束 ```go // mian函数会启动一个主 gotoutine 去执行 func main() { // 开启一个gotoutine协程执行函数 for i := 0; i < 10; i++ { go func(i int) { fmt.Println("func(){}()", i) }(i) // 不传入参数会形成闭包 } fmt.Println("main") // 使用time.Sleep让 main goroutine 等待 hello goroutine执行结束是不优雅的,后面使用sync包解决 time.Sleep(1 * time.Second) } ``` 5. goroutine 同步 ```go func f() { rand.Seed(int64(time.Now().UnixNano())) // 随机数种子,保证程序每次创新执行都不一样 for i := 0; i < 5; i++ { r1 := rand.Int() r2 := rand.Intn(20) // 0<= x <20 fmt.Printf("r1: %v r2: %v\n", r1, r2) } } var wg sync.WaitGroup func main() { // 随机数的用法 // f() for i := 0; i < 5; i++ { wg.Add(1) // 计数器加一 // 每个goroutine协程随机睡眠x秒 go func(i int) { defer wg.Done() // 计数器减一 time.Sleep(time.Duration(rand.Intn(20)) * time.Millisecond) // time.Sleep接收一个Duration类型的参数,需要轻质类型转换 fmt.Printf("goroutine i: %v wake up\n", i) }(i) } // 如何直到上面的goroutine全部结束? wg.Wait() // 等待计数器为0 } ``` 6. goroutine与线程 goroutine是用户态的线程。线程通常是指系统的线程。 一个操作系统线程对应用户态的多个goroutine;go程序可以同时使用多个操作系统线程;goroutine和OS线程是m:n的关系。 两者的区别在于:可增长的栈 操作系统线程一般都有固定的栈内存(通常为2MB),而 Go 语言中,一个 goroutine 的初始栈空间很小(一般为2KB),并且 goroutine 的栈不是固定的,可以动态增大或缩小, Go 的 runtime 会自动为 goroutine 分配合适的栈空间。 7. GMP调度 目前 Go 语言的调度器采用的是 GPM 调度模型。 ![Go并发调度模型](./public/gpm.png) 其中: G:表示 goroutine,每执行一次go f()就创建一个 G,包含要执行的函数和上下文信息。 全局队列(Global Queue):存放等待运行的 G。 P:表示 goroutine 执行所需的资源,最多有 GOMAXPROCS 个。 P 的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建 G 时,G 优先加入到 P 的本地队列,如果本地队列满了会批量移动部分 G 到全局队列。 M:OS线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。 Goroutine 调度器和操作系统调度器是通过 M 结合起来的,每个 M 都代表了1个内核线程,操作系统调度器负责把内核线程分配到 CPU 的核上执行。 P-M:两者一一对应,发生阻塞时,发生阻塞所在的 P 会尝试新建一个 M,将其它的 G 转移挂载到这个 M 上,当则色完成或者其已经死亡时回收旧的 M。 GOMAXPROCS:Go运行时调度器使用GOMAXPROCS参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数,可以通过`runtime.GOMAXPROCS`函数设置当前程序并发时占用的 CPU逻辑核心数 ### 5)channel类型 函数之间需要交换数据才能实现并发的意义,虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,很多并发模型中必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。 Go语言采用的并发模型是CSP(Communicating Sequential Processes),提倡**通过通信共享内存**而不是通过共享内存而实现通信(全局变量 --> 读/写)。 如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。 channel 是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是**声明channel的时候需要为其指定元素类型**。 声明 channel 类型:`var 变量名称 chan 元素类型` 通道是引用类型,通道类型的空值是nil,声明通道后,必须使用make函数初始化之后才能使用`make(chan 元素类型,[缓冲大小])` 通道使用完毕后,关闭通过`close()`关闭,但不是必须的(垃圾回收机制会回收) 1. 通道的操作: ```go b = make(chan int) //make函数初始化通道(无缓冲区) go func() { var a int a = <-b // 从通道中取值 fmt.Printf("a: %v\n", a) }() b <- 200 // 发送到通道中,此时main函数会阻塞,等待通道中的值被获取 ``` 2. 有缓冲通道与无缓冲通道的区别 使用make初始化Channel,可以设置容量:`make(chan int, 100)`,容量(capacity)代表Channel容纳的最多的元素的数量,代表Channel的缓存的大小。 如果没有设置容量,或者容量设置为0, 说明Channel没有缓存,只有sender和receiver都准备好了后它们的通讯(communication)才会发生(Blocking)。如果设置了缓存,就有可能不发生阻塞, 只有buffer满了后 send才会阻塞, 而只有缓存空了后receive才会阻塞。一个nil channel不会通信。 可以通过内建的close方法手动关闭Channel。 ```go // 下面是两个goroutine 通过channel通信 var wg sync.WaitGroup func main() { ch1 := make(chan int, 100) wg.Add(2) go func() { defer wg.Done() for i := 0; i < 100; i++ { ch1 <- i } close(ch1) }() ch2 := make(chan int, 100) ch2 <- 10 go func() { defer wg.Done() for v := range ch1 { fmt.Printf("ch2 <- ch1 : %v\n", v) ch2 <- v * v } close(ch2) }() for v := range ch2 { fmt.Printf("main <- ch2 : %v\n", v) } wg.Wait() } ``` ```go // 接收channel中的值 并判断channel状态 ch1 := make(chan int, 1) ch1 <- 10 close(ch1) // 10 true x, ok := <-ch1 fmt.Println(x, ok) y, ok := <-ch1 fmt.Println(y, ok) // 0 false ``` 3. 单双向通道 单向通道通常被用来限制函数参数(限制某个函数内对通道的读取/写入行为) ```go var wg sync.WaitGroup func getChannel(c chan int) chan<- int { return c } func setChannel(c chan int, n int) <-chan int { c <- n return c } func main() { wg.Add(2) go func() { defer wg.Done() var c1 = make(chan int, 10) temp := setChannel(c1, 10) // 返回只读通道 fmt.Printf("temp: %T\n", temp) // temp: <-chan int x, _ := <-c1 fmt.Printf("x:%d\n", x) // intx:10 // 尝试写入 err:cannot send to receive-only channel temp // temp <- 100 // }() var c2 = make(chan int, 5) go func() { defer wg.Done() c2 <- 11 temp := getChannel(c2) // 返回只写通道 temp <- 100 // 尝试读取 err:cannot receive from send-only channel temp // n := <-temp // }() num := <-c2 fmt.Printf("num: %v\n", num) // num: 11 num2 := <-c2 fmt.Printf("num2: %v\n", num2) // num2: 100 wg.Wait() } ``` 4. channel常见异常 ![通道操作结果表](./public/channel%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C%E7%BB%93%E6%9E%9C.png) **注意**:对已经关闭的通道再执行 close 也会引发 panic。 5. worker pool(goroutine池) ```go func syncFun(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf(" 任务:%d 开始, %v <- job \n", id, j) time.Sleep(1 * time.Second) results <- j * 2 fmt.Printf(" 任务:%d 结束, results <- %v\n", id, j*2) } } func main() { jobs := make(chan int, 100) results := make(chan int, 100) // 开启3个goroutine for i := 0; i < 3; i++ { go syncFun(i, jobs, results) } // 发送5个任务 for j := 0; j < 5; j++ { jobs <- j } close(jobs) // 输出结果 (for...range 会 panic) for a := 0; a < 5; a++ { ret := <-results fmt.Printf("ret: %v\n", ret) } } ``` ```go 结果: worker:2 start job: 0 worker:1 start job: 2 worker:0 start job: 1 worker:0 end job: 1 worker:0 start job: 3 worker:2 end job: 0 worker:2 start job: 4 worker:1 end job: 2 worker:1 start job: 5 worker:2 end job: 4 worker:0 end job: 3 worker:1 end job: 5 ``` 运行结果: ``` 任务:2 开始, 0 <- job 任务:1 开始, 2 <- job 任务:0 开始, 1 <- job 任务:0 结束, results <- 2 任务:0 开始, 3 <- job 任务:1 结束, results <- 4 ret: 2 ret: 0 ret: 4 任务:2 结束, results <- 0 任务:1 开始, 4 <- job 任务:0 结束, results <- 6 ret: 6 任务:1 结束, results <- 8 ret: 8 ``` 6. select多路复用 如果需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以被接收那么当前 goroutine 将会发生阻塞 Select 类似于 switch 语句,它也有一系列 case 分支和一个默认的分支。每个 case 分支会对应一个通道的通信(接收或发送)过程。 select 会一直等待,直到其中的某个 case 的通信操作完成时,就会执行该 case 分支对应的语句。具体格式如下: ```go select { case <-ch1: //... case data := <-ch2: //... case ch3 <- 10: //... default: //默认操作 } ``` Select 语句具有以下特点。 可处理一个或多个 channel 的发送/接收操作。 如果多个 case 同时满足,select 会**随机**选择一个执行。(下面第二段代码) 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。 ```go func main() { ch := make(chan int, 1) for i := 0; i < 10; i++ { select { case x := <-ch: fmt.Printf("x: %v\n", x) case ch <- i: // 写入时注意通道缓冲区 default: fmt.Print("default") } } } /* x: 0 x: 2 x: 4 x: 6 x: 8 */ ``` ```go func main() { ch := make(chan int, 10) for i := 0; i < 10; i++ { select { case x := <-ch: fmt.Printf("x: %v\n", x) case ch <- i: // 写入时注意通道缓冲区 default: fmt.Print("default") } } } // 当缓冲区变为10时,上面这段代码的运行结果是不确定的!!! ``` 7. 并发同步和锁 1) **sync.Mutex** | 互斥锁 互斥锁能够保证同一时间只有一个 goroutine 可以访问共享资源。Go 语言中使用sync包中提供的Mutex类型来实现互斥锁。 多个 goroutine 同时等待一个锁时,下一个 goroutine 唤醒的策略是随机的。 ```go var ( y int = 0 wg sync.WaitGroup lock sync.Mutex // 互斥锁 ) func addToY() { defer wg.Done() for i := 0; i < 5000; i++ { lock.Lock() // 加锁 y = y + 1 lock.Unlock() // 释放锁 } } func main() { wg.Add(2) go addToY() go addToY() wg.Wait() fmt.Printf("y: %v\n", y) } ``` 2) **sync.RWMutex** | 读写互斥锁 上面的互斥锁是完全互斥的,当我们并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的。 读写锁:当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;而当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。 ```go // 读写互斥锁(读 > 写的场景) var ( Num = 0 wg sync.WaitGroup rwLock sync.RWMutex // 读写互斥锁 ) func write(x int) { defer wg.Done() // 加锁 // lock.Lock() rwLock.Lock() Num += x time.Sleep(3 * time.Millisecond) // 释放锁 // lock.Unlock() rwLock.Unlock() } func rend() { defer wg.Done() // 加锁 rwLock.RLock() fmt.Printf("Num: %v\n", Num) time.Sleep(time.Millisecond) // 释放锁 rwLock.RUnlock() } func main() { startNow := time.Now() for i := 0; i < 10; i++ { go write(1) wg.Add(1) } time.Sleep(time.Second) for i := 0; i < 1000; i++ { go rend() wg.Add(1) } wg.Wait() fmt.Println(time.Now().Sub(startNow)) } ``` 3) **sync.Once()**| 多个goroutine执行,但只能执行一次(如关闭通道) ```go // 读写互斥锁(读 > 写的场景) var ( icons map[string]image.Image Once sync.Once ) func loadIcons() { icons = map[string]image.Image{ "left": loadIcon("left.png"), "center": loadIcon("center.png"), "right": loadIcon("right.png"), "down": loadIcon("down.png"), } } // Icon被多个goroutine调用时不是并发安全的,使用once保证并发安全 func Icon(name string) image.Image { // if icons == nil { // loadIcons() // } Once.Do(loadIcons) // 接收一个无参数五返回值的函数,有需要可以使用闭包 return icons[name] } ``` 4) **sync.Map()** | 内置的map是并发不安全的,并发操作map使用sync.Map() ```go var m = make(map[string]int) // 原生map func get(key string) int { return m[key] } func set(key string, value int) { m[key] = value } func main() { wg := sync.WaitGroup{} for i := 0; i < 20; i++ { wg.Add(1) go func(n int) { key := strconv.Itoa(n) set(key, n) fmt.Printf("key: %v value: %v\n", key, get(key)) wg.Done() }(i) } wg.Wait() } // fatal error: concurrent map writeskey ``` Go语言的sync包中提供了一个**开箱即用**的并发安全版 map——sync.Map。开箱即用表示其不用像内置的 map 一样使用 make 函数初始化就能直接使用。同时sync.Map内置了诸如**Store、Load、LoadOrStore、Delete、Range**等操作方法。 ```go var m2 = sync.Map{} func main() { wg := sync.WaitGroup{} // sync.Map for i := 0; i < 20; i++ { wg.Add(1) go func(n int) { key := strconv.Itoa(n) m2.Store(key, n) // 使用内置的store方法设置键值对 value, _ := m2.Load(key) // 使用内置的Load方法获取键值对 fmt.Printf("key: %v value: %v\n", key, value) wg.Done() }(i) } wg.Wait() } ``` 5 **sync.Waitgroup** | 等待组 用来等待groutine执行完再继续,是一个结构体,值类型,给函数传参的时候要传指针 ```go var wg sync.Waitgroup go function(){ wg.Add(1) // 开启一个goroutine,计数器加一 defer wg.Done() // 结束一个goroutine,计数器减一 } wg.Wait() // 阻塞 等待所有的goroutine结束 ``` ### 6)原子操作 通常直接使用原子操作比使用锁操作效率更高,借助内置 atomic 包 ```go var ( x int64 = 0 wg = sync.WaitGroup{} lock = sync.Mutex{} ) func Add() { // lock.Lock() // x++ // lock.Unlock() // defer wg.Done() // 不使用lock ,使用aotic atomic.AddInt64(&x, 1) defer wg.Done() } var m2 = sync.Map{} func main() { for i := 0; i < 1000; i++ { go Add() wg.Add(1) } wg.Wait() fmt.Printf("x: %v\n", x) // // x: 1000 var n1 int64 = 10 ok2 := atomic.CompareAndSwapInt64(&n1, 10, 1) // 第一个参数 == 第二个参数 ? 将第三个参数赋值给第一个参数 : return if !ok2 { fmt.Println("比较转换失败") } fmt.Printf("n1: %v\n", n1) // n1: 1 } ``` ## 四、GoWeb ### 1)网络基础 1. OSI七层网络模型(了解) ![osi网络模型](./public/osi.png) 越往上的层越靠近用户,越往下的层越靠近硬件 **物理层**:规定网络电气特性,作用时负责传送电信号(1/0) **数据链路层**:规定了电信号分组方式以及代表的意义(如以太网协议)以太网规定:一组电信号构成一个数据包(帧--Frame)。每一帧分成两个部分:标头(Head)和数据(Data)。其中”标头”包含数据包的一些说明项,如发送、接受者、数据类型等等,标头长度固定为18字节;”数据”则是数据包的具体内容,长度最短为46字节,最长为1500字节。因此,”帧”最短为64字节,最长为1518字节。如果数据很长,就必须分割成多个帧进行发送。 发送者和接受者是如何标识呢?以太网规定,连入网络的所有设备都必须具有”网卡”接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做MAC地址。每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,通常用12个十六进制数表示。前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号。有了MAC地址,就可以定位网卡和数据包的路径了。 我们会通过ARP协议来获取接受方的MAC地址,有了MAC地址之后,如何把数据准确的发送给接收方呢?其实这里以太网采用了一种很”原始”的方式,它不是把数据包准确送到接收方,而是向本网络内所有计算机都发送,让每台计算机读取这个包的”标头”,找到接收方的MAC地址,然后与自身的MAC地址相比较,如果两者相同,就接受这个包,做进一步处理,否则就丢弃这个包。这种发送方式就叫做”广播”(broadcasting)。 **网络层**:理论上依靠MAC地址,你电脑的网卡就可以找到身在世界另一个角落的某台电脑的网卡了,但是这种做法有一个重大缺陷就是以太网采用广播方式发送数据包,所有成员人手一”包”,不仅效率低,而且发送的数据只能局限在发送者所在的子网络。 网络层的作用是引进一套新的地址,使能够区分不同的计算机是否属于同一个子网络。这套地址就叫做”网络地址”,简称”网址”。 “网络层”的出现导致每台计算机有了两种地址,一是MAC地址,另一种是网络地址。两种地址之间没有任何联系,MAC地址是绑定在网卡上的,网络地址则是网络管理员分配的。网络地址帮助我们确定计算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理MAC地址。 规定网络地址的协议,叫做*IP协议*。它所定义的地址,就被称为IP地址。目前,广泛采用的是IP协议第四版,简称IPv4。IPv4这个版本规定,网络地址由32个二进制位组成,我们通常习惯用分成四段的十进制数表示IP地址,从0.0.0.0一直到255.255.255.255。 根据IP协议发送的数据,就叫做IP数据包。IP数据包也分为”标头”和”数据”两个部分:”标头”部分主要包括版本、长度、IP地址等信息,”数据”部分则是IP数据包的具体内容。IP数据包的”标头”部分的长度为20到60字节,整个数据包的总长度最大为65535字节。 **传输层**:有了MAC地址和IP地址,我们已经可以在互联网上任意两台主机上建立通信。但问题是同一台主机上会有许多程序都需要用网络收发数据,我们如何区分某个数据包到底是归哪个程序的呢?也就是说,我们还需要一个参数,表示这个数据包到底供哪个程序(进程)使用。这个参数就叫做”端口”(port),它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。 “端口”是0到65535之间的一个整数,正好16个二进制位。0到1023的端口被系统占用,用户只能选用大于1023的端口。有了IP和端口我们就能实现唯一确定互联网上一个程序,进而实现网络间的程序通信。 我们必须在数据包中加入端口信息,这就需要新的协议。最简单的实现叫做UDP协议,它的格式几乎就是在数据前面,加上端口号。UDP数据包,也是由”标头”和”数据”两部分组成:”标头”部分主要定义了发出端口和接收端口,”数据”部分就是具体的内容。UDP数据包非常简单,”标头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。 UDP协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一旦数据包发出,无法知道对方是否收到。为了解决这个问题,提高网络可靠性,TCP协议就诞生了。TCP协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。 **应用层**:应用程序收到”传输层”的数据,接下来就要对数据进行解包。由于互联网是开放架构,数据来源五花八门,必须事先规定好通信的数据格式,否则接收方根本无法获得真正发送的数据内容。”应用层”的作用就是规定应用程序使用的数据格式,例如我们TCP协议之上常见的Email、HTTP、FTP等协议,这些协议就组成了互联网协议的应用层。 数据传输过程: ![httptcpip](./public/httptcpip.png) 2. socket编程 Socket是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket后面,对用户来说只需要调用Socket规定的相关函数,让Socket去组织符合指定的协议数据然后进行通信。 ![socket](./public/socket%26osi.png) ![socket](./public/socket.png) ### 2) Go实现TCP网络通信 TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。 一个TCP服务端可以同时连接很多个客户端,因为Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。 TCP服务端程序的处理流程: -> 监听端口 listen,err := net.Listen("tcp","127.0.0.1:8001") -> 接收客户端请求建立链接 conn,err := listen.Accept() -> 创建goroutine处理链接。 go function(){//conn.Close()、conn.Read()} ```go // tcp server端 func processConn(conn net.Conn) { defer conn.Close() // 关闭连接 // 3. 通信 var temp [128]byte for { n, err := conn.Read(temp[:]) if err != nil { fmt.Println("read from conn failed, err:", err) return } fmt.Println(string(temp[:n])) } } func main() { // 1. 启动服务 listener, err := net.Listen("tcp", "127.0.0.1:8000") if err != nil { fmt.Println("err :", err) return } for { // 2. 等待连接 conn, err := listener.Accept() if err != nil { fmt.Println("accept failed, err:", err) return } go processConn(conn) } } ``` TCP客户端程序流程: -> 建立与服务端的链接 conn, err := net.Dial("tcp", "127.0.0.1:8001") -> 进行数据收发 conn.Write([]byte(msg)) -> 关闭链接 conn.Close() ```go // tcp client端 func main() { // 1. 与server建立连接 conn, err := net.Dial("tcp", "127.0.0.1:8000") if err != nil { fmt.Println("err :", err) return } // 2. 发送数据 render := bufio.NewReader(os.Stdin) for { fmt.Print("输入:") // fmt.Scanf(&msg) // fmt.Scanln(&msg) msg, _ := render.ReadString('\n') msg = strings.TrimSpace(msg) if msg == "exit" { break } conn.Write([]byte(msg)) } conn.Close() } ``` 2) TCP粘包 tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。 TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方: 原因1:由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。 原因2:接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。 解决粘包:(思路:在发送数据的时候可以指定数据包中的一个位置来存放当前数据包的实际大小,可以自定义一个小协议,在数据包的前4个字节存储数据的长度) ``` 引申知识点:(大端和小端) 对于 0x123456 的存储,高位写在右边,则在读取的时候应该从右往左读,反之亦然;高位写在内存低地址:大端;高位写在内存高地址:小端。 ``` ![大端和小端](./public/%E5%A4%A7%E5%B0%8F%E7%AB%AF.png) 编解码具体示例: ```go // socket_stick/proto/proto.go package proto import ( "bufio" "bytes" "encoding/binary" ) // Decode 解码消息 func Decode(reader *bufio.Reader) (string, error) { // 读取消息的长度 lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据 lengthBuff := bytes.NewBuffer(lengthByte) var length int32 err := binary.Read(lengthBuff, binary.LittleEndian, &length) if err != nil { return "", err } // Buffered返回缓冲中现有的可读取的字节数。 if int32(reader.Buffered()) < length+4 { return "", err } // 读取真正的消息数据 pack := make([]byte, int(4+length)) _, err = reader.Read(pack) if err != nil { return "", err } return string(pack[4:]), nil } ``` ### 3)UDP通信 UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。 ```go func main() { conn, err := net.ListenUDP("udp", &net.UDPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: 40000, }) if err != nil { fmt.Printf("listen UDP field err: %v\n", err) return } defer conn.Close() // 不需要建立连接,直接收发数据 var data [1024]byte for { n, adr, err := conn.ReadFromUDP(data[:]) if err != nil { fmt.Printf("read from UDP failed ,err: %v\n", err) } fmt.Printf("data: %v\n", data[:n]) reply := strings.ToUpper(string(data[:n])) // 发送数据 conn.WriteToUDP([]byte(reply), adr) } } ``` ```go // UDP 客户端 func main() { socket, err := net.DialUDP("udp", nil, &net.UDPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: 40000, }) if err != nil { fmt.Println("连接服务端失败,err:", err) return } defer socket.Close() sendData := []byte("Hello server") _, err = socket.Write(sendData) // 发送数据 if err != nil { fmt.Println("发送数据失败,err:", err) return } // data := make([]byte, 4096) // n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据 var data [1024]byte n, remoteAddr, err := socket.ReadFromUDP(data[:]) if err != nil { fmt.Println("接收数据失败,err:", err) return } fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n) } ``` ### 4)HTTP通信(go内置net/http包非常优秀) 1. 创建简单响应服务 ```go // 读取 index.html 静态文件,,返回静态文件字符串 func openFile(fileName string) string { var str [1024]byte var fileStr string fileObj, err := os.Open("../../public/index.html") if err != nil { fmt.Printf("open file, err: %v\n", err) } defer fileObj.Close() for { n, err := fileObj.Read(str[:]) if err == io.EOF { break } if err != nil { fmt.Printf("err: %v\n", err) } fileStr += string(str[:n]) } return fileStr } // 解析请求 无query参数,返回静态文件文本 func fun(w http.ResponseWriter, r *http.Request) { str := openFile("../public/index.html") w.Write([]byte(str)) } // 解析query参数请求,返回 ok func fun2(w http.ResponseWriter, r *http.Request) { fmt.Printf("URL: %v\n", r.URL) fmt.Printf("Method: %v\n", r.Method) fmt.Println(ioutil.ReadAll(r.Body)) // 带有query参数 arr := r.URL.Query() fmt.Println("name:", arr.Get("name"), " age:", arr.Get("age")) w.Write([]byte("ok")) } func main() { fmt.Println("127.0.0.1:8001 is running ...") // 注册路由管理 http.HandleFunc("/", fun) http.HandleFunc("/static", fun2) // 指定监听地址和处理器启动一个 http 服务端 http.ListenAndServe("127.0.0.1:8001", nil) } ``` 2. 发送请求 发送请求(不携带query参数) ```go // 发送http请求 不携带query参数 resp, err := http.Get("http://127.0.0.1:8001/static") if err != nil { fmt.Printf("err: %v\n", err) return } // 读取返回响应数据 b, err := ioutil.ReadAll() if err != nil { fmt.Println("read resp.Body failed.err", err) } defer resp.Body.Close() fmt.Printf("b: %v\n", string(b[:])) ``` 发送请求(携带query参数) ```go // 发送http请求 携带query参数 // 方式 1 // http.NewRequest("GET", "http://127.0.0.1:8001/static?name=tom&pwd=1234") // 方式 2 urlObj, _ := url.Parse("http://127.0.0.1:8001/static") urlData := url.Values{} // url encode urlData.Set("key", "value") urlData.Set("name", "张三") urlData.Set("pwd", "135fba") queryStr := urlData.Encode() //url encode之后的url urlObj.RawQuery = queryStr fmt.Printf("query编码之后的数据: %v\n", queryStr) req, err := http.NewRequest("GET", urlObj.String(), nil) resp, err := http.DefaultClient.Do(req) // 创建客户端 if err != nil { fmt.Printf("err: %v\n", err) return } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) fmt.Println(string(b)) ``` 3. 禁用KeepAlive的client, 关于http长连接/短连接请参考:[2021 HTTP长连接与短连接使用方法及测试详解](https://www.html.cn/softprog/other/1135101893025.html) ,[2021 http长连接(http长连接什么时候断开)](https://www.ltonus.com/web-basic/http-link.html) ```go urlObj, _ := url.Parse("http://127.0.0.1:8001/static") urlData := url.Values{} // url encode urlData.Set("key", "value") queryStr := urlData.Encode() //url encode之后的url urlObj.RawQuery = queryStr req, err := http.NewRequest("GET", urlObj.String(), nil) // 禁用KeepAlive的client tr := &http.Transport{ DisableKeepAlives: true, } client := http.Client{ Transport: tr, } resp, err := client.Do(req) if err != nil { fmt.Printf("err: %v\n", err) return } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) fmt.Println(string(b)) ``` ```go 共用一个client,适用于请求频繁的场景 var ( client := http.Client{ Transport: &http.Transport{ DisableKeepAlives: true, }, } ) ``` ## 五、单元测试 ### 1)context(面试点之一) 参考文章: [GO 上下文Context-舍是境界 | 简书](https://www.jianshu.com/p/4cad0c0ba321)、 [深度揭秘Go语言之context-Stefno | 博客园](https://www.cnblogs.com/qcrao-2018/p/11007503.html) ### 2)单元测试 Go语言中的测试依赖`go test`命令。编写测试代码和编写普通的Go代码过程是类似的,不需要学习新的语法、规则或工具。 `go test`命令是一个按照一定约定和组织的测试代码的驱动程序。 在包目录内,所有以**_test.go**为后缀名的源代码文件都是go test测试的一部分,不会被`go build`编译到最终的可执行文件中。 1. *_test.go 在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。 | 类型 | 格式 | 作用 | | -------- | -------------- | ----- | | 测试函数 | 函数名前缀为Test | 测试程序的一些逻辑行为是否正确 | | 基准函数 | 函数名前缀为Benchmark| 测试函数的性能 | | 示例函数 | 函数名前缀为Example | 为文档提供示例文档 | go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。 测试函数格式: 每个测试函数都要导入`testing`包,测试函数的基本格式(签名)如:`func TestName(t *testing.T){ ... }` 测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,如:**TestAdd**、**TestLog**等 测试函数的参数t用于报告测试失败和附加的日志信息,testing.T的拥有的方法如下: ```go func (c *T) Cleanup(func()) func (c *T) Error(args ...interface{}) func (c *T) Errorf(format string, args ...interface{}) func (c *T) Fail() func (c *T) FailNow() func (c *T) Failed() bool func (c *T) Fatal(args ...interface{}) func (c *T) Fatalf(format string, args ...interface{}) func (c *T) Helper() func (c *T) Log(args ...interface{}) func (c *T) Logf(format string, args ...interface{}) func (c *T) Name() string func (c *T) Skip(args ...interface{}) func (c *T) SkipNow() func (c *T) Skipf(format string, args ...interface{}) func (c *T) Skipped() bool func (c *T) TempDir() string ``` 2. 测试用例(了解) ```go package main import ( "reflect" "testing" ) // 测试函数 func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数 got := Split("a:b:c", ":") // 程序输出的结果 want := []string{"a", "b", "c"} // 期望的结果 if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较 // 测试用例失败,输出错误提示 t.Errorf("want:%v, but got:%v", want, got) } } func TestSplit2(t *testing.T) { got := Split("103ab5", "ab") want := []string{"103", "5"} if !reflect.DeepEqual(want, got) { t.Errorf("want:%v, but got:%v", want, got) } } ``` ```go package main import ( "fmt" "strings" ) // 单元测试-切割字符串 func Split(str string, sep string) []string { var ret []string index := strings.Index(str, sep) for index >= 0 { ret = append(ret, str[:index]) str = str[index+1:] // str = str[index+len(sep):] index = strings.Index(str, sep) } ret = append(ret, str) return ret } func main() { str := "2012-03-12-22-241-404" temp := Split(str, "-") fmt.Printf("temp: %v\n", temp) str2 := "name:tom jerry:ass" temp2 := Split(str2, " ") fmt.Printf("temp2: %v\n", temp2) str3 := "/wd=key&user=jack&time=2022/06/21" temp3 := Split(str3, "&") fmt.Printf("temp2: %v\n", temp3) } ``` 测试命令 `go test`或者`go test -v`,运行结果: ``` === RUN TestSplit --- PASS: TestSplit (0.00s) === RUN TestSplit2 --- PASS: TestSplit2 (0.00s) === RUN TestSplit3 split_test.go:30: want:[103 5], but got:[103 b5] --- FAIL: TestSplit3 (0.00s) FAIL exit status 1 FAIL split/demo 0.056s ``` 3. 测试组(熟悉) ```go //利用切片实现测试组 func TestSplit(t *testing.T) { type TestCase struct { str string sep string want []string } // 测试用例切片 testGroup := []TestCase{ {"abacdeaf", "a", []string{"", "b", "cde", "f"}}, {"2012/06/12", "/", []string{"2012", "06", "12"}}, {"sex:男", ":", []string{"sex", "男"}}, } for _, tc := range testGroup { got := Split(tc.str, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Fatalf("want:%#v got:%#v\n", tc.want, got) } } } ``` 在Go1.7+中新增了子测试 优势:可以单独跑某个测试用例 ,如:`go test -run TestSplit/testCase2` ```go func TestSplit(t *testing.T) { type TestCase struct { str string sep string want []string } testGroup := map[string]TestCase{ "testCase1": {"abacdeaf", "a", []string{"", "b", "cde", "f"}}, "testCase2": {"2012/06/12", "/", []string{"2012", "06", "12"}}, "testCase3": {"sex:男", ":", []string{"sex", "男", "多余"}}, // 错误测试 } for name, tc := range testGroup { t.Run(name, func(t *testing.T) { got := Split(tc.str, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Fatalf("want:%#v got:%#v\n", tc.want, got) } }) } } ``` ### 3) 性能基准测试 基准测试用例的定义如下:`func BenchmarkName(b *testing.B){...}` 函数名必须以 Benchmark 开头,后面一般跟待测试的函数名 参数为 b *testing.B。 执行基准测试时,需要添加 -bench 参数。 ```go package main import ( "testing" ) // 基准测试 func BenchmarkSplit(b *testing.B) { for i := 0; i < b.N; i++ { Split("112:f4:4c:22:1:10", ":") } } ``` 运行结果:(BenchmarkSplit-8表示`GOMAXPROCS`值,2718679和425.7 ns/op表示每次调用Split函数耗时425.7ns,这个结果是2718679次调用的平均值) ``` C:\Users\Mrnianj\Desktop\goDemo\nianj\modules_demo>go test -bench=BenchmarkSplit goos: windows goarch: amd64 pkg: split/demo cpu: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz BenchmarkSplit-8 2718679 425.7 ns/op PASS ok split/demo 1.933s ``` 还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据,如`go test -bench=BenchmarkSplit -benchmem` 其它还有**并行测试**、**SetUp和Teardown**等 ### 4)pprof调试工具 profiling -- 指的是对应用程序的画像,go内置了 profiling 库(包括了runtime/pprof和net/http/pprof) runtime/pprof --- 采集工具型应用运行数据分析 net/http/pprof --- 采集服务型应用运行数据分析 1. 扩展点 flag包 使用os.Args 可以获取终端命令行参数 ```go package main // os.Args import ( "fmt" "os" ) func main() { fmt.Printf("value: %v\n", os.Args) fmt.Printf("type: %T\n", os.Args) /* 命令:main.exe a b 111 运行结果: value: [main.exe a b 111] type: []string */ } ``` flag包的具体使用 ```go package main import ( "flag" "fmt" ) // os.Args // 命令行携带参数 mian.exe -name "tom"或者mian.exe -name="tom" func main() { // 创建标志位参数方式1 ,取值使用指针 name := flag.String("name", "游客", "请输入用户名") age := flag.Int("age", 999, "请输入年龄") // 创建标志位参数方式1 ,直接使用 // var name string // flag.StringVar(&name, "name", "游客", "请输入用户名") // 使用标志位解析命令行参数 flag.Parse() // 获取输入参数 fmt.Printf("name: %s\n", *name) fmt.Printf("age: %d\n", *age) // 其它方法 fmt.Printf("flag.Args(): %v\n", flag.Args()) // 返回命令行参数后的其它参数 fmt.Printf("flag.NArg(): %v\n", flag.NArg()) // 返回命令行参数后的其它参数个数 fmt.Printf("flag.NFlag(): %v\n", flag.NFlag()) // 返回使用的命令行参数个数 } /* 执行命令: go build main.go main.exe -name "tom" -age 19 other a b 执行结果: name: tom age: 19 flag.Args(): [other a b] flag.NArg(): 3 flag.NFlag(): 2 */ ``` 2. pprof ```go // 这是一段有问题的函数 func logisCode() { var c chan int // nil for { select { case v := <-c: // 阻塞 fmt.Println("value:", v) default: } } } func main() { var isCPUPprof bool var isMemPprof bool flag.BoolVar(&isCPUPprof, "cpu", false, "turn cpu pprof on") flag.BoolVar(&isMemPprof, "mem", false, "turn mem pprof on") flag.Parse() if isCPUPprof { fileObj, err := os.Create("./cpu.pprof") if err != nil { fmt.Printf("err: %v\n", err) return } pprof.StartCPUProfile(fileObj) defer func() { pprof.StopCPUProfile() fileObj.Close() }() } for i := 0; i < 6; i++ { go logisCode() } time.Sleep(10 * time.Second) // 以上代码没有内存开销,仅为演示使用 // if isMemPprof { // fileObj, err := os.Create("./mem.pprof") // if err != nil { // fmt.Printf("err: %v\n", err) // return // } // pprof.WriteHeapProfile(fileObj) // fileObj.Close() // } } ``` 以上代码编译执行生成cpu.pprof文件,使用`go tool pprof cpu.pprof`解析文件 `top 3`查看文件占用率前三的函数 `list logisCode`查看具体函数的占用(top、list是内置的命令) 图形化:请参考[graphviz 工具](http://graphviz.org/)的使用 ### 5)两道面试题 链表闭环问题:如何判断一个链表有没有闭环 思路:第一个数据走一步x,第一个数据走两步y,如果x与y在某一个节点相遇,则此链表是闭环 ```go``` 排序问题:n个台阶,一次迈两步/一步,一共有多少种走法 思路:走到最后,会剩下一步台阶和两步台阶 ```go func f(n int){ if(n == 1) { return 1 } if(n == 2) { return 2 } return f(n-1) + f(n-2) } ``` ## 六、Go数据库管理 ### 1)MySql 1. 了解Mysql 主流的关系型数据库管理,常见的数据库SQLlite、MySQL、postareSQL、Oracle SQL语句: DDL:操作数据库 DML:操作表 DCL:操作用户以及权限 存储引擎: 1. Mysql支持插件式的存储引擎 2. 常见的存储引擎:MylSAM(查询速度快、只支持表锁、不支持事务)和innoDB(整体速度均衡、支持表锁、行锁和事务) 3. 补充:事务的特点(ACID)--- 原子性(事务要么成功/失败,没有中间状态)、一致性(事务开始-结束,数据库完整性没有被破坏)、隔离性(不同事务之间独立,隔离的四个级别...)、持久性(事务操作结果不会丢失) 关系型数据库:用表来存储数据,表结构设计的三大范式。 索引:索引的原理(B树和B+树)、索引类型(唯一索引、联合索引...)、索引的命中 分库分表 SQL注入 SQL慢查询优化 MySql主从(binlog) MySql读写分离 2. Go操作mysql Go语言中**database/sql**包提供了保证SQL或类SQL数据库的泛用接口,并不提供具体的数据库驱动。使用**database/sql**包时必须注入(至少)一个数据库驱动。 **database/sql**包原生支持连接池,是线程安全的。但没有具体实现,只是列出了一些需要第三方实现的具体内容。 我们常用的数据库基本上都有完整的第三方实现。例如:[mysql驱动](https://github.com/go-sql-driver/mysql) 下载驱动:`go get -u github.com/go-sql-driver/mysql`,`go get 包的路径`就是下载第三方依赖,下载完成后默认保存在$GOPATH/src/路径下 **补充数据库建库建表sql:** ```sql mysql> show databases -> ; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | sys | | test | +--------------------+ 5 rows in set (0.02 sec) mysql> create database go_sql_test; Query OK, 1 row affected (0.01 sec) mysql> use go_sql_test; Database changed mysql> CREATE TABLE `user` ( -> `id` BIGINT(20) NOT NULL AUTO_INCREMENT, -> `name` VARCHAR(20) DEFAULT '', -> `age` INT(11) DEFAULT '0', -> PRIMARY KEY(`id`) -> )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; Query OK, 0 rows affected, 2 warnings (0.02 sec) mysql> insert into user(name,age) values("tom",500); Query OK, 1 row affected (0.02 sec) mysql> select * from user; +----+------+------+ | id | name | age | +----+------+------+ | 1 | tom | 500 | +----+------+------+ 1 row in set (0.00 sec) mysql> insert into user(name,age) values("张三三",500); Query OK, 1 row affected (0.01 sec) mysql> select * from user; +----+-----------+------+ | id | name | age | +----+-----------+------+ | 1 | tom | 500 | | 2 | 张三三 | 500 | +----+-----------+------+ ``` 3. 查询单条记录 ```go package main // go连接mysql import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" // init() ) var db *sql.DB // 是一个连接池 var dsn string = "root:root333@tcp(127.0.0.1:3306)/go_sql_test" // 数据库信息 type user struct { id int name string age uint } // 初始化数据库 func initDb() (err error) { // 连接数据库 db, err = sql.Open("mysql", dsn) if err != nil { return } err = db.Ping() if err != nil { return } // 设置数据库连接池最大数目,默认为0,即不限制,超过连接数目,会产生阻塞 db.SetMaxOpenConns(10) // 设置连接池中最大闲置连接数目(请求数据量较小时,会关闭部分连接池) db.SetMaxIdleConns(5) return nil } // 查询 func queryOne(id int) { var u user sqlStr := "select id,name,age from user where id=?;" db.QueryRow(sqlStr, id).Scan(&u.id, &u.name, &u.age) fmt.Printf("u: %v\n", u) } func main() { err := initDb() if err != nil { fmt.Printf("init DB failed, err: %v\n", err) return } queryOne(2) } ``` 4. 查询多条记录 ```go package main // go连接mysql import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" // init() ) var db *sql.DB // 是一个连接池 var dsn string = "root:root333@tcp(127.0.0.1:3306)/go_sql_test" // 数据库信息 type user struct { id int name string age uint } // 初始化数据库 func initDb() (err error) { // 连接数据库 db, err = sql.Open("mysql", dsn) if err != nil { return } err = db.Ping() if err != nil { return } // 设置数据库连接池最大数目,默认为0,即不限制,超过连接数目,会产生阻塞 db.SetMaxOpenConns(10) // 设置连接池中最大闲置连接数目(请求数据量较小时,会关闭部分连接池) db.SetMaxIdleConns(5) return nil } // 查询 func queryMore(id int) { // 1.sql sqlStr := "select id,name,age from user where id= ?" var userList []userInfo err := db.Select(&userList, sqlStr2, 1) if err != nil { fmt.Println("查询失败:", err) return } fmt.Println("userList:", userList) } ``` ### 2)Redis 开源的内存数据库,提供了多种不同类型的数据结构映射,支持诸如字符串(string)、哈希(hashe)、列表(list)、集合(set)、带范围查询的排序集合(sorted set)、bitmap、hyperloglog、带半径查询的地理空间索引(geospatial index)和流(stream)等数据结构。 用处:cache缓存(降低数据库压力)、简单的队列(LIST )、排行榜(ZSET)、带半径查询和地理位置(geospatial index)等... go中连接redis,安装 go-redis 库`go get github.com/go-redis/redis/v8` 连接Redis示例: ```go package main import ( "fmt" "github.com/go-redis/redis" ) var redisdb *redis.Client func initRedis() (err error) { // go-redis 库中使用 redis.NewClient 函数连接 Redis 服务器。 redisdb = redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", Password: "", DB: 0, }) _, err = redisdb.Ping().Result() return } func main() { err := initRedis() if err != nil { fmt.Printf("err: %v\n", err) return } fmt.Println("连接redis成功") } ``` 更多用法参考:[Go语言操作Redis | 简书](https://www.jianshu.com/p/77bc3013ed4d) ### 3)NSQ | 消息队列 [NSQ 中文文档](http://nsqio.cn/index.html) nsq工作模式: ![nsq 工作模式](./public/nsq%20%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%BC%8F.png) 在一个 shell 中,开始 nsqlookupd: ```nsqlookupd``` 开启一个 shell ,运行 nsqd: ``` nsqd -broadcast-address=127.0.0.1 -lookupd-tcp-address=127.0.0.1:4160 ``` 开启一个 shell ,运行 nsqadmin: ``` nsqadmin --lookupd-http-address=127.0.0.1:4161 ``` 本地浏览器此时访问:127.0.0.1:4171 ![127.0.0.1:4171](./public/nsqadmin%204171.png) ## 七、依赖管理和架构设计 ### 1)go module go 1.11版本之后引入的版本管理工具,[GO modules详解 | 简书](https://www.jianshu.com/p/2d4d0bd7d2e4) goproxy 设置代理,下载代理(国内被墙,可以设置国内镜像站) go.sum 详细的包名和版本信息 go.mod 当前项目依赖的第三方包信息和版本信息(第三方包都下载到了`GOPATH/pkg/mod`目录下) 常用命令: ``` go mod init [包名] //初始化项目 go mod tidy //检查代码中的依赖更新go.mod go get go mod download ``` ### 2)context 应用场景:在web请求中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。 退出一个 goroutine 退出: 方式1: ```go func f() { defer wg.Done() var n int = 0 FORLOOKUP: for { n++ fmt.Println("## ", n) select { case <-exitChan: break FORLOOKUP default: } time.Sleep(1 * time.Second) } } var wg sync.WaitGroup // var exitChan struct{} var exitChan = make(chan bool, 1) func main() { wg.Add(1) go f() time.Sleep(3 * time.Second) exitChan <- true wg.Wait() } ``` 方式2: ```go func f() { defer wg.Done() var n int = 0 for { n++ fmt.Println("## ", n) if notify { break } time.Sleep(1 * time.Second) } } var wg sync.WaitGroup var notify bool func main() { wg.Add(1) go f() time.Sleep(5 * time.Second) notify = true wg.Wait() //阻塞 } ``` 方式3: ```go func f(ctx context.Context) { defer wg.Done() var n int = 0 go f2(ctx) LOOP: for { n++ fmt.Println("## ", n) select { case <-ctx.Done(): break LOOP default: } time.Sleep(1 * time.Second) } } func f2(ctx context.Context) { defer wg.Done() var n int = 0 LOOP: for { n++ fmt.Println("@@ ", n) select { case <-ctx.Done(): break LOOP default: } time.Sleep(1 * time.Second) } } var wg sync.WaitGroup func main() { ctx, canael := context.WithCancel(context.Background()) wg.Add(1) go f(ctx) time.Sleep(5 * time.Second) canael() wg.Wait() //阻塞 } ``` 标准库context,定义了Context类型,专门用来简化对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。 对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用WithCancel、WithDeadline、WithTimeout或WithValue创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。 **两个默认值:** context.Background() context.TODO() **四个方法:** context.WithCancel(context.Background()) 返回取消函数 context.WithDeadline(context.Background(),time.Time) 返回绝对终止时间 context.WithTimeout(context.Background(),time.Duration) 返回相对终止时间 context.WithValue(context.Background(),key,value) 创建上下文 ### 3) kafka | 分布式发布订阅消息系统 [kafka架构](./public/kafka%E9%9B%86%E7%BE%A4%E6%9E%B6%E6%9E%84.png) 1. kafka集群架构 broker topic partition(分区) leader:主节点 follower:从节点 consumer Group 2. 生产者向kafka发送数据的步骤(6步) ![producer发送数据](./public/kafka%E7%94%9F%E4%BA%A7%E8%80%85%E5%8F%91%E9%80%81%E6%95%B0%E6%8D%AE.png) 3. kafka选择分区的模式(3种) 指定分区 指定key,根据key做hash 轮询方式 4. 生产者向kafka发送数据的模式(3种) 0 将数据发送给leader就视为成功 1 将数据发送给leader,等待leader落盘,返回ack all 将数据发送给leader,等待leader落盘,follower主动从leader拉取数据并落盘,返回ack得到leader,leader收到全部返回ack,将ack返回给producer 5. 分区存储文件的原理 6. 消费者消费数据的原理 7. 为什么kafka快? ### 4) 补充:消息队列的通信模式 1. 点对点模式(queqe) 消息生产者生产消息发送到queue中,消息消费者从queue中消费数据(一条消息被消费以后,queue中就没有了,不存在重复消费) 2. 发布/订阅模式(topic) 消息生产者(发布) 将消息发布到topic中,同时有多个消息消费者(订阅) 消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费(类似于关注了微信公众号的人都能收到推送的文章)。 补充:发布订阅模式下,当发布者消息量很大时,显然单个订阅者的处理能力是不足的。实际上现实场景中是多个订阅者节点组成一个订阅组负载均衡消费topic消息即分组订阅,这样订阅者很容易实现消费能力线性扩展。可以看成是一个topic 下有多个Queue,每个Queue是点对点的方式,Queue之间是发布订阅方式。 3. kafka 是一款开源的基于发布订阅模式的消息引擎系统。本质是一个分布式数据流平台,可以运行在单台服务区上,也可以多态服务器部署形成集群。[腾讯云-学习kafka入门知识看这一篇就够了!(万字长文)](https://cloud.tencent.com/developer/article/1547380) topic 同一类消息记录(record)的集合,Kafka中一个主题通常有哥订阅者,对于每个主题,kafuka集群维护了一个分区数据日志文件结构如下: [topic](./public/kafka%20topic.png) 每个partition都是一个有序并且不可变的消息记录集合。当新的数据写入时,就被追加到partition的末尾。 在每个partition中,每条消息都会被分配一个顺序的唯一标识, 这个标识被称为**offset**, 即**偏移量**。 注意,Kafka只保证在同一个partition内部消息是有序的,在不同partition之间,并不能保证消息有序。 Kafka可以配置一个保留期限,用来标识日志会在Kafka集群内保留多长时间。Kafka集群会保留在保留期限内所有被发布的消息,不管这些消息是否被消费过。比如保留期限设置为两天,那么数据被发布到Kafka集群的两天以内,所有的这些数据都可以被消费。当超过两天,这些数据将会被清空,以便为后