# go_keep_learning **Repository Path**: dciwang/go_keep_learning ## Basic Information - **Project Name**: go_keep_learning - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-10-19 - **Last Updated**: 2022-07-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # go 学习巩固 ## 指针 Go 语言保留了指针,但与 C 语言指针有所不同,主要体现在 - 默认值为 nil - 操作符 "&" 取变量地址,"\*" 通过指针访问目标对象 - 不支持指针运算,不支持 "\_>" 运算符,直接用 "." 访问目标成员 指针就是地址,。指针变量就是存储地址的变量。 *p : 解引用,间接引用 栈帧存储:1.局部变量 2.形参 (形参与局部变量存储的地位等同) 3. 内存字段描述值 ```go func main() { var a int = 100 fmt.Println("a = ", a) var p *int = &a //借助 a 变量的地址,操作 a 对应的空间 *p = 1000 //*p 取值运算(解引用) fmt.Println("a = ", a) fmt.Println("*p = ", *p) a = 250 fmt.Println("*p = ", *p) test(a) } func test(m int) { b := 100 m += b } ``` 一个函数就是一个栈帧,一个栈帧由栈基指针,栈顶指针分配空间,当main()函数调用另test()函数的时候,原本分配main()函数空间的指针去给test()函数分配空间,同时main()函数的栈帧存储着这两个指针的值,当test()函数执行结束之后,test()栈帧释放,指针回到main()函数栈帧记录的位置。 --- ### new() ![内存存储图](./png/memer.png) ```go func main() { var p *string //在heap 上申请一片内存地址空间 p = new(string) *p = "100" fmt.Printf("%q\n", *p) //默认类型的 默认值 } ``` var p *string 在stack 上开辟一个空指针,p =new(string) 在heap 上开篇一个对象空间,并且将这个对象的地址值赋给 p ,此时*p 的默认值为默认类型的默认值(此处为""),*p = "100" 将 "100" 这个值写入到heap中开辟的对象里。 **指针使用注意:** > - 空指针:未被初始化的指针。 var p \*int > - 野指针:被一片无效的地址空间初始化 **变量存储:** > - 左值:等号左边的变量,代表变量所指向的内存空间。 此时是写操作 > - 右值:等号右边的变量,代表变量内存空间存储的数据值。 此时是读操作 **注意:** > 当方法结束后,栈帧被释放,但是在方法里创建的对象并没有被释放,因为他是存在于 heap 上的。(栈内存默认为 1M 大小(可以手动分配大小,linux 环境下最大 16M),但是堆内存默认是 1G 以上的) ### 指针的函数传参 - 传地址(引用): 将地址值作为函数参数/返回值后传递 - 传值: 将实参的值拷贝一份给行参。 > > > 所有函数传参都是值传递 ```go func main() { a, b := 10, 20 swap1(a, b) //值传递,实参将自己的值拷贝一份,给形参 fmt.Printf("main :a=%d,b=%d\n", a, b) //main :a=10,b=20 c, d := 10, 20 swap2(&c, &d) //值传递,实参将自己的值(地址值)拷贝一份,给形参 fmt.Printf("main :c=%d,d=%d\n", c, d) //main :c=20,d=10 } func swap1(a, b int) { a, b = b, a fmt.Printf("swap1:a=%d,b=%d\n", a, b) //swap1:a=20,b=10 } func swap2(c, d *int) { *c, *d = *d, *c //等号左边的*c,*d 代表空间内存,等号右边的 *c,*d代表内容(值) fmt.Printf("swap2:*c=%d,*d=%d\n", *c, *d) //swap2:*c=20,*d=10 } ``` **传引用: 在 A 栈帧内部,修改 B 栈帧中的变量值** ## 切片(slice) ### 为什么用切片 1. 数组的内容是固定的,不能自动扩容 2. 值传递,数组作为函数参数时,将整个数组拷贝一份给行参 **在 Go 语言中,我们几乎可以在所有场景中,使用切片替换数组使用** ### 切片的本质 **不是一个数组的指针,是一种数据结构,用来操作数组内部元素** ```go type notInHeapSlice struct { array *notInHeap //底层数组的指针 len int //切片的长度 cap int //切片的容量 } ``` ### 切片与数组的区别 创建数组时 [] 内指定数组长度 创建切片时 [] 内为空,或者为 ... **截取数组建切片:** 切片名称[low:high:max] low: 起始下标位置 high: 结束下标位置; len = high - low max: 容量结束下标位置; cap = max - low **截取数组,初始化切片时,没有指定切片容量,切片容量跟随原数组(切片)** s[:high:max] :从0开始,到high结束; 「左闭右开」 s[low:] :从low开始,到末尾 s[:high] :从0开始,到high结束,容量跟随原先的容量。「常用」 ```go func main() { arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} s := arr[2:5] fmt.Println("s=", s) //s= [3 4 5] fmt.Println("s.len()=", len(s)) //s.len()= 3 fmt.Println("s.cap()=", cap(s)) //s.cap()= 8 s2 := s[2:7] fmt.Println("s2=", s2) //s2= [5 6 7 8 9] fmt.Println("s2.len()=", len(s2)) //s2.len()= 5 fmt.Println("s2.cap()=", cap(s2)) //s2.cap()= 6 } ``` ### 切片的创建 1. 自动推导类型创建切片。slice :=[]int{1,2,3,4} 2. slice := make([]int,len(),cap()) 3. slice := make([]int,len()) 创建切片时,没有指定容量,容量= 长度。 「常用」 ```go func main() { slice1 := []int{1, 2, 3, 4} //slice1=[1 2 3 4],len()=4,cap()=4 fmt.Printf("slice1=%v,len()=%d,cap()=%d\n", slice1, len(slice1), cap(slice1)) slice2 := make([]int, 2, 5) //slice2=[0 0],len()=2,cap()=5 fmt.Printf("slice2=%v,len()=%d,cap()=%d\n", slice2, len(slice2), cap(slice2)) slice3 := make([]int, 2) //slice3=[0 0],len()=2,cap()=2 fmt.Printf("slice3=%v,len()=%d,cap()=%d\n", slice3, len(slice3), cap(slice3)) } ``` > **注:切片作为参数---传引用(传地址)** ### append 基本使用 ```go append(切片对象,追加的元素) ``` ```go func main() { slice := []int{1, 2, 3, 4} slice = append(slice, 888) slice = append(slice, 888) slice = append(slice, 888) slice = append(slice, 888) slice = append(slice, 888) fmt.Println("slice=", slice) //slice= [1 2 3 4 888 888 888 888 888] } ``` > **append 函数会智能将底层的容量自动扩容,一旦超过底层数组容量,通常以 2 倍(1024 以下)容量重新分配底层数组。因此,使用 append 给切片扩容时,切片的地址可能发生变化,但,数据重新保存了,不影响使用** ### copy 的基本使用 > copy(目标位置切片,源切片) ```go func main() { data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := data[8:] s2 := data[:5] copy(s2, s1) fmt.Printf("s2=%v\n", s2) //s2=[9 2 3 4 5] } ``` 练习: 删除 slice 中间的某个元素并保存原有的元素顺序 ```go func deleteNode() { data := []int{1, 2, 3, 4, 5} //删除元素 3 copy(data[2:], data[2+1:]) data = data[:len(data)-1] fmt.Printf("data=%v\n", data) } ``` ## map(字典,映射) > **key:无序、唯一。不能是引用类型数据\*** ### map 声明、初始化、赋值 ```go func main() { //申明但是没有初始化,不能赋值,= nil var map1 map[int]string fmt.Printf("map1=%v\n", map1) //声明并且初始化且赋值, key 不能重复 var map2 = map[int]string{1: "wnag", 2: "duo", 3: "cong"} fmt.Printf("map2=%v\n", map2) //申明且初始化,长度为0, key 重复会覆盖 map3 := map[int]string{} map3[1] = "wind" map3[4] = "chime" fmt.Printf("map3=%v\n", map3) //make 初始化 不指定长度, key 重复会覆盖 map4 := make(map[int]string) map4[1] = "li" map4[4] = "qiao" fmt.Printf("map4=%v\n", map4) //make 初始化并指定长度, key 重复会覆盖 map5 := make(map[int]string, 5) map5[4] = "li" map5[6] = "yu" map5[1] = "mei" fmt.Printf("map5=%v\n", map5) } ``` > **map 没有容量的概念,只有长度。当到 map 中添加数据时,超出 map 长度,map 会自动扩容** ### map 的使用 ```go func main() { var m = map[int]string{1: "wnag", 2: "duo", 3: "cong"} // 遍历 for k, v := range m { fmt.Printf("k=%d,v=%s\n", k, v) } //查看是否存在 if _, ok := m[4]; !ok { fmt.Printf("%s 不存在\n", "m[4]") } else { fmt.Println("m[1]=", m[4]) } //删除 delete(m, 1) } ``` ### map 遍历,查看是否存在,删除元素 ```go func main() { var m = map[int]string{1: "wnag", 2: "duo", 3: "cong"} // 遍历 for k, v := range m { fmt.Printf("k=%d,v=%s\n", k, v) } if _, ok := m[4]; !ok { //查看是否存在 fmt.Printf("%s 不存在\n", "m[4]") } else { fmt.Println("m[1]=", m[4]) } //删除 delete(m, 1) fmt.Printf("m=%v\n", m) } ``` ```go //练习:归类 字符串中单词的个数。 func main() { str := "i love my country, family, i love Q" slice := strings.Fields(str) //将字符串分割成切片 m := make(map[string]int, 10) //遍历切片 for _, v := range slice { if _, have := m[v]; have { m[v]++ } else { m[v] = 1 } } for k, v := range m { fmt.Printf("%s 出现了 %d 次\n", k, v) } } ``` ## struct > **是一种数据类型** > **未初始化的成员变量,是该数据类型的默认值** > > 是一种类型定义(地位等价于 int byte bool string) ### 声明、初始化、赋值 ```go type stu struct { name string age int } ``` **声明** ```go var s stu fmt.Printf("s=%v\n", s) //s={ 0} ``` **初始化** 1. 顺序初始化,依次将结构体 nebula 所有成员初始化 ```go var s1 = stu{ "wind chime", 26, } fmt.Printf("s1=%v\n", s1) //s1={wind chime 26} ``` 2. 指定成员初始化: ```go s2 := stu{ name: "q", age: 25, } fmt.Printf("s2=%v\n", s2) //s2={q 25} ``` **结构体变量的比较和赋值** 1. 比较:只能使用 == 和 != 不能使用 > < >= <= 等 2. 赋值:`.`来进行赋值,例:`s.name="wdc"` 3. **相同结构体类型( 成员变量的类型、个数、顺序都一致 )变量可以直接赋值** **结构体传参:** > funSafe.Size(变量名) --> 此种类型的变量所占用的内存空间大小 > 结构体是值类型,所以传参是传值 ---几乎不用,内存消耗大,效率低 **结构体指针变量定义、初始化、赋值** ```go s1 := &stu{"wdc", 23} s2 := &stu{ name: "q", age: 22, } s3 := new(stu) s3.name ="wind chime" //跟普通结构体赋值操作一样 ``` **结构体地址:** > 结构体变量的地址 == 结构体首个元素的地址 ```go func main() { type stu struct { name string age int } s := stu{"wdc", 26} fmt.Printf("&s=%p\n", &s) //&s=0xc00000c030 fmt.Printf("&s.name=%p\n", &s.name) //&s=0xc00000c030 } ``` **结构体指针传参** > unSafe.size(指针):不管任何类型的指针,在 64 位操作系统下,大小一致,均为 8 字节 !!! > 传递结构体变量地址值(传引用) ---- 基本这样使用 **结构体指针作为返回值** > > **动态 new 出来的局部变量,go 语言编译器也会根据是否有逃逸行为来决定是分配在堆还是栈,而不是直接分配在堆中** ## strings ```go s := "i love work and i love my family too" ``` **按 指定字符截取,返回 slice** ```go s1 := strings.Split(s, " ") fmt.Println("s1=", s1) // s1= [i love work and i love my family too] ``` **按 空格截取,返回 slice** ```go s2 := strings.Fields(s) fmt.Println("s2=", s2) // s1= [i love work and i love my family too] ``` **判断是否存在某个字符或子串 返回 bool** ```go flag := strings.Contains(s, "love") fmt.Println("flag=", flag) // flag = true ``` **字符或者字符串在字符中第一次出现的位置,返回 下标,不存在返回 -1** ```go index := strings.Index(s, "love") fmt.Println("index =", index) //index = 2 ``` **字符或者字符串在字符中第一次出现的位置,返回 下标** ```go lastIndex := strings.LastIndex(s, "love") fmt.Println("lastIndex =", lastIndex) //lastIndex = 19 ``` **子串出现的次数** ```go count := strings.Count(s, "love") fmt.Println("count=", count) //count= 2 ``` **比较** ```go bs := strings.Compare("love", "Love") fmt.Println("bs=", bs) //bs= 1 bs1 := strings.Compare("love", "love") fmt.Println("bs1=", bs1) //bs1= 0 bs2 := strings.Compare("Love", "love") fmt.Println("bs2=", bs2) //bs2= -1 ``` **连接字符串** ```go join := strings.Join(strings.Fields(s), ",") fmt.Println("join=", join) //join= i,love,work,and,i,love,my,family,too ``` **替换** ```go // 用 new 替换 s 中的 old,一共替换 n 个。 // 如果 n < 0,则不限制替换次数,即全部替换 func Replace(s, old, new string, n int) string ``` ## os,io ### 创建,打开文件 1. 创建文件 Create:文件不存在则创建,文件存在则覆盖。 ```go //返回文件指针,参数为地址(绝对目录/相对目录) file, err := os.Create("../file/20211028.vim") if err != nil { fmt.Println("create file faild,err:", err) return } ``` 2. 打开文件 Open:以只读方式打开 ```go f, err := os.Open("../file/20211028.vim") if err != nil { fmt.Println("open file faild,err:", err) return } //往文件写数据 _, err = f.WriteString("tody tired") //因为文件只读,所以报错 if err != nil { fmt.Println("writeSting file faild,err:", err) return } defer f.Close() ``` 1. 打开文件 OpenFile: 只读,只写,读写 - 参数 1:打开文件的路径:相对/绝对 - 参数 2:打开文件的权限:1:O_RDONLY、2:O_WRONLY、3:ORDWR - 参数 3:表示权限范围,一般传 6 ```go ff, err := os.OpenFile("../file/20211028.vim", os.O_RDWR, 6) defer ff.Close() if err != nil { fmt.Println("openFile faild,err:", err) return } if err != nil { fmt.Println("writeSting file faild,err:", err) return } fmt.Println("writeSting file success") ``` ### 写文件 - 按字符串写 ```go n, err := file.WriteString("hello world \n") //n 写入的字符数 ``` - 按位置写 file.Seek(参 1,参 2):修改文件的读写指针位置 参 1: 偏移量: 正:向文件尾偏 负:想文件头偏 参 2:表示文件起始位置: io.SeekStart: 文件起始位置 io.SeekEnd: 文件结尾位置 io.SekCurrent: 文件当前位置 返回值:表示从文件的起始位置,到当前文件读写指针位置的偏移量。 ```go for i := 0; i < 10; i++ { f.WriteString("hello q\n") f.Seek(0, io.SeekEnd) } ``` - 按字节写 (常用) file.WriteAt():在文件指定偏移位置,写入[]byte,通常搭配 Seek() 参 1:待写入的数据 参 2:偏移量 返回:实际写出的字节数 ```go for i := 0; i < 20; i++ { off, _ := f.Seek(0, io.SeekEnd) f.WriteAt([]byte("hello wdc\n"), off) } ``` ### 读文件 - 按行读 1. 创建一个有缓冲区(用户缓冲)的 Reader(读写器) `reader := bufio.NewReader(打开文件的指针)` 2. 从 reader 的缓冲区,读取指定长度的数据。数据长度取决于 参数 dlime `buf,err :=reader.ReadBytes('\n')` (按行读) 3. 判断到达结尾:`if err !=nil && nil == io.EOF` 文件结束标记,是要单独读一次获取到的。 ```go //创建一个有缓冲区的reader reader := bufio.NewReader(file) for { buf, err := reader.ReadBytes('\n') if err != nil && err != io.EOF { fmt.Println("read file faild,err=", err) return } else if err == io.EOF { fmt.Println("文件读完了") return } fmt.Println(string(buf)) } ``` - 按字节读 1.read():按字节读 2.write():按字节写 练习,文件拷贝: ```go //文件拷贝 func main() { //打开读文件 f_r, err := os.Open("../file/20211029.vim") if err != nil { fmt.Println("open err :", err) return } defer f_r.Close() //创建写文件 f_w, err := os.Create("../file/20212031.vim") if err != nil { fmt.Println("create err :", err) return } defer f_w.Close() //从读文件中获取数据,放到缓冲区中 buf := make([]byte, 4*1024) //循环从读文件中获取数据,原封不动的写到写文件中 for { n, err := f_r.Read(buf) if err != nil && err != io.EOF { fmt.Println(err) return } else if err == io.EOF { fmt.Printf("文件读完了, %d\n", n) return } //读多少,写多少 f_r.Write(buf[:n]) } } ``` ### 目录操作 1. 打开目录 OpenFile: 只读,只写,读写 - 参数 1:打开文件的路径:相对/绝对 - 参数 2:打开文件的权限:1:O_RDONLY、2:O_WRONLY、3:ORDWR - 参数 3:os.ModeDir - 返回值:返回一个可以读写目录的文件指针 2. 读取目录项:file.ReadDir() - 参数:读取目录项的个数,-1 代表全部 - 返回值:[]os.DirEntry 3. 目录项函数: - DirEntry.IsDir() 是否为目录 - DirEntry.Name() 目录文件名称 ```go func main() { var path string fmt.Println("请输入要查看的目录(绝对路径)") fmt.Scan(&path) //打开目录 f, err := os.OpenFile(path, os.O_RDONLY, os.ModeDir) if err != nil { fmt.Println("open err :", err) return } defer f.Close() // 读取目录项 info, err := f.Readdir(-1) //-1,读取目录中的所有目录项 for _, fileInfo := range info { if fileInfo.IsDir() { fmt.Println(fileInfo.Name(), "是一个目录") } else { fmt.Println(fileInfo.Name(), "是一个文件") } } } ``` 练习:文件目录操作,完成特定 格式文件检索,文件拷贝,目录下特定单词数量统计 ```go var ( path string doType int ) func main() { fmt.Println("操作功能:\n1. 在特定目录下找出特定格式文件\n2.拷贝特定文件到特定目录下\n3.统计出给定目录下某个单词出现的次数") fmt.Println("请输入要进行的操作") fmt.Scan(&doType) fmt.Println("请输入要检索的目录") fmt.Scan(&path) openOldFile(path, doType) } func openOldFile(path string, doType int) { //打开目录文件 file, err := os.OpenFile(path, os.O_RDONLY, os.ModeDir) if err != nil { fmt.Println("open file faild ,err:", err) return } defer file.Close() //读取全部目录文件 info, err := file.ReadDir(-1) if err != nil { fmt.Println("read dir faild,err:", err) return } switch doType { case 1: { var fileType string fmt.Println("请输入要检索的文件类型") fmt.Scan(&fileType) nams := search(path, fileType, info) fmt.Printf("%s 下有 %s 格式文件有:%v\n", path, fileType, nams) } case 2: { var ( fileName string targetPath string ) fmt.Println("请输入要拷贝的文件") fmt.Scan(&fileName) fmt.Println("请输入要考入到的目录") fmt.Scan(&targetPath) copy(path, targetPath, fileName, info) } case 3: { var word string fmt.Println("请输入要检索的的单词") fmt.Scan(&word) c := count(path, word, info) fmt.Printf("%s 出现了%d次\n", word, c) } } } // 找出给定文件下 给定格式文件 func search(path, fileType string, info []os.DirEntry) (names []string) { //遍历目录文件 for _, fileInfo := range info { //判断是否为目录 if !fileInfo.IsDir() { //判断是否为 给定文件格式结尾 if strings.HasSuffix(fileInfo.Name(), fileType) { names = append(names, fileInfo.Name()) } } } return } func complatementPath(path string) string { if strings.HasSuffix(path, "/") { return path } else { path = path + "/" return path } } //拷贝给定文件到给定目录下 func copy(path, targetPath, fileName string, info []os.DirEntry) { for _, fileInfo := range info { if !fileInfo.IsDir() { if fileInfo.Name() == fileName { oldFile, err := os.OpenFile(complatementPath(path)+fileName, os.O_RDONLY, 6) if err != nil { fmt.Println("open old file faild,err :", err) return } defer oldFile.Close() newfile, err := os.Create(complatementPath(targetPath) + fileInfo.Name()) if err != nil { fmt.Println("create new file faild,err:", err) return } buf := make([]byte, 4*1024) for { //读文件到缓冲区中 n, err := oldFile.Read(buf) //判断文件是否读完毕 if n == 0 { fmt.Println("old file read over") return } if err != nil { fmt.Println("old file read faild ,err :", err) return } //写文件 newfile.Write(buf[:n]) } } fmt.Printf("已经将%s copy 到 %s中\n", complatementPath(path)+fileName, complatementPath(targetPath)+fileName) } } } //统计出给定目录下某个单词出现的次数。 func count(path, word string, info []os.DirEntry) int { var c int for _, fileInfo := range info { if !fileInfo.IsDir() { file, err := os.Open(complatementPath(path) + fileInfo.Name()) if err != nil { fmt.Printf("open %s faild,err:%s", complatementPath(path)+fileInfo.Name(), err) } defer file.Close() //创建一个带缓冲区的reader reader := bufio.NewReader(file) for { //按行读 buf, err := reader.ReadBytes('\n') if err != nil && err != io.EOF { fmt.Println("reader readBytes faild,err:", err) return 0 } else if err == io.EOF { fmt.Println(" file read over") break } //转换为字符串后统计 bufString := string(buf) count := strings.Count(bufString, word) c = c + count } } } return c } ``` ## 并发 **并行** 借助多核 cpu 实现。(真并行) **并发** - 程序:用户体验上,程序在并行执行 - 微观:多个计划任务,顺序执行。在飞快的切换,轮换使用 cpu 时间轮片。(假并行) **进程并发** - 程序:编译成功的到的二进制文件。 占用磁盘空间。 死的 1 - 进程:运行起来的程序。占用系统资源。(内存) 活的 N > 最小的系统资源分配单位 **进程状态** > 状态:初始态、就绪态、运行态、挂起(阻塞)态、终止(停止)态 **线程并发** - 线程:轻量级的进程(LWP) -----cpu 分配时间轮片的对象 > 最小的执行单位 **同步** > 协同步调,规划先后顺序 线程同步机制: - 互斥锁:建议锁。拿到锁之后才能访问数据,没有拿到锁的线程,阻塞等待,等拿到锁的线程释放锁。 - 续写锁:一把锁(读属性、写属性)。写独占,读共享。写优先级高。 - ... **协程并发** 提高程序执的效率 **总结** > - 进程:稳定性强 --->每个进程产生的时候都会开辟一个独立的进程地址空间(虚拟的,32 位机器可用范围高达 4G) > - 线程:节省资源 > - 协程:效率高 ### goroutine go 程是在进程里创建的 **goroutine 特性** > 主 goroutine 退出后,其它的工作子 goroutine 也会自动退出 ### runtime **runtime.Gosched()** > 出让当前 go 程所占用的 cpu 时间片,当再次获得 cpu 时,从出让位置继续执行。 **runtime.Goexit()** - return:返回当前函数调用到调用着那里去,return 之前的 defer 注册生效 - runtime.Goexit():结束调用该函数的当前 go,Goexit()之前注册的 defer 都生效。 **runtime.GoMAXPROCS()** - 来设置可以并行计算的 CPU 和数的最大值,并返回之前的值。 **runtime.GOROOT()** - 返回 Go 的根目录,如果存在 GOROOT 环境变量,返回环境变量的值,否则,返回创建 Go 时的跟目录。 **runtime.GC()** - 执行一次垃圾回收 ### channel > > channel 是一种数据类型,主要来解决 go 程的同步问题以及协程之间数据共享(数据传递)的问题,FIFO。 > > **goroutine 奉行 通过通信来共享内存,而不是共享内存来通信** **初始化** - 有缓冲:`make(chan int,3)`,缓冲区长度为 3,存储 int 类型的 channel - 无缓冲:`make(chan int)` - **读、写** - 读:`str := <- ch` > 也可以用 range 读取 channel: ```go for v:=range ch{ fmt.Println("v=",v) } ``` - 写:`ch <- "hello"` **len()、cap()** - len(ch):获取 channel 中剩余未读取数据个数,无缓冲区 channel 为 0 - cap(ch):通道的容量,无缓冲区 channel 为 0 ```go func main() { ch := make(chan int, 0) go func() { for { <-ch //等从该通道消费数据之后才继续执行,否则阻塞 fmt.Println("world") } }() go func() { for { fmt.Println("hello") ch <- 0 // 打印之后往 通道 塞入一个值,等其他go程消费,如果通道内的值没有被消费,该go程阻塞 time.Sleep(1 * time.Second) } }() for { } } ``` **无缓冲区 channel** ——同步通信 - 创建:ch :=make(chan int) 或 ch :=make(chan int,0) - 通道容量为 0,len()=0, 不能存储数据。 - channel 应用于两个 go 程中,一个读一个写 - 具备同步的能力。读写同步。(打电话) **有缓冲区 channel** ——异步通信 - 创建:ch :=make(chan int,5) - len(ch) :channel 中剩余未读取的元素个数。cap(ch):通道容量,不为 0。 - 缓冲区可以进行 数据存储,存储至容量上限,阻塞。具备异步能力,不需同时操作 channel 缓冲区。(发短信) **关闭 channel** - close(ch) : 确定不再向对端发送、接受数据时。 对段可以判断 channel 是否关闭: `if num,ok := <- chan ;chan ==true{}` 1. 如果对端已经关闭,ok --> false 2. 如果对端没有关闭,ok --> true, num 保存读到的数据 > 总结: > > 1. 数据不发送完,不应该关闭 > 2. 已经关闭的 channel,不能向其写数据。 > 3. 写端已经关闭 channel,可以从中读到数据 - 无缓冲:读到 0。———— 读到 0,说明:写端关闭 > - 有缓冲: 缓冲区的数据都读完之后,会读到 0。(读到 0,说明写端已经关闭) **单向 channel** - 默认的 channel 是双向的。 `var ch chan int`,`ch =make(chan int,8)` - 单向写 channel: `var sendChan chan <- int`, `sendChan = make(chan <- int,8)` 不能读操作 - 单向读 channel:`var recvChan <- chan int`, `recvChan = make(<- chan int ,8)` 不能写操作 - 转换: 1. 双向 channel 可以隐式转化为任意一种单向 channel sendChan = ch 2. 单向 channel 不能转换为双向 channel ```go func main() { ch := make(chan int) go sendChan(ch) //传双向channel,隐式转换 recvChan(ch) //传双向channel,隐式转换 } func sendChan(sendChan chan<- int) { //传参 只能写的channel for i := 0; i < 8; i++ { sendChan <- i } } func recvChan(recvChan <-chan int) { //传参 只能写的channel for i := 0; i < 8; i++ { i := <-recvChan fmt.Println("读到了", i) } ``` > **传参:传引用** **生产者消费者模型** - 生产者:生产数据 - 消费者:消费数据 - 缓冲区: - 解偶:降低生产者和消费者之间的耦合度 - 并发:生产者消费者数量不对等时,能保证正常通信 - 缓存:生产者消费者数据处理速度不一致时,暂存数据 模拟处理订单: ```go type orderInfo struct { orderId int } //模拟订单(生产消费者模型) func main() { ch := make(chan *orderInfo) go producter(ch) consumer(ch) } func producter(out chan<- *orderInfo) { //生产者:生成订单 for i := 1; i < 11; i++ { //循环生成10份订单 oi := &orderInfo{i} out <- oi //写入channel } close(out) //写完,关闭channel } func consumer(in <-chan *orderInfo) { //消费者:消费订单 for oi := range in { fmt.Printf("读到的orderId为:%d\n", oi.orderId) //模拟处理订单 } } ``` **定时器** - `time.Timer` Timer 是一个 定时器。代表未来的一个单一事件,你告诉 timer 你要等待多长时间。 > 创建定时器,指定定时时长,定时到达后,系统会自动向定时起成员 C 写系统当前时间。(对 chan 的写操作) > 读取 Timer.C 得到定时后的系统时间,并完成一次 chan 的读操作。 ```go type Time struct { C <- chan Time; //它提供一个 channel,在定时时间到达之前,没有数据写入,timer.C会一只阻塞,知道定时时间到达,系统会自动想timer.C这个channel中写入当前的时间,阻塞即被接触。 r runtimeTimer; } ``` - `time.After()` - 指定时长,定时到达后,系统会自动向定时器的成员写入系统当前时间。 - 返回可读 channel,读取可获取系统写入时间 **三种定义定时器方式** ```go func main() { //定时器1 fmt.Println("系统现在时间:", time.Now()) timer := time.NewTimer(2 * time.Second) //返回一个 timer nowTime := <-timer.C //系统将当前时间写进channel,本地读出来 fmt.Println("经过定时器1后当下时间:", nowTime) //定时器2 nowTime = <-time.After(2 * time.Second) //直接返回一个读的通道 fmt.Println("经过定时器2后当下时间:", nowTime) //定时器3 time.Sleep(2 * time.Second) fmt.Println("经过定时器3后当下时间:", time.Now()) } ``` > 总结:Sleep(),NewTimer(),After() 都属于 time 包 **定时器的停止,重置** 1. 创建定时器: timer := time.NewTimer(2 \* time.Second) 2. 停止:timer.Stop() ————重置定时器归 0 <-timer.C 会阻塞 3. 重置:timer.Reset(time.Second) ```go func main() { fmt.Println("现在的时间是:", time.Now()) timer := time.NewTimer(2 * time.Second) go func() { nowTime := <-timer.C //阻塞状态 fmt.Println("定时之后的时间是:", nowTime) }() timer.Stop() fmt.Println("定时停了之后的时间是:", time.Now()) } ``` **周期定时** ```go type Ticeker struct{ C <- chan Time r runtimeTimer } ``` - 创建 周期定时器 myTicker := time.NewTicker(time.Second) - 定时时长到达后,系统会自动向 Ticker 的 C 中写入系统当前时间 -并且每隔一个定时时长后,循环写入系统当前时间。 - 在子 go 程中循环获取 C,获取系统写入的时间 ```go func main() { fmt.Println("当前时间是:", time.Now()) ticker := time.NewTicker(1 * time.Second) go func() { for { nowTime := <-ticker.C fmt.Println("nowTime:", nowTime) } }() for { } } ``` ### select > Go 提供的一个关键字,通过 select 可以监听 channel 上的数据流动,语法跟 switch 类似,最大的区别是**select 的每个 case 语句必须是一个 IO 操作** 大致结构如下: ```go select{ case: <- channel1 //如果channel1 成功读到数据,则进行该case处理语句 case: <- channel2 //如果channel2 成功读到数据,则进行该case处理语句 case: <- channel3 //如果channel3 成功读到数据,则进行该case处理语句 default: //如果上面都没有成功,则进入default处理语句(平时不怎么使用) } ``` - 作用:用来监听 channel 上的数据流动方向。 - 注意事项: - 监听的 case 中,没有满足监听条件,阻塞 - 监听的 case 中,满足多个满足监听条件,任选一个执行 - 可以使用 defaultl 来处理所有 case 都不满足监听条件的状况。通常不用(会产生忙轮询) - select 自身不带有循环机制,需要借助外层 for 来循环监听 - beak 只能跳出 select 中的 case。类似于 switch 中的用法。 ```go func main() { ch := make(chan int) //用来进行数据通信的channel quit := make(chan bool)//用来用来是否退出的channel go func() { for i := 0; i < 5; i++ { <-time.After(time.Second) ch <- i } close(ch) quit <- true //通知主go程退出 }() for { select { case num := <-ch: fmt.Println("读到:", num) case <-quit: return //终止程序 } fmt.Println("============") } } ``` select 实现 feibonacci 数列 ```go func main() { ch := make(chan int, 0) quit := make(chan bool) go feibonacci(ch, quit) x, y := 1, 1 for i := 0; i < 100; i++ { <-time.After(time.Second) ch <- x x, y = y, x+y } close(ch) quit <- true close(quit) } func feibonacci(ch <-chan int, quit chan bool) { for { select { case num := <-ch: fmt.Print(num, " ") case <-quit: runtime.Goexit() } } } ``` **程序超时处理案例:** ```go func main() { ch := make(chan int) quit := make(chan bool) go sl(ch, quit) var i int for ; i < 7; i++ { time.Sleep(2 * time.Second) ch <- i } <-quit //如果读出数据,说明超时,程序该退出了 } //专门用来判断程序是否超时 func sl(ch <-chan int, quit chan<- bool) { for { select { case num := <-ch: fmt.Println("读到:", num) case <-time.After(5 * time.Second): fmt.Println("程序超时了") goto lable } } lable: fmt.Println("程序退出了") quit <- true //如果超时了,则在 该通道写入数据 } } ``` ### 锁跟条件变量 **_死锁_** 1. 单 go 程自己死锁 ```go ch := make(chan int) ch <- 1 //数据写进去,但是没有地方读,导致死锁 <-ch ``` > channel 应该在至少 2 个 go 程以上的程序中进行通信,否则死锁! 2. go 程间 channel 访问顺序导致死锁 ```go ch := make(chan int) <-ch //处于阻塞状态,下面的程序没法执行 导致死锁 go func() { ch <- 1 }() ``` > 使用 channel 一端读(写),要保证 另一端写(读)操作,同时有机会执行,否则死锁! 3. 多 go 程,多 channel 交叉死锁 ```go ch1 := make(chan int) ch2 := make(chan int) go func() { for { select { case num := <-ch1: ch2 <- num } } }() for { select { case num := <-ch2: ch1 <- num } } ``` > A go 程,掌握 M 的同时,尝试拿 N,B go 程,掌握 N 的同时尝试拿 M。 4. **在 go 语言中,尽量不要将 互斥锁、读写锁、与 channel 混用。——隐性死锁** **互斥锁** 建议锁:操作系统提供,建议你在编程中使用。 强制锁:操作系统自己会用到,我们在编程中使用不到。 - 使用: - 创建: var mutex sync.Mutex - 加锁:mutex.Lock() - 解锁:mutex.Unlock() - A、B go 程共同访问共享数据,由于 cpu 调度机制,需要队共享数据访问顺序加以限制(同步) - 创建 mutex(互斥锁),访问共享数据之前,加锁——>访问结束——>解锁,在 go 程加锁期间,B go 程加锁会失败(阻塞) - 直至 A go 程解锁 mutex,B 从阻塞处,恢复执行。 ```go //同步打印字符 func print(s string) { mutex.Lock() for _, c := range s { time.Sleep(300 * time.Millisecond) fmt.Printf(" %c", c) } mutex.Unlock() } ``` **读写锁** > 读时共享,写时独占,写锁优先级比读锁高 - 使用: - 创建:var rwMutex sync.RWMutex - 读锁: 1. 锁定:rwMutex.RLock() 2. 解锁:rwMutex.RUnlock() - 写锁: 1. 锁定:rwMutex.Lock() 2. 解锁:rwMutex.Unlock() 读写锁例子: ```go var value int var rwMutex sync.RWMutex func main() { //播种随机数种子 rand.Seed(time.Now().UnixNano()) //创建5个写go程 for i := 0; i < 5; i++ { go write(i + 1) } //创建5个读go程 for i := 0; i < 5; i++ { go read(i + 1) } for { } } func read(idx int) { for { rwMutex.RLock() //读上锁 num := value fmt.Printf("================第%d 个读go程,读入:%d\n", idx, num) //每一个写go程写了数据,所有的读go程 都能读到 rwMutex.RUnlock() //读解锁 time.Sleep(time.Second) } } func write(idx int) { for { //生成随机数 num := rand.Intn(1000) rwMutex.Lock() //写上锁 value = num fmt.Printf("第%d 个写go程,写入:%d\n", idx, value) rwMutex.Unlock() //写解锁 time.Sleep(2 * time.Second) } } ``` **条件变量** > 在对应的共享数据的状态发生变化时,通知阻塞在某个条件上的协程(线程)。**条件变量不是锁,在并发中不能达到同步的目的,因此条件变量总是与锁一块使用。** ```go type Cond struct { noCopy noCopy L Locker notify notifyList checker copyChecker } ``` 对应的有三个常用方法: - `func (c *Cond) Wait()` 该函数的作用可归纳为如下 3 点: - 阻塞等待条件变量满足 (谁调用谁阻塞) - 释放已掌握的互斥锁相当于`cond.L.Unlock()`。注意:**1、2 两步为原子操作** - 当被唤醒,`Wait()`函数返回时,解除阻塞并重新获取互斥锁,相当于`cond.L.Lock()` - `func (c *Cond) Signal()` 单发通知,给一个正等待(阻塞)在个该条件变量上的 go 程发送通知 - `func (c *Cond) Broadcast()` 单发通知,给所有正等待(阻塞)在个该条件变量上的 go 程发送通知(惊群效应) ### 网络 **协议** > 一组 **规则**,要求使用协议的双方,必须严格遵守协议内容。 **典型协议** - 传输层:常见协议有 TCP/UDB 协议 - 应用层:常见协议有 HTTP,FTP 协议 - 网络程:常见协议有 IP 协议,ICMP 协议,IGMP 协议 - 网络接口层:常见协议有 ARP 协议,RARP 协议。 **网络分层架构** > 每一层都有自己的功能,就像建筑一样,每一层靠下一层支持。**每一层利用下一层提供的服务来为上一层提供服务,本层服务的实现细节队上层屏蔽** [网络分层架构](./png/fencengmoxing.png) **层与协议** > 每一层都为了完成一种功能而遵守的共同规则。 > > 网络的每一层,都定义了很多协议,这些协议的总称,叫"TCP/IP 协议"。TCP/IP 协议是一个大家族,不仅仅只有 TCP 和 IP 协议,它还包括如下: > > ![层与协议](./png/层与协议.png) **各层功能** ![各层功能](./png/各层功能.png) - **链路层**:ARP 协议 **以太网规定,连入网络的所有设备,都必须具有"网卡"接口**。数据包必须是从一块网卡,传输到另一块网卡。通过网卡能够使不同的计算机之间连接,从而完成数据通信等功能。网卡的地址——MAC 地址,就是数据包的物理发送地址和物理接收地址。 > > ARP 协议作用:**借助 IP 地址,帮助获取 mac 地址**: > ARP 请求包携带这自己的 mac 地址,ip 地址跟目的 ip 地址等信息发送个路由器,路由器进行广播(局域网范围内),广播出去之后,目的 ip 地址匹配到之后返回 ARP 应答包,应答包中携带了自己的 mac 地址。 ARP 请求: ![ARP请求](./png/ARP协议请求.png) ARP 应答: ![ARP应答](./png/ARP协议返回.png) - **网络层**: IP 协议 > > IP 协议作用:**在网络环境中唯一标识一台主机** > > IP 地址本质:2 进制数。—— 点分十进制 IP 地址(String) - **传输层**:TCP/UDP 协议 > > TCP/UDP 协议作用:封装端口 port——在一台主机上唯一标识一个让进程 - **应用层**:ftp、http、自定义 > > 作用:解封装 **数据通信过程** ![数据通信过程](./png/数据包封装与解封装.png) - 封装:应用层——>传输层——>网络层——>链路层 (**没有经过封装的数据,不能在网络环境中传递**) - 解封装:链路程——>网络层——>传输层——>应用层 ### 网络应用设计模式 - C/S: - 优点: 数据传输效率高,协议选择灵活 - 缺点: 工作量大,对使用机器安全性构成危险 - B/S: - 优点:开发工作较小,不受平台限制,安全危险小 - 缺点:缓存数据差,协议选择不灵活 ### Socket 编程 > **在网络通信过程中,socket 一定是成对出现的。** socket 实质是内部封装了两个通道,形成一个两端各有一个出入口的双向通道。 ![socket特性](./png/socket特性.png) **TCP 的 C/S 架构** tcp 流程图: ![TCP流程图](./png/TCP流程图.png) 注: - `net.Listen()` 其实不是真正的监听,而是设置服务器监听的资源(通信方式,ip 地址,port 等) - 真正实现监听的方法是 `net.Accept()`,方法执行后产生一个套接字,该套接字阻塞等待用户链接。 - `net.Listen()` 会产生一个套接字,不用来通信,而是将自己保存的服务器监听的资源 copy 给别人使用。 - 客户端`net,Dial()`产生一个套接字,与服务器建立连接。建立起链接 之后就可以字节流的方式传输数据了。 socket 通信架构: ![socket通信架构](./png/简单CS模型通信.png) **TCP-CS 并发服务器** 1. 创建监听套接字 `listen ,err := net.Listen("tcp",服务器IP+Port)` //tcp 不能大写 2. defer listen.Close() 3. for 循环阻塞监听,客户端连接事件————`conn,err :=listen.Accept()` 4. 创建 go 程,对每一个客户端进行数据通信 `go handlerConnet()` - defer conn.Close() - 获取成功连接的客户端 addr————`conn.RemoteAddr()` - for 循环读取客户端发送数据————`conn.Read()` - 处理数据 - 回写给客户端 **服务器判断关闭** > Read 读客户端,返回 0——对端关闭 ```go func main() { //指定服务器通信协议,Ip,Port。创建一个用于监听套接字 listen, err := net.Listen("tcp", "localhost:8888") if err != nil { fmt.Println("net.Listen err:", err) return } defer listen.Close() fmt.Println("服务器等待客户端链接") for { //阻塞监听客户端连接请求,成功建立连接,返回用于通信的socket conn, err := listen.Accept() if err != nil { fmt.Println("net.Accept err:", err) return } //具体完成服务器和客户端的数据通信 go handConn(conn) } } func handConn(c net.Conn) { defer c.Close() //获取连接的客户端 addr addr := c.RemoteAddr() fmt.Println(addr, "客户端连接成功") //循环读取客户端发送的数据 buf := make([]byte, 4*1024) for { n, err := c.Read(buf) if err != nil { fmt.Println("conn.Read() error:", err) return } if n == 0 { fmt.Println("检测到客户端已经关闭,断开连接!!!") return } if string(buf[:n]) == "exit\n" { fmt.Println("客户端已经退出") return } fmt.Println("服务器接收到:", string(buf[:n])) //小写转大写,并回显给客户端 c.Write([]byte(strings.ToUpper(string(buf[:n])))) } } ``` **TCP-CS 并发客户端** 1. 主动发起请求连接:`conn,err :=net.Dail("tcp",服务器IP+Port)` 2. 另起 go 程获取用户键盘输入`os.Stdin.Read(buf)` 将输入数据发送给服务器 3. 写数据给服务器`conn.Write()` 4. 读取服务器回发的数据 `conn.Read()` 5. 关闭`conn.Close()` ```go func main() { //主动发起连接请求 conn, err := net.Dial("tcp", "localhost:8888") if err != nil { fmt.Println("net.Dial() error:", err) return } defer conn.Close() //获取用户键盘输入(os.sdin) 将输入数据发送给服务器 go func() { str := make([]byte, 4*1024) for { n, err := os.Stdin.Read(str) if err != nil { fmt.Println("os.Stdin.Read() err :", err) continue } //写给服务器,读多少,写多少 conn.Write(str[:n]) } }() //回显服务器挥发的大写数据 buf := make([]byte, 4*1024) for { n, err := conn.Read(buf) if err != nil { fmt.Println("conn.Read() err :", err) continue } fmt.Println("客户端读到服务器回发:", string(buf[:n])) } } ``` **TCP 通信过程** 下图是 TCP 通讯的时序图,TCP 连接建立断开,包含**三次握手和四次挥手** 三次握手 ![三次握手](./png/三次握手.png) 四次挥手 ![四次挥手](./png/四次挥手.png) > TCP 是面向连接的,可靠的数据包传输 ### UDP 通信 > 无连接的,不可靠的报文传递 - **udp 服务端** 1. 创建 server 端 地址结构`serverAddr, err := net.ResolveUDPAddr("udp", ip+port)` 2. 创建用于通信的 socket,绑定地址结构 `udpConn,err :=net.ListenUDP("udp",serverAddr)` 3. `defer UDPConn.Close()` 4. 读取客户端发送的数据 `n,cltAddr,err :=udpConn.ReadFromUDP(buf)` 5. 开 go 程写数据给客户端 `udpConn.WriteToUDP([]byte("..."))` - **udp 客户端** 参考 TCP 客户端, `net.Dail("udp",Ip+por)` **TCP 跟 UDP 对比** | | |'TCP'|'UDP'| |面向连接|面向无连接| |要求系统资源较多|要求系统资源较少| |TCP 程序结构复杂|UDP 程序结构简单| |使用流式|使用数据包式| |保证数据准确性|不保证数据准确性| |保证数据顺序|不保证数据顺序| |通讯速度较慢|通讯速度较快| 使用此场景: - TCP:对数据传输安全性、稳定性要求较高的场合。如:网络文件传输,下载,上传。 - UDP:对数据事实传输要求较高的场合。如:视频直播,在线电话会议,游戏。 ### 网络文件传输 **步骤** 发送端: 1. 提示用户命令行参数输入文件名,接受文件名 filePath(含访问路径) 2. 使用 `os.State()`获取文件属性,得到纯文件名 fileName(取出访问路径) 3. 主动发起连接服务器请求,结束时关闭连接。 4. 发送文件名到接受端 `conn.Write()` 5. 读取接收端确认数据 `con.Read()` 6. 判断是否 "ok" ,如果是,函数封装 `SendFile()`发送文件内容,传参 `filePath `和 conn 7. 只读`Open`文件,结束时`Close`文件 8. 循环读本地文件,读到 `EOF`,读取完毕 9. 将读到的内容原封不动 `conn.Write()`给接受端(服务端) 接受端 1. 创建监听 listener,程序结束时关闭 2. 阻塞等待客户端连接 conn,程序结束时关闭 conn 3. 读取客户端发送文件名,保存 fileName 4. 回发 "ok" 5. 封装函数 RecvFile 接受客户端发送的文件内容,传参 fileNanem 和 conn 6. 按文件名 Create 文件。结束时 Close 7. 循环 Read 发送端网络文件内容,读到 0 说明文件读取完毕 8. 将读取到的内容原封不动 Write 到创建文件中 ### 聊天室 简单写一个聊天室(群聊),支持用户上线、下线、查看用户列表、超时强踢、私聊功能。 **服务端** - server : ```go package main import ( "fmt" "net" "sync" ) type Server struct { IP string Port int OnlineMap map[string]*User //用来存储上线的用户 Messag chan string //用来广播需要发送给用户的消息 Lock sync.RWMutex } //创建服务 func NewServer(ip string, port int) *Server { return &Server{ IP: ip, Port: port, OnlineMap: make(map[string]*User), Messag: make(chan string), } } //服务启动 func (s *Server) Start() { //设置监听 listen, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.IP, s.Port)) if err != nil { fmt.Println("net.Listen err:", err) return } defer listen.Close() //开启go程开启消息监听 go s.ListenMsg() for { //建立链接 conn, err := listen.Accept() if err != nil { fmt.Println("listen.Accept err", err) return } defer conn.Close() go s.HandlerServer(conn) } } //处理主要业务 func (s *Server) HandlerServer(conn net.Conn) { //根据链接创建用户 user := NewUser(conn, s) user.Online() //读取用户客户端发来的数据 go user.HandlerServer() } //将用户发送的消息发送到server的chann中 func (s *Server) BroadCast(u *User, msg string) { BroadCastMsg := fmt.Sprintf("「%s」:%s", u.Name, msg) s.Messag <- BroadCastMsg } //监听server中 channel 中的消息,将里面的消息发送给每个用户的channel func (s *Server) ListenMsg() { for { msg := <-s.Messag s.Lock.Lock() for _, user := range s.OnlineMap { user.C <- msg } s.Lock.Unlock() } } ``` - user: ```go package main import ( "fmt" "io" "net" "strings" "time" ) //用户角色 type User struct { Name string Addr string C chan string //用来接受服务广播的消息 conn net.Conn //用户与服务建立的链接 Server *Server } type PriUser struct { //用来存储私聊姓名 // var toUserName string PriName string // 用来存储私聊对象 PriUser *User } func NewUser(conn net.Conn, server *Server) *User { addr := conn.RemoteAddr().String() user := &User{ Name: addr, Addr: addr, C: make(chan string), conn: conn, Server: server, } // 开启go程监听消息 go user.ListenMsg() return user } //随时监听接收服务发来的消息 func (u *User) ListenMsg() { for { //服务器发给用户的消息 msg := <-u.C //将接受到的消息发送给客户端 u.conn.Write([]byte(msg)) } } //user 上线功能 func (u *User) Online() { //将用户添加到server.OnlineMap 中 u.Server.Lock.Lock() u.Server.OnlineMap[u.Name] = u u.Server.Lock.Unlock() //将该用户上线的消息存入到server通道中 u.Server.BroadCast(u, "上线了\n") } //user 下线功能 func (u *User) offline() { u.Server.Lock.Lock() u.Server.OnlineMap[u.Name] = u delete(u.Server.OnlineMap, u.Name) u.Server.Lock.Unlock() u.Server.BroadCast(u, "下了,拜拜") u.conn.Close() } //user 处理业务逻辑功能 func (u *User) HandlerServer() { //用来判断是否超时 isLine := make(chan bool) go func() { priUser := new(PriUser) buf := make([]byte, 1024*4) for { n, err := u.conn.Read(buf) if n == 0 { u.offline() return } if err != nil && err != io.EOF { fmt.Println("conn.Read err:", err) return } msg := string(buf[:n-1]) userMsg := priUser.PriName + msg isLine <- true // 查看用户列表 if userMsg == "who" { u.SendMsg("当前用户有:") u.Server.Lock.Lock() for userName, _ := range u.Server.OnlineMap { u.SendMsg(userName) } u.Server.Lock.Unlock() //修改用户姓名 } else if len(userMsg) > 7 && strings.Contains(userMsg, "rename|") { newName := strings.Split(userMsg, "|")[1] u.Server.Lock.Lock() u.Server.OnlineMap[newName] = u delete(u.Server.OnlineMap, u.Name) u.Server.Lock.Unlock() u.Name = newName u.SendMsg(fmt.Sprintf("成功将姓名修改为:%s", newName)) //私聊功能 } else if len(userMsg) > 3 && strings.Contains(userMsg, "to|") { userName := strings.Split(userMsg, "|")[1] if strings.Count(userMsg, "|") == 1 { if user, ok := u.Server.OnlineMap[userName]; ok { u.SendMsg(fmt.Sprintf("开始跟%s私聊吧", userName)) //确定私聊对象后将 对象姓名按一定格式保存 priUser.PriName = fmt.Sprintf("to|%s|", userName) //同时将私聊对象保存起来 priUser.PriUser = user priUser.PriUser.C <- fmt.Sprintf("即将有来自[%s]用户的私聊", u.Name) } else { u.SendMsg(fmt.Sprintf("%s不在线上", userName)) } } else if strings.Count(userMsg, "|") == 2 { //私聊时 消息发送 if msg != "exit" { priUser.PriUser.C <- msg //退出私聊时,将私聊对象跟对象姓名置为空 } else { priUser.PriName = "" priUser.PriUser = new(User) } } } else { u.Server.BroadCast(u, userMsg) } } }() //超时强踢 for { select { case <-isLine: case <-time.After(100 * time.Second): u.SendMsg("你因超时要被踢出去了") u.Server.Lock.Lock() delete(u.Server.OnlineMap, u.Name) u.Server.Lock.Unlock() u.conn.Close() } } } //用户自己给客户端发送消息 func (u *User) SendMsg(msg string) { u.conn.Write([]byte(msg + "\n")) } ``` - main ```go package main func main() { server := NewServer("127.0.0.1", 8888) server.Start() } ``` **Client** - main ```go package main import ( "fmt" "io" "net" "strings" ) func main() { //客户端主动发起链接 conn, err := net.Dial("tcp", "127.0.0.1:8888") if err != nil { fmt.Println("net.Dial err", err) return } defer conn.Close() go func() { //向客户端写如数据 var msg string //读取命令行数据 for { fmt.Scan(&msg) conn.Write([]byte(msg + "\n")) } }() //循环读服务端用户写过来的数据 buf := make([]byte, 1024*4) for { n, err := conn.Read(buf) if n == 0 { fmt.Println("断开链接...") return } if err != nil && err != io.EOF { fmt.Println("conn.read err:", err) return } msg := string(buf[:n]) go func() { //判断是否是私聊消息 if strings.Contains(msg, "即将有来自[") && strings.Contains(msg, "]用户的私聊") { priUserName := strings.Split(strings.Split(msg, "[")[1], "]")[0] //开启私聊模式 priMsg := fmt.Sprintf("to|%s", priUserName) conn.Write([]byte(priMsg + "\n")) } }() // 打印 fmt.Println(msg) } } ``` ### HTTP 编程 - DNS 域名服务器 :域名和与之相对应的 IP 地址相互转换的服务器 - **WEB 工作方式:** 1. 客户端 --> 访问域名 --> DNS 服务器,返回该域名相对应的 IP 地址 2. 客户端 --> IP + Port --> 访问网页数据。 (TCP 连接,HTTP 协议) - **HTTP 和 URL:** **HTTP**:超文本传输协议,规定了浏览器访问 WEB 服务器进行数据通信的规则。http(明文) --> TLS、SSL --> https(加密) **URL**:统一资源定位,在网络环境中唯一定位一个资源数据,浏览器地址栏内容。 - **http 请求包** - 请求行 :请求方法「GET、POST」(空格)请求文件 URL(空格)协议版本(/r/n) - 请求头:语法格式:key :value - 空行: \r\n (代表 http 请求头结束) - 请求包体:请求方法对应的数据内容。GET 方法没有内用 - http 应大包 - 状态行:协议版本号(空格) 状态码(空格) 状态码描述(\r\n) - 响应头:语法格式:key : value - 空行: \r\n (代表 http 响应头结束) - 响应包体:请求内容存在:返回请求页面内容; 请求内容不存在:返回错误页面描述 ![web工作方式](./png/http请求报文.png) ### gRPC - 环境安装 #### protobuf 是一种轻便高效的结构化数据存储格式,平台无关,语言无关,可扩展,可用于用讯协议和数据存储等领域。 数据交互的格式比较 1. json:一般的 web 项目中,最流行的还是 json,因为 浏览器对与 json 数据支持非常友好。 2. xml:在 webservice 中应用最为广泛,但相比 json。它的数据更佳冗余,因为需要成对的闭合标签,json 使用了键值对的方式,不仅压缩了一定的数据空间,同时也具有可读性。 3. protobuf:适合高性能,对相应速度有要求的数据传输场景,因为 protobuf 是二进制数据格式,需要编码和解码,数据本身不具有可读性,因此只能反序列化之后得到真正可读的数据。 **优势** - 序列化后体积相比 json 和 xml 很小,适合网络传输。 - 支持跨平台多语言. - 消息格式升级和兼容性不错. - 序列化反序列化速度很快,快于 json 的处理速度。 **安装** ```shell 1. git clone https://github.com/protocolbuffers/protobuf.git 或者 unzip protonuf.zip 2. sudo pacman -S autoconf automake libtool curl make g++ unzip libffi-dev -y 3. ./autogen.sh #进行环境检查 4. ./configure #进行文件检查 5. make #编译 6. sudo make install #安装 7. sudo ldconfig #刷新共享库 8.protoc --version ``` 获取 proto 包 ```go go install github.com/golang/protobuf/proto@latest ``` 安装 protoc-gen-go 插件 ```go #安装 go install google.golang.org/protobuf/cmd/protoc-gen-go@latest sudo cp $GOPATH/bin/protoc-gen-go $GOROOT/bin ``` **protobuf 语法** 1. 定义 proto 文件 (1). 定义消息 ```go //版本 syntax = "proto3"; //包位置 option go_package = "prototest"; //定义消息类型 message PandaRequest{ string name =1; int32 age =2 repeated int32 height = 3; //切片或者数组的数据类型 } message PandaRespons{ int32 err =1; string ermessage =2 } ``` (2). 定义服务 ```go service SearchService{ rpc Search (SearchRequest) return (SeachResponse); } ``` > 注:因为 protobuf 是可扩展的序列化结构数据格式。字段等号后面的顺序为它们的序列。 2. 将 proto 文件编译成 go 文件 (rpc 方式) ```go protoc --go_out=./ *.proto ``` 3. 使用编译的文件 ```go pandaRequest := &prototext.PandaRequest{Name: "wdc", Age: 25, Height: []int32{175}} fmt.Println(pandaRequest) //编码 data, err := proto.Marshal(pandaRequest) if err != nil { fmt.Println("编码失败") } fmt.Println(data) //解码 newtest := &prototext.PandaRequest{} proto.Unmarshal(data, newtest) fmt.Println(newtest) ``` #### rpc > 跟远程访问或 web 请求差不多。都是一个 client 向远端服务请求服务返回结果,但是 web 请求使用的网络协议是 http 协议,而 rpc 所使用的协议多为 TCP,是网络层协议,减少了信息的包装,加快了处理速度。 > 服务端注册一个对象,使它作为一个服务被暴露,服务的名字是该对象的类型名,注册之后,对象的导出方法就可以被远程访问。服务端可以注册多个不同类型的对象(服务),但注册具有相同类型的多个对象是错误的。 满足能被远程访问的的标准: - 方法是导出的。 - 方法有两个参数,都是导出类型或者内建类型。 - 方法的第二个参数是指针。 - 方法只有一个 error 接口类型的返回值。 看起来向这样 ```go func (t *T) MethodName(argeType T1, replyType *T2) error ``` 例子: server: ```go type Rpctest int func (r *Rpctest) GetInfo(argType int, reply *int) error { fmt.Println("打印端发送过来的内容为:", argType) reply +=1 return nil } func test(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "hello go") } func main() { //页面的请求 http.HandleFunc("/test", test) { //实例化对象 rt := new(rpctest) //服务注册一个对象 rpc.Register(rt) rpc.HandleHTTP() } listen, err := net.Listen("tcp", ":8888") if err != nil { fmt.Println("网络错误") } http.Serve(listen, nil) } ``` client ```go func main() { //建立网络链接 cli, err := rpc.DialHTTP("tcp", ":8888") if err != nil { fmt.Println("网络链接错误") } var pd int cli.Call("Rpctest.GetInfo", 8888, &pd) if err != nil { fmt.Println("打 call 失败") } fmt.Println("最后得到的值", pd) } ``` #### gRPC **安装** ```shell go install google.golang.org/grpc@latest ``` **安装 protoc-gen-go-grpc 插件** ```shell go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest ``` **编写proto 文件** ```proto syntax = "proto3"; option go_package ="/pb"; message HelloReq { string name = 1; } message HelloResp { string message = 1; } service Greeter { rpc SayHello(HelloReq)returns(HelloResp){} rpc SayHelloAgain(HelloReq)returns(HelloResp){} } ``` **将 proto 文件编译成 go 文件(grpc 注册方式)** ```shell protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto ``` **编写服务端** ```golang package main import ( "context" "fmt" "go_keep_learning/grpc/micro/pb" "net" "google.golang.org/grpc" ) type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, req *pb.HelloReq) (rep *pb.HelloResp, e error) { return &pb.HelloResp{Message: "hello" + req.GetName()}, nil } func (s *server) SayHelloAgain(ctx context.Context, req *pb.HelloReq) (rep *pb.HelloResp, e error) { return &pb.HelloResp{Message: "helloAgin" + req.GetName()}, nil } func main() { // 初始化 grpc 对象 grpcServer := grpc.NewServer() //注册服务 pb.RegisterGreeterServer(grpcServer, new(server)) //监听 listen, err := net.Listen("tcp", ":8800") if err != nil { fmt.Println("listen err", err) return } defer listen.Close() //run grpcServer.Serve(listen) } ``` **编写客户端** ``` golang package main import ( "context" "fmt" "go_keep_learning/grpc/micro/pb" "google.golang.org/grpc" ) func main() { // 链接服务 grpcConn, _ := grpc.Dial(":8801", grpc.WithInsecure()) //初始化 grpc 客户端 grpcClient := pb.NewGreeterClient(grpcConn) //调用远程函数 resp, err := grpcClient.SayHello(context.TODO(), &pb.HelloReq{Name: "wc"}) if err != nil { fmt.Println(nil) return } fmt.Println(resp) resp3, err := grpcClient.SayHello(context.TODO(), &pb.HelloReq{Name: "wc"}) fmt.Println(resp3) } ``` #### go-micro **安装** https://releases.hashicorp.com/consul/ 网页地址 解压之后将命令移动到 /user/local/bin/目录下 **启动/关闭consul** - 默认方式开启consul服务: consul agent -dev -config-dir=/etc/consul.d/ - 默认ui地址:http://localhost:8500/ (命令查看实例:consul members) - 查看基本信息 :consul info - 重新加载配置: consul reload - 优雅关闭consul: consul leave **注册服务(本地)** 在配置文件中 /etc/consul.d/ 添加 一个web.json : ```json { "service":{ "name":"windchim", "tags":[ "qimi" ], "port":8800, "check":{ "id":"api", "name":"HTTP API on port 9000", "http":"http://localhost:9000", "interval":"10s", "timeout":"1s" } } } ``` **注册服务(代码形势)** - 安装consul/api包: go get -u github.com/hashicorp/consul/api - 服务端代码 ```golang package main import ( "context" "fmt" "go_keep_learning/grpc/micro/micropb" "net" "github.com/hashicorp/consul/api" "google.golang.org/grpc" ) type Server struct { micropb.UnimplementedGreeterServer } func (s *Server) SayHello(ctx context.Context, in *micropb.HelloReq) (out *micropb.HelloResp, e error) { return µpb.HelloResp{Message: "helllo " + in.Name}, nil } func main() { // 把grpc服务,注册到consul上 //初始化 consul配置 consulConfig := api.DefaultConfig() //创建 consul 对象 cli, err := api.NewClient(consulConfig) if err != nil { fmt.Println(err) return } //告诉consul 即将注册的服务配置的信息 registerService := &api.AgentServiceRegistration{ ID: "wind chime", Tags: []string{"qimi"}, Name: "grpc and consul", Address: "127.0.0.1", Port: 8800, Check: &api.AgentServiceCheck{ CheckID: "consul grpc test", TCP: "localhost:8800", Timeout: "1s", Interval: "5s", }, } //注册 服务到 consul 上 cli.Agent().ServiceRegister(registerService) grpcServer := grpc.NewServer() micropb.RegisterGreeterServer(grpcServer, new(Server)) listener, err := net.Listen("tcp", ":8800") if err != nil { fmt.Println(err) return } grpcServer.Serve(listener) } ``` -客户端代码 ```golang package main import ( "context" "fmt" "go_keep_learning/grpc/micro/micropb" "github.com/hashicorp/consul/api" "google.golang.org/grpc" ) func main() { //初始化consul配置 consulConfig := api.DefaultConfig() //创建consul对象 cli, err := api.NewClient(consulConfig) if err != nil { fmt.Println(err) } //////服务发现,从consul上获取健康的服务 ////参数 //service :服务名 // tag: 别名 // passingOnly :是否通过健康检查, true //q :查询参数,通常为nil ////返回值 //ServiceEntry:存储服务的切片 //QueryMeta :额外的查询返回值,通常为nil //error :错误 services, _, err := cli.Health().Service("grpc and consul", "qimi", true, nil) if err != nil { fmt.Println(err) } for _, v := range services { addr := fmt.Sprintf("%s:%d", v.Service.Address, v.Service.Port) ////////////////////////以下为 grpc 服务远程调用//////////////////////////////// grpcCon, err := grpc.Dial(addr, grpc.WithInsecure()) if err != nil { fmt.Println(err) } grpcClient := micropb.NewGreeterClient(grpcCon) resp, err := grpcClient.SayHello(context.TODO(), µpb.HelloReq{Name: "wc"}) if err != nil { fmt.Println(err) } fmt.Println(resp) } } ``` - 注销服务代码 ```golangi package main import ( "github.com/hashicorp/consul/api" ) func main() { consulConfig := api.DefaultConfig() cli,_ := api.NewClient(consulConfig) cli.Agent().ServiceDeregister("wind chime") } ``` ### etcd 作为 服务注册中心 **安装etcd** ```shell wget https://github.com/etcd-io/etcd/releases/download/v3.4.5/etcd-v3.4.5-linux-amd64.tar.gz tar -zxvf etcd-v3.4.5-linux-amd64.tar.gz cd etcd-v3.4.5-linux-amd64 ./etcd ```