> 技术文档 > Golang基础语法

Golang基础语法


1.Golang介绍与环境搭建

1.1Golang介绍

golang又称go language简称golang, go语言是谷歌推出的一种编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗勃派克说:‘’我们之所以开发go,是因为过去十年间软件开发的难度令人沮丧‘’。派克表示,和今天C和C++一样,go是一种系统语言。使用它可以快速开发,同时它还是一个真正的编译语言,我们之所以将它开源,原因是我们认为它已经非常有用和强大。

1)计算机硬件更新频繁,性能提高很快。目前主流的编程语言发展明显落后于硬件,不能合理的利用多核多CPU的优势来提升软件系统的性能。

2)软件系统的复杂度越来越高,维护成本越来越高,目前需要一个足够简洁高效的编程语言。

3)企业运行维护很多C/C++的项目,C/C++运行速度快但是编译速度却很慢。同时还存在内存泄漏等一系列困扰需要解决。

1.2Golang安装

1.官网下载SDK(运行go的前提)

2.一直下一步

1.3Windows下常用的命令

1)dir:查看当前目录下的文件列表

2) D: 进入D盘

3) MD/md/mkdir fileName :创建文件名目录

       RD fileName :删除目录

4) cls :清空DOS命令窗口内容

5) copy file1 file2 :将file1的内容复制到file2中

6) cd .. :返回上级目录

7) ./ :当前目录

8) del fileName :删除文件

1.4Golang项目的大致目录结构

文件手动创建

 1.5Golang的hello world

 注意:

1) 在go语言里命名为main的包具有特殊的含义,Go语言的编译程序会试图把这种命名的包编译成二进制的可执行程序文件

2)所有用Go语言编译的可执行程序都必须要有一个名叫main的包

3)一个可执行程序有且只有一个main包

4)当编译器发现某一个包为main时,它一定会发现main() 函数 ,否则不会创建可执行文件

5)程序编译时,会使用声明main包代码所在目录的目录名为二进制可执行程序的文件名

6)源文件以go为拓展名

7)源文件字母严格区分大小

8)方法由语句构成,每条语句末尾不需要“;”结尾

9)go的编译器是一行一行进行编译,因此每行只能写一条语句,否则报错,或者使用\';\'进行隔开,但是失去代码简洁性

10)定义的变量或者import包没有使用到则编译不能通过,体验简洁性

1.6Golang的文件编译

使用命令窗口 进入主程序目录下

  1. 使用命令: go build test.go 在当前目录下生成test.exe二进制可执行文件

  2. 使用.\\test.exe可运行该编译文件,或者使用go run test.go直接编译执行源文件

  3. 说明:编译之后生成的二进制可执行文件变大是因为是编译程序把程序所依赖的库文件打包编译。

  4. 直接go run 运行文件不可迁移至非go环境上运行,而go build之后的文件能够运行在其他环境上

  5. 编译时可以额外指定名字,默认使用main函数文件名,使用go build -o name.exe test.go 可指定编译后生成的二进制可执行文件的文件名

1.7前置知识

1) 缩进:

Tab键向后缩进

Shift + Tab键向后缩进

格式化命令:格式效果展示 gofmt test.go 效果写入源文件 gofmt -w test.go

Ctrl + s 保存。在vscode中会附带格式化

2)格式统一。声明函数同一行中必须要由{,以下代码是错误示范:

 一行尽量不要超过80个字符,Println函数可以输出多个,使用‘,’进行隔开。

3)Go语言的API文档Go语言标准库文档中文版 | Go语言中文网 | Golang中文社区 | Golang中国

 源码查看方式:

对应官方文档:

 2.Golang的变量与运算

2.1Golang的变量

Golang是一种强制类型定义的语言。其变量类型如下:

 (1) 整型
类型 有无符号 占用存储空间 表示范围 int 有 32位操作系统4字节/64位操作系统8字节 -2**31~-2**31-1/-2**63~2**63-1 int8 有 1字节8位 -2**7~2**7-1 int16 有 2字节16位 -2**15~2**15-1 int32 有 4字节32位 -2**31~2**31-1 int64 有 8字节64位 -2**63~2**63-1 uint 无 32位操作系统4字节/64位操作系统8字节 0~-2**32-1/0~2**64-1 uint8 无 1字节8位 0~2**8-1 uint16 无 2字节16位 0~2**16-1 uint32 无 4字节32位 0~2**32-1 uint64 无 8字节64位 0~2**64-1

注意:使用适当的变量类型能够节约内存空间

package mainimport (\"fmt\"\"math\")func main() {// 声明int类型var num1 int// << 左移运算符,1表示0000 0000 · 0000 0000 · 0000 0000 · 0000 0000 · 0000 0000 · 0000 0000 · 0000 0000 · 0000 0001num1 = 1 << 62  // num1 = 1 << 63将会报错,因为int类型在64位操作系统中只有64位,其中一位为符号位,左移63位超出表示范围num1 = int(math.Pow(2, 63)) //使用math库中的Pow函数进行幂运算fmt.Println(\"num1 = \", num1)num1 = -1 * int(math.Pow(2, 64)) //使用math库中的Pow函数进行幂运算fmt.Println(\"num1 = \", num1)// 声明uint类型var num2 uint8num2 = 128fmt.Println(\"num2 = \", num2)// num2 = 263// fmt.Println(\"num2 = \", num2)/* 超出范围报错# command-line-arguments.\\main.go:22:9: cannot use 263 (untyped int constant) as uint8 value in assignment (overflows)*/}
(2) 浮点型

浮点型常用来存放小数使用

类型 存储空间 表示范围 单精度float32 4字节32位 -3.403E38~3.403E38 双精度float64 8字节64位 -1.798E308~1.798E308

注意:底层存储空间和操作系统无关,浮点数在计算机中的存储方式导致其存在精度损失,建议使用float64

package mainimport \"fmt\"func main() {// 定义一个浮点数var num1 float32 = 3.1415926fmt.Println(\"num1 = \", num1) // num1 = 3.1415925num1 = 3.141592697989293849fmt.Println(\"num1 = \", num1) // 存在精度损失 num1 = 3.1415927// 扩大精度var num2 float64 = 3.1415926fmt.Println(\"num2 = \", num2) //num2 = 3.1415926num2 = 3.141592697989293849fmt.Println(\"num2 = \", num2) // 更精确时仍然存在精度损失num2 = 3.1415926979892936// 系统默认使用float64位num3 := 3.1293fmt.Printf(\"num3的数据类型:%T\", num3)}
 (3) 字符类型

Golang没有单独的字符型,使用Byte来保存单个字母字符,byte:uint8的别名,ASCII码字符用此表示,1个字节

package mainimport (\"fmt\"\"unsafe\")func main() {// 汉字使用unicode的utf-8编码 不能使用byte接收需要更大的接收如uint16var c int = \'中\'fmt.Println(\"c :\", c)  //c1 : 20013fmt.Printf(\"c 类型 :%T\\n\", c)  //c1 类型 :intfmt.Println(\"c占用字节数 :\", unsafe.Sizeof(c)) //c1占用字节数 : 8// 想现实对应的字符必须采用格式化输出fmt.Printf(\"c所表示的字符:%c\\n\", c) // c所表示的字符:中// 定义字符类型的数据,通常使用ASCII码var c1 byte = \'a\'fmt.Println(\"c1 :\", c1)  //c1 : 97fmt.Printf(\"c1 类型 :%T\\n\", c1)  //c1 类型 :uint8fmt.Println(\"c1占用字节数 :\", unsafe.Sizeof(c1)) //c1占用字节数 : 1// 想现实对应的字符必须采用格式化输出fmt.Printf(\"c1所表示的字符:%c\\n\", c1) // c1所表示的字符:a}
(4) 转义字符

\\转移字符: 将后面的字母表示成特殊的含义

转义字符 含义 unicode值 \\b 退格(backspace) \\u0008 \\n 换行 \\u000a \\r 回车 \\u000d \\t 制表符(tab) \\u0009 \\\" 双引号 \\u0022 \\\' 单引号 \\u0027 \\\\ 反斜杠 \\u005c
(5) 布尔类型

布尔类型又称bool类型,只允许取true和false,布尔类型占用一个字节,适用于逻辑判断流程控制中

package mainimport (\"fmt\"\"unsafe\")func main() {//布尔类型声明和使用var b1 bool = truefmt.Println(\"b1的数据值:\", b1)  //b1的数据值: truefmt.Printf(\"b1的类型:%T\\n\", b1) //b1的类型:boolfmt.Printf(\"b1的真实表示:%t\\n\", b1)  //b1的真实表示:truefmt.Println(\"b1的数据值大小:\", unsafe.Sizeof(b1)) //b1的数据值大小: 1}
 (6) 字符串类型

字符连起来的字符序列

package mainimport \"fmt\"func main() {// 字符串的使用var s1 string = \"全面拥抱golang\"fmt.Println(s1)// 字符串不可变,字符串一旦定义好其中字符值就不能改变,除非重新创建新的字符串s2 := \"abc\"// s2[0] = \'n\' //.\\main.go:11:2: cannot assign to s2[0] (neither addressable nor a map index expression)s2 = \"zhan\"fmt.Println(s2)// 字符串的表示形式,遇到多个特殊字符时可使用反引号``进行完整输出表示s3 := \"askjdhjk\\\"asd\"s3 = `func main() {//布尔类型声明和使用var b1 bool = truefmt.Println(\"b1的数据值:\", b1)  //b1的数据值: truefmt.Printf(\"b1的类型:%T\\n\", b1) //b1的类型:boolfmt.Printf(\"b1的真实表示:%t\\n\", b1)  //b1的真实表示:truefmt.Println(\"b1的数据值大小:\", unsafe.Sizeof(b1)) //b1的数据值大小: 1}`fmt.Println(s3)// 字符串还能进行拼接s4 := s1 + \"\\t\" + s2fmt.Println(s4)// 注意字符串拼接时,➕应该保存在每行的最后,不能使用s6 := \"abc\"*8s5 := \"abc\" + \"abc\" + \"abc\" + \"abc\" + \"abc\" + \"abc\" + \"abc\" + \"abc\" + \"abc\" + \"abc\" +\"abc\" + \"abc\" +\"abc\" + \"abc\"fmt.Println(s5)}
(7) 数据默认值
数据类型 默认值 整数类型 0 浮点类型 0 布尔类型 false 字符类型 \'\' 字符串类型 \"\"
package mainimport \"fmt\"func main() {// 整数的默认值var i intfmt.Println(\"整数的默认值:\", i) //整数的默认值: 0// 浮点数的默认值var f float32fmt.Println(\"浮点数的默认值:\", f) // 浮点数的默认值: 0// 布尔类型的默认值var b boolfmt.Println(\"布尔类型的默认值:\", b) // 布尔类型的默认值: false// 字符类型默认值var c bytefmt.Println(\"字符类型的默认值:\", c) //字符类型的默认值: 0// 字符串类型的默认值var s stringfmt.Println(\"字符串类型的默认值:\", s) //字符串类型的默认值:}
(8) 基本数据类型之间转换

不同类型之间赋值时需要显示转换,并且只有显示转换(强制转换)

语法:

        1.表达式 T(v)将v强制转换成T类型

        2.使用SPrintf()按照基本类型的格式进行转换返回字符串

        3.使用strconv库中的函数进行转换

package mainimport (\"fmt\"\"strconv\")func main() {//浮点型转整型b := 3.14a := int(b)fmt.Println(a) // 3//整型转浮点型c := 8d := float64(c)fmt.Println(d) //8//字符型转整形e := \'e\'f := int(e)fmt.Println(f) //101//字符型转浮点型g := \'g\'h := float64(g)fmt.Println(h) //103//大整型转小整型var num1 int32 = 2999num2 := int8(num1)fmt.Println(num2) //-73// 整型转字符型num3 := 99num4 := byte(num3)fmt.Printf(\"%c\\n\", num4) // c// 基本类型转换成字符串类型n1 := 90f1 := 3.14b1 := falses1 := fmt.Sprintf(\"%d\", n1) // 90fmt.Print(s1)fmt.Printf(\"%T\\n\", s1) // strings2 := fmt.Sprintf(\"%3.3f\", f1) // 3.140fmt.Print(s2)fmt.Printf(\"%T\\n\", s2) // strings3 := fmt.Sprintf(\"%t\", b1) // 3.140fmt.Print(s3)fmt.Printf(\"%T\\n\", s3) // string// 使用strconv库函数进行转换fmt.Println(strconv.FormatFloat(f1, \'E\', 3, 64))}
(9) string类型转成基本类型

使用strconv包下的Parse开头的函数

当不满足转换条件时,比如PrarseInt(\"dasd\",10,32)时“dasd无法转换,函数返回Int默认值和报错(err)

package mainimport (\"fmt\"\"strconv\")func main() {// 字符串转成其他类型s1 := string(\'t\')s2 := \"-10203\"s3 := \"0921\"s4 := \"0.293\"fmt.Println(strconv.ParseBool(s1))fmt.Println(strconv.ParseInt(string(s2), 10, 32))fmt.Println(strconv.ParseUint(s3, 10, 16))fmt.Println(strconv.ParseFloat(s4, 32))}
(10) 指针

表示内存地址

package mainimport \"fmt\"func main() {// 指针使用var a int = 10// &变量名 就能获取变量的内存地址fmt.Println(&a) //0xc000096068 表示a变量的地址,程序每次运行时分配的地址可能不同//指针类型就是 在基本类型前加*f := 3.1001var b *float64 = &ffmt.Println(b) // 0xc00000a0f0表示b值,也就是f的地址fmt.Println(*b) // 3.1001代表b指向的内存地址所存储的值*b = 3.01fmt.Println(f) // 通过指针改变地址的值 3.01fmt.Println(&b) // 0xc000068030表示存储地址b的地址}
(11) 标识符

1.中文也能作为标识符,但是不建议使用

2.下划线\"_\"在Go语言里十个特殊的标识符,称为空标识符,可以作为占位符忽略某些值(某些返回值或者其后的值)

3. go文件中首字母大小是对变量或者函数的安全限制,当首字母大写时,该包被导入后该标识符能够被访问,当首字母小写时,该包即使被导入近也不能被访问

4.进行导包时需要用双引号扩起来,推荐使用go mod init方式构建的项目进行导包

1) 创建项目目录,进入项目目录下,构建一个main.go文件,和一个utils文件,进入项目 根目录下命令行执行:go mod init myProject,生成go.mod文件

2)在utils目录下编辑可以创建多个go文件,但是go文件的包名原则上需要与utils目录名同名,且一个目录下不允许有多个不同的包名

3)在main中就能直接导入包,导入该目录下的所有的标识符首字母大写的变量或者函数

2.2 运算符

(1) 算数运算符

算数运算符包括:+ 、- 、* 、/ 、%、 ++ 、--,同java类似,但是这里的++只能单独使用,在变量的前面,不能运算赋值

package mainimport \"fmt\"func main() {var num1 int = 10var num2 int = 3num3 := num1 + num2 // num3 : 13num4 := num1 - num2 // num4 : 7num5 := num1 * num2 // num5 : 30num6 := num1 / num2 //num6 : 3num7 := num1 % num2 //num7 : 1num1++  //num1 : 11num2--  //num2 : 2fmt.Println(\"num3 :\", num3)fmt.Println(\"num4 :\", num4)fmt.Println(\"num5 :\", num5)fmt.Println(\"num6 :\", num6)fmt.Println(\"num7 :\", num7)fmt.Println(\"num1 :\", num1)fmt.Println(\"num2 :\", num2)}
(2) 赋值运算符

赋值运算符包括:= 、 += 、-= 、*= 、/= 、%=

package mainimport \"fmt\"func main() {// 赋值运算符a := 10fmt.Println(\"a=\", a) // 10a += 2fmt.Println(\"a=\", a) // 12a -= 3fmt.Println(\"a=\", a) // 9a *= 6fmt.Println(\"a=\", a) // 54a /= 3fmt.Println(\"a=\", a) // 18a %= 5fmt.Println(\"a=\", a) // 3}
(3) 关系运算符

关系运算符包含:== 、 != 、 > 、= 、 <=,常用于流程控制

package mainimport \"fmt\"func main() {// 关系运算符fmt.Println(\"2>3 : \", 2 > 3)fmt.Println(\"2<3 : \", 2 =3 : \", 2 >= 3)fmt.Println(\"2<=3 : \", 2 <= 3)}
(4) 逻辑运算符

逻辑运算符包含: && (短路与、且)、||(或)、!(非)

短路与:只要有一个是false就都是false后续不需要再判断了

或:只要有一个是true后续不需要判断表达式为true

package mainimport \"fmt\"func main() {fmt.Println(\"true && true :\", true && true) // truefmt.Println(\"true && false :\", true && false) //falsefmt.Println(\"false && false :\", false && false) //falsefmt.Println(\"true || true :\", true || true) //truefmt.Println(\"false || true :\", false || true) //truefmt.Println(\"false || false :\", false || false) //falsefmt.Println(\"!false  :\", !false) //truefmt.Println(\"!true :\", !true) //false}
(5) 位运算符

位运算符:& 、| 、 ^ 、 <>

&:按位与:两位都为1,结果为1,否则为0

|:按位或:只要有一位为1,结果就为1

^:按位异或:两对应的二进位相异时,结果为1

<<:左移动运算符:运算数的各二进位全部左移若干位,由 << 右边的数字指定了移动的位数,高位丢弃,低位补0。

>>:右移动运算符:把\">>\"左边的运算数的各二进位全部右移若干位,>> 右边的数字指定了移动的位数

a = 60 # 二进制位 0011 1100b = 13 # 二进制位 0000 1101\'\'\'a&b 按位与:两位都为1,结果为1,否则为0a 0011 1100b 0000 1101二进制结果 0000 1100十进制结果 12\'\'\'fmt.Println(a & b)\'\'\'a|b 按位或:只要有一位为1,结果就为1a 0011 1100b 0000 1101二进制结果 0011 1101十进制结果 61\'\'\'fmt.Println(a | b)\'\'\'a^b 按位异或:两对应的二进位相异时,结果为1 a 0011 1100b 0000 1101二进制结果 0011 0001十进制结果 49\'\'\'fmt.Println(a ^ b)\'\'\'<< 左移动运算符:运算数的各二进位全部左移若干位,由 << 右边的数字指定了移动的位数,高位丢弃,低位补0a 0011 1100 a << 2  1111 0000十进制结果 240 \'\'\'fmt.Println(a <> 右移动运算符:把\">>\"左边的运算数的各二进位全部右移若干位,>> 右边的数字指定了移动的位数a 0011 1100 a >> 2 0000 1111十进制结果 15 \'\'\'fmt.Println(a >> 2)
(6) 键盘录入Scanf

可查看fmt包下的相关介绍。

package mainimport \"fmt\"func main() {// 键盘录入值var age intvar name stringfmt.Println(\"请录入学生的年龄:\")// 将age的地址传递给该函数,函数扫描输入传递给该地址num1, _ := fmt.Scanln(&age) // 录入数据式类型应该匹配,底层会自动判别fmt.Println(\"获取到的输入:\", num1)fmt.Printf(\"输入的类型:%T\\n\", age)fmt.Println(\"输入的值:\", age)fmt.Println(\"请录入学生的姓名:\")// 将age的地址传递给该函数,函数扫描输入传递给该地址num2, _ := fmt.Scanln(&name)fmt.Println(\"获取到的输入:\", num2)fmt.Printf(\"输入的类型:%T\\n\", name)fmt.Println(\"输入的值:\", name)// 方式2:var age2 intvar name2 stringvar score float32var isVip boolfmt.Println(\"请按照 “年龄-姓名-成绩-是否是VIP” 输入学生 年龄、姓名、成绩以及是否是VIP!\")fmt.Scanf(\"%d %s %f %t\", &age2, &name2, &score, &isVip)fmt.Println(\"输入学生信息为:年龄-\", age2, \";姓名-\", name2, \";成绩-\", score, \";是否是VIP-\", isVip)}

3.流程控制与函数

3.1流程控制

(1) 分支结构
package mainimport \"fmt\"func main() {// 单分支结构// if (3 > 2) { go语言建议不写括号,简洁// fmt.Println(\"3>2\")// }if 3 > 2 {fmt.Println(\"3大于2\")}//双分支if 5 == 6 {fmt.Println(\"5等于6\")} else {fmt.Println(\"5不等于6\")}//多分支if 10 < 5 {fmt.Println(\"10小于5\")} else if 10 < 7 {fmt.Println(\"10小于7\")} else if 10 < 10 {fmt.Println(\"10小于10\")} else if 10 < 11 {fmt.Println(\"10小于11\")} else {fmt.Println(\"10很大\")}//switch 多分支a := 0001switch a { // 这里a可以是表达式比如 a++,或者有返回值的函数case 0001: // case后值得类型必须和表达式得值类型一样,也可以是表达式,而且可以包含多个表达式fmt.Println(\"第一位为零\")// break case后不再需要带breakcase 0010:fmt.Println(\"第二位为零\")case 0100:fmt.Println(\"第三位为零\")fallthrough //表示穿透,需要继续进行下一个casecase 1000:fmt.Println(\"第四位为零\")default:fmt.Println(\"全为零\")break}}
(2) 循环结构
package mainimport (\"fmt\")func main() {// 循环结构for i := 0; i  0 {j -= 10fmt.Print(\"\\t\", j)}fmt.Print(\"\\n\")/*for true {死循环}*/// 键值循环lists := []int{1, 2, 3, 4}for index, value := range lists {fmt.Printf(\"下标%d的元素为:%d\\n\", index, value)}s := \"我是中国人\"// 使用普通for循环// for i := 0; i < len(s); i++ {// fmt.Printf(\"%c\\t\", s[i])// }// 上述进行遍历时是按照字节一个字节一个字节进行遍历,// 当出现中文时,unicode编码占用四个字节就会出现乱码for _, c := range s {fmt.Printf(\"%c\\t\", c)// fmt.Print(unsafe.Sizeof(c))}// for range lists { //可以直接进行循环// fmt.Print(\"元素\\t\")// }/*0 1 2 3 4 5 6 7 8 9 90 80 70 60 50 40 30 20 10 0下标0的元素为:1下标1的元素为:2下标2的元素为:3下标3的元素为:4我 是 中 国 人*/}
(3) 控制流程 关键字
  • break: 跳出本层循环,

  • continue : 本轮循环碰到continue结束本次循环,直接跳到下次循环

  • goto:直接跳到指定行执行,不建议使用

  • return: 直接跳出当前函数

3.2函数

函数的定义语法,函数不支持重载

func functionName(parameter1 type1,parameter2 type2...) returnType{// 函数逻辑return returnType}
  • 参数:可以是值传递也可以是引用传递。另外go语言也支持可变参数传递

  • 返回值:返回值可以是单个的,也可以是多返回值

  • 一等公民:go语言中的函数作为一等公民可以被当作变量一样被传递,被赋值、被当作参数传递给函数、被当作返回值返回

  • 函数名:函数名首字母不能是数字,若是大写字母可以被外部包引用,若是小写则是私有的不能被外部引用

函数作用:提高代码的复用性

package mainimport \"fmt\"func main() {// 求和函数a := 10b := 19fmt.Println(sum(a, b))fmt.Println(\"===========\") //交换两数需要传递引用类型n1 := 10n2 := 80fmt.Printf(\"调用函数前 a:%d \\t b:%d \\n\", n1, n2)exchange(&n1, &n2)fmt.Printf(\"调用函数后 a:%d \\t b:%d \\n\", n1, n2)fmt.Println(\"===========\") l, s := multSum(1, 2, 4, 5, 6)fmt.Println(\"共有:\", l, \"个数;\\t 总和:\", s)myfunc(\"占山\", \"为王\", 12, 22, \"李四\")fmt.Println(\"===========\") fun := myfuncfmt.Printf(\"fun对应的类型%T,以及函数对应的类型:%T\\n\", fun, myfunc)reFunc := feedBack(100, callFunc)reFunc() fmt.Println(\"===========\") // 起别名type myInt intvar x1 myInt = 10var x2 int = 10// fmt.Println(\"x1==x2?\",x1==x2)fmt.Println(\"x1==x2?\", x1 == myInt(x2))type myfunction func(string, ...interface{})var myfunct myfunction = callFuncfeedBack(100, myfunct)()}// 对返回值直接命名func sum(x int, y int) (sum int) {sum = x + yreturn}func exchange(num1 *int, num2 *int) {temp := *num1*num1 = *num2*num2 = temp}func multSum(Samples ...int) (int, int) {sum := 0l := 0for k, value := range Samples {l++sum += valuefmt.Printf(\"第%d个元素值:%d\\n\", k, value)}return l, sum}// 任意参数的传递func myfunc(args ...interface{}) {for key, value := range args {fmt.Println(\"======\", key, \"\\t\", value, \"=====\")}}// 一等公民func feedBack(inNum int, call func(string, ...interface{})) func() { // 返回一个匿名函数return func() {outNum := float32(inNum + 10)call(\"传递输入值:%f,类型:%T\", outNum, outNum)}}func callFunc(formatS string, li ...interface{}) {fmt.Printf(formatS, li[0], li[1])}

封装函数集合时,导包注意事项:

  • go文件的package声明原则上需要和go文件所在目录同名

  • 引入包的过程:

    1.先创建项目 go mod init moduleName

    2.import “包所在的目录/包”

  • 一个目录下不能有重复的函数名

  • 一个目录下的go文件归属一个包

  • 包的底层:

    1.在程序层面,所有报名相同的源文件组成的代码块

    2.在源文件层面 指的是文件夹

  • 导入的包在使用时可通过别名进行使用

  • init函数:程序执行前的初始化函数,执行顺序:

    初始化导入的包(包括初始化包里面的变量函数)-----》初始化包作用域变量-----》init函数------》main函数

package mainimport (u \"demo02/utils\"f \"fmt\")var T1 func() = myFuncvar T2 int = myFunc2()func myFunc() {f.Println(\"函数执行了\")}func myFunc2() int {f.Println(\"函数执行了,返回10\")return 10}// 该函数会在所有包执行开始前被调用,通常用于注册程序所需要的依赖。如mysql注册和配置文件加载func init() {f.Println(\"init函数被执行了!\")}func main() {f.Println(\"main函数被执行了!\")u.GetInfo()// 匿名函数,被定义时就被调用sum := func(a int, b int) int {return a * b}f.Println(sum(99, 9))func() { // 形参是空的,返回值也是空f.Println(\"匿名函数执行===\")}() //实参也是空}

 闭包:匿名函数+外部数据

package mainimport \"fmt\"/*闭包产生的条件:1.在函数A直接或者间接返回一个函数B2.B函数内部使用着A函数的私有变量3.A函数要被调用且由变量接收着B形成闭包闭包的优点:1.延长了变量的声明周期,闭包允许函数捕获外部作用域的变量,形成一个封闭的环境,函数执行空间不销毁,变量也不会销毁2.保护私有变量,通过闭包我们可以访问函数的私有变量,同时保证函数的私有变量不会被外界访问3.延迟执行,闭包可以用于延迟执行一些操作,使其在某一个特定时刻执行。需要函数执行后再执行某些操作时非常有用4.闭包可以用作回调函数,将特定的行为传递给其他函数。闭包的缺点:1.资源泄露,闭包捕获大量外部资源时,或者长时间不释放,会造成内存泄漏等问题2.性能损耗,闭包的外部资源可能需要在内存堆上进行分配,因此会导致内存分配和垃圾回收等性能消耗3.代码可读性,使用复杂的闭包可能降低代码的可读性*/func getMethod() func(int) int {// 闭包所需的外部资源或者称作环境sum := 0// 闭包所需要的函数return func(i int) int {// 对外部资源进行处理管理sum += ifmt.Println(\"sum:\", sum)return sum}}func main() {// 调用getMethod函数,将返回值赋值给f,随后结束getMethod()方法// 但是返回值中包含sum局部变量,而被封装至匿名函数中f := getMethod()f(1) // 1f(2) // 3// 重新返回一个匿名函数并附带所需要的外部资源f1 := getMethod()f1(1) //1f1(2) //3}

defer关键字

声明延迟函数,能够在创建资源之后及时释放资源,会将defer声明的语句或者函数放到当前函数的栈中。

当程序执行一个函数时,会将函数的上下文(输入参数、输出参数、返回值等)作为栈帧放在程序内存的栈中,当函数执行完之后,设置返回值,然后返回调用方,此时栈帧已经退出栈,函数才算真正的执行完成。或者异常中断之后。而defer声明过的语句或者代码片段会在return 语句之后或者函数结束处执行,遵循先声明后执行的原则。另外带有return语句的函数,defer需要声明在return语句之前。

package mainimport \"fmt\"func test1() {fmt.Println(\"执行6==\")defer fmt.Println(\"执行4==\")return}// defer函数的传递参数在被定义时就已经明确,无论传入的时变量还是语句还是函数,在声明时就被计算确定,// 随后头插法将该_defer结构体(函数栈帧)插入链表,在最后结束时再从链表头取出延迟结构体执行。func test2() (i int) {i = 0defer fmt.Println(\"i:\", i) // 被计入defer表时是0,取出表时仍然是0i++return}func main() {//defer 关键字defer fmt.Println(\"执行1==\")defer fmt.Println(\"执行2==\")test1()defer fmt.Println(\"执行3==\")defer fmt.Println(test2())}

常用的系统函数:

统计字符串的长度:len(str)

字符串的遍历函数:r := []rune(str) rune是int32的别名

字符串转整数:num,err := strconv.Atoi(\"1234\")

整数转字符串:str := strconv.Itoa(1234)

查找子串是否在字符串中: b := strings.contains(\"golang\",\"go\")

统计字符串有几个子串:n = strings.Count(\"golang\",\"g\")

不区分大小写的字符串比较: b := strings.EqualFold(\"g\",\"G\")

返回子串在字符串第一次出现的索引值,没有返回-1:strings.Index(\"golang\",\"an\")

替换字符串的某些子串:strings.Replace(\"golanggood\",\"go\",\"do\",n)

切割字符串:strings.Split(\"go-go-goo\",\"-\") 返回切割数组

字符串大小写切换:strings.ToLower() strings.ToUpper()

字符串去除左右两边的空格:strings.TrimSpace()

获取当前时间:time.Now() 返回Time结构体,Time结构体包含一些方法可以用于获取具体的成员变量

builtin包下有很多内置的函数 :

package mainimport (\"fmt\"\"strconv\"\"strings\"\"time\"\"unsafe\")func main() {// 常用的字符串命令s1 := \"zhangsan\"s2 := \"中国二年\"s3 := []byte{}fmt.Println(len(s1))  //8 英文采用ASCLL编码 每个字母占用一个字节fmt.Println(len(s2))  //12 中文每个字符占用三字节fmt.Println(unsafe.Sizeof(s2)) //16 这里由两个值决定了16,8字节的地址和8字节的长度fmt.Println(unsafe.Sizeof(s3)) //24 这里由于切片的数据结构决定,首元素地址、切片长度、切片容量fmt.Println([]rune(s1))  // 对字符串进行遍历返回切片fmt.Println(strconv.Atoi(\"123\")) // 将字符串装成整数fmt.Println(strconv.Itoa(123)) // 将数字转成字符串fmt.Println(strings.Contains(\"golang\", \"go\"))  // 查找字符串是否包含子串fmt.Println(strings.Count(\"anbdhskjdfhj\", \"j\")) // 查找字符串包含几个子串fmt.Println(strings.EqualFold(\"Go\", \"gO\"))  //不区分大小比较字符串是否相等fmt.Println(strings.Index(\"asdfghjkl\", \"df\"))  //返回子串第一次出现的下标fmt.Println(strings.Replace(\"golanggood\", \"go\", \"do\", -1)) //替换字符串中的某些字串fmt.Println(strings.Split(\"go-go-do-good\", \"-\"))  //切割某字符串返回数组fmt.Println(strings.ToLower(\"HJjkk\"))// 字符串转小写fmt.Println(strings.ToUpper(\"HJjkk\"))//字符串转大写fmt.Println(strings.TrimSpace(\" hdghkd \"))  //去除左右两边的字符空格fmt.Println(time.Now())  //获取当前时间,返回Time结构体fmt.Println(\"======================\")// 特别注意rune,作为int32的别名使用。常用统计包含中文字符的字符串长度和字符串的截取s4 := \"hhhh中国\"fmt.Println(len(s4))  //10 占用10个字节fmt.Println(len([]rune(s4))) //6 总共6个字符fmt.Println(string([]rune(s4)[2:5])) //hh中 截取下标2、3、4的字符fmt.Println(\"======================\")// builtin包的内置函数// new用于分配内存使用,主要用于分配值类型(int系列、float系列、bool、string、数组和结构体)ptr := new(int)*ptr = 10fmt.Println(*ptr)fmt.Printf(\"ptr类型:%T,ptr的值:%v,关联的值是:%d\\n\", ptr, ptr, *ptr)// make用于分配内存使用,常用于切片、map和管道的分配ptr1 := make([]int, 9, 10)ptr1[0] = 100fmt.Println(ptr1)}

错误处理:不会轻易的结束程序运行

错误处理/错误捕获机制:使用defer+recover处理

// 使用panic抛出一个异常func triggerPanic() { panic(\"a problem occurred\")} // 使用recover捕获并处理异常func recoverFromPanic() { defer func() { if r := recover(); r != nil { fmt.Println(\"Recovered from a panic\", r) } }() triggerPanic()}

 自定义错误:使用errors报下的new函数自定义错误

// 函数可能会返回错误func mightFail() error { // 如果发生错误,返回一个错误信息 if someCondition { return errors.New(\"some error message\") } return nil // 表示没有错误发生} // 在函数调用中检查错误if err := mightFail(); err != nil { fmt.Println(\"Error:\", err)}

4.数组、切片和Map

4.1数组

package mainimport (\"fmt\")func main() {// 数组定义/*var 数组名 [数组大小]数据类型数据名 := [数组大小]数据类型{num1,num1……}var 数组名 = [数组大小]数组类型{num1,num2,num3,……}*/var n1 = [3]int{1, 2, 3}var n2 = [...]int{1, 2, 3, 4, 5}var n3 = [...]int{2: 4, 3: 9} // 下标2的值为4,下标3的值为9,未定义的值默认0fmt.Println(n1)fmt.Println(n2)fmt.Println(n3)nums1 := [5]int8{1, 2, 3, 4, 5}for i := 0; i < len(nums1); i++ {fmt.Print(nums1[i], \"\\t\")}fmt.Print(\"\\n\")test(nums1) //nums1的值并未改变,说明调用传递给函数的的参数是值 不是引用for _, value := range nums1 {fmt.Print(value, \"\\t\")}fmt.Print(\"\\n\")test2(&nums1) //nums1的值并改变,说明调用传递给函数的的参数是引用for _, value := range nums1 {fmt.Print(value, \"\\t\")}fmt.Print(\"\\n\")fmt.Println(nums1)fmt.Printf(\"数组的类型:%T\\n\", nums1) //长度也属于数组的类型的一部分fmt.Printf(\"数组的地址:%p\\n\", &nums1)fmt.Printf(\"数组首元素的地址:%p\\n\", &nums1[0])fmt.Printf(\"数组首元素的地址:%p\\n\", &nums1[1])// 二维数组var nums2 [5][6]intfor i := 0; i < len(nums2); i++ {nums2[i] = [6]int{1, 2, 3, 4, 5, 6}}fmt.Println(nums2)//二维数组的遍历su := 0for _, v := range nums2 {for _, t := range v {su++fmt.Print(t, \"\\t\")}}fmt.Print(\"\\n\")fmt.Print(su, \"\\n\")}func test(arr [5]int8) {arr[0] = 10}func test2(arr *[5]int8) {arr[0] = 10}

 4.2切片

对数组连续片段的引用

package mainimport \"fmt\"func main() {// 定义一个数组nums1 := [5]int{1, 2, 3, 4, 5}// 切片sp1 := nums1[2:4]var sp2 []int = []int{1, 2, 3}fmt.Printf(\"sp1的数据类型:%T\\n\", sp1)fmt.Printf(\"sp2的数据类型:%T\\n\", sp2)fmt.Printf(\"sp1的数据:%d\\n\", sp1)fmt.Printf(\"sp1的数据长度:%d\\n\", len(sp1))fmt.Printf(\"sp1的数据容量:%d\\n\", cap(sp1))/*切片结构体:1.切片首元素地址2.切片长度3.切片容量*/fmt.Printf(\"sp1的数据地址:%p\\n\", sp1) //切片属于引用类型 可以直接使用%p进行取地址fmt.Printf(\"nums1的数据地址:%p\\n\", &nums1) // 数组属于值类型,需要使用&去取地址// 改变切片sp1的值,nums1的值也改变sp1[1] = 10fmt.Printf(\"nums1的数据:%d\\n\", nums1)// 使用make()进行切片创建,使用append()函数对切片末尾添加元素sp3 := make([]int8, 0, 10)sp3 = append(sp3, 8, 9)sp3[1] = 10fmt.Printf(\"切片sp3的类型:%T\\n\", sp3)fmt.Printf(\"切片sp3数据:%v\\n\", sp3)// 切片遍历同数组遍历一样for i := 0; i < len(sp1); i++ {fmt.Printf(\"第 %d 个元素值:%d\\n\", i+1, sp1[i])}for index, value := range sp1 {fmt.Printf(\"第 %d 个元素值:%d\\n\", index+1, value)}}

注意:使用for range进行遍历时,其value值只是对元素的复制,因此不能修改值。

 4.3映射(Map)

包含key-value键值对的集合,且key-value是无须的

key通常为int、string等基本类型,指针也可以作为key,另外接口、数组也可以作为key,只要是可比较的且不能改变的都能作为key

注意:只声明了map,但是内存中没有分配实际地址,因此不能使用

package mainimport \"fmt\"func main() {// 映射(map)// var map1 map[int]string // 这里只声明了map,但是内存中没有分配实际地址,因此不能使用var map1 map[int]string = map[int]string{} // 需要进行申请map2 := map[int]int{1: 2}  //方式1map3 := make(map[string]int)  //方式2map4 := map[[2]int]string{  //方式3{1, 2}: \"张三\",{2, 3}: \"李四\",}fmt.Println(\"map4值:\", map4)fmt.Println(\"map3值:\", map3)fmt.Println(\"map2值:\", map2)// 新增键值对map1[1] = \"张三\" // 新增键值对1:\"张三\"// 删除键值对delete(map2, 1)// 清空map3 = make(map[string]int) // 直接清空// 访问键值对value, ok := map1[1]if ok {fmt.Println(\"map1[2]的值:\", value)} else {fmt.Println(\"不存在map1[2]的值\")}fmt.Println(\"map1[2]的值:\", map1[2]) // 当不存在时使用0作为默认值fmt.Println(\"map4长度:\", len(map4))// 对数组进行遍历s := \"kjasdghkiqwe\"fmt.Println(getCounts(s))}func getCounts(s string) map[string]int {res := make(map[string]int, 0)for _, char := range s {res[string(char)]++}return res}

 5.Golang的“面向对象”

go语言使用结构体(struct)和接口(interface)作为对象,因此go语言基于结构体和接口实现面向对象,go语言不是面向对象语言,也不是面向过程语言,只是支持面向对象。

目的:易于数据的管理和维护,代码简洁性。

5.1结构体

结构体定义与使用:

package mainimport (\"demo01/model\"\"fmt\")// 定义一个结构体type Address struct {city string}type Student struct {name stringage intaddr Address}// 带有引用类型的声明都是传递地址,不带引用类型都是值传递func (s Student) test1() {fmt.Println(\"学生测试一===\")}func (s Student) test2() {s.name = s.name + \"学生\"}func (s *Student) test3() {s.addr.city = s.addr.city + \"城市\"}func test4(s Student) {s.addr.city = s.addr.city + \"城市\"s.age += 5}func main() {// 实例化一个结构体t1 := Student{\"张三\", 18, Address{\"安徽合肥\"}}fmt.Println(\"0t1: \", t1)t1.test1()fmt.Println(\"1t1: \", t1)t1.test2()fmt.Println(\"2t1: \", t1)t1.test3() //底层实现 (*t1).test3()fmt.Println(\"3t1: \", t1)test4(t1)fmt.Println(\"4t1: \", t1)t1.name = \"李四\"fmt.Println(\"5t1: \", t1)var t2 *Student = new(Student)(*t2).name = \"王五\"// go语言提供了简化的复制方式,底层自动把t2转换成(*t2)t2.age = 26t2.addr.city = \"太原市\"fmt.Println(\"0t2: \", t2)fmt.Println(\"0t2: \", *t2)// 使用外部包导入teacher1 := model.NewTeacher(\"张三\", 68)teacher1.Show()// fmt.Println(&teacher1)}

 model下的包:

package modelimport \"fmt\"/*封装的实现:某些变量采用首字母采用小写(private),某些变量首字母采用大写(public)*//* 使用方法实现 工厂模式,类似构造函数*/type teacher struct {name stringage int}// 注意这里返回的是引用,teacher的引用而不是值类型,只有返回引用类型才能算是工厂模式func NewTeacher(name string, age int) *teacher {// return &teacher{// name,// age,// }c 这里会有赋值顺序的限制return &teacher{age: age,name: name,}}// 这里的Show首字母必须大写,否则外界无法使用func (t *teacher) Show() {fmt.Println(\"当前实列数据:\", t)}// 当结构体实现String()方法时,Println时会自动调用String方法func (t teacher) String() string {return fmt.Sprintf(\"老师的信息包括 姓名:%v; 年龄:%v\", t.name, t.age)}

 继承注意事项:

显示嵌入:外部实列dog需要通过dog.A来访问结构体animal的变量,如果animal结构体内的变量首字母是大写,则在由于外部dog实列能访问到A,也就是能访问到A的对外可见变量(首字母大写),如果A改成a则,外部dog实列不管animal中的变量首字母是否大写都不能访问到animal内嵌结构体,这里与声明animal的大小写无关。

type dog struct { A animal}

 匿名嵌入:这种嵌入方式更像继承,animal所有的变量都将是Cat的一部分,外部Cat实例可以直接访问animal的可见变量

type Cat struct { animal}

另外。go语言支持多继承,但是可能导致代码混乱,当出现不同父类的包含相同命名的变量时,可使用 ‘父类.相同变量’ 区分。匿名嵌入的结构体还支持基本数据类型  

type A struct{ a int b string int32}type B struct{ c int b string int32}type C struct{ B A string}c := C{B{1,\"b\",64},A{12,\"张\",32},\"张三\"}fmt.Println(c.string, c.B.b, c.A.int32)

 5.2接口

目的:为了定义规范、定义规则或者定义某种能力

1.一个结构体可以实现多个接口

2.接口本身不能实例化,但是可以指向实现了接口的结构体实列

3.只要是自定义数据类型都可以实现接口

4.接口之间也可以继承,一个接口可以继承多个接口

5.接口是一个引用数据类型

举个列子:

                A接口: a,b方法

                B接口:a,b方法

C结构体实现了a,b方法,则可以说C结构体实现了A接口和实现了B结构,所有需要A,B接口能力规范或者 规则的函数,C都能够作为参数传入,执行A,B所具备的方法。

package mainimport (\"demo02/model\"\"fmt\")type A interface {a()}type B interface {a()}type Ainterface interface {ABAinter()}type s struct{}func (s s) a() {fmt.Println(\"实现了a接口的a函数\")}func (s s) Ainter() {fmt.Println(\"实现了Ainter接口的Ainter函数\")}func main() {// 使用工厂创建类Af := model.AnimalFactory{}A1 := Af.CreateObject(\"王五\", 29)A1.AnimalRun()D1 := model.DogFactory{}.CreateObject(\"李四\", 10)D1.A.AnimalRun() // 由于dog结构体采用显示嵌入,需要通过A来访问animal的实现接口。// fmt.Println(D1)Cf := model.CatFactory{}C1 := Cf.CreateObject(\"张三\", 10)// 接口可以指向实现了接口的结构体实列,但是自身不能实列化var r model.Run = C1r.AnimalRun()sport(C1)fmt.Printf(\"c1的类型:%T\", *C1)// 接口的继承var i Ainterface = new(s)i.a()i.Ainter()var b B = new(s)b.a()var a A = new(s)a.a()// 多态数组,可以定义一个空接口,通过空接口可以实现多种数据类型的融合数组rs := [3]model.Run{}rs[0] = *model.AnimalFactory{}.CreateObject(\"动物\", 0)rs[1] = *model.CatFactory{}.CreateObject(\"狗\", 1)fmt.Println(rs) var mult []interface{} = []interface{}{\"zhangsan\", 10, \'c\'}fmt.Println(mult)}// 函数用于接收具备特定能力(实现了接口的结构体)的变量r参数,传入的参数是实现了接口的结构体(体现出多态)// 多态参数func sport(r model.Run) {r.AnimalRun()}

model包:

package modelimport \"fmt\"type Run interface {AnimalRun()}type Factory interface {CreateObject(name string, age int) *animal}type animal struct {name stringage int}/*以下是给结构体起别名 不同于继承type dog animaltype cat animal*/type dog struct {A animal}type cat struct {animal}// 实现接口所有的方法func (a animal) AnimalRun() {fmt.Printf(\"动物%v在跑:\\n\", a.name)}// 定义一个狗的实现接口工厂type DogFactory struct{}func (d DogFactory) CreateObject(name string, age int) *dog {return &dog{animal{name,age,},}}type CatFactory struct{}func (c CatFactory) CreateObject(name string, age int) *cat {return &cat{animal{name,age,},}}type AnimalFactory struct{}func (a AnimalFactory) CreateObject(name string, age int) *animal {return &animal{name,age,}}

 5.3断言

目的:用于判断某个接口实列是否是是它的某一个类型 value,ok := element.(T)

语法含义:当element(变量)是T(实现了element接口的结构体)类型时,将转换成T类型并赋值给value,并把ok赋值成true

package mainimport (\"fmt\")type i interface {iF()}type c struct {name string}func (c c) l() {fmt.Println(\"结构体c独有的函数\")}type d struct {name string}func (d d) p() {fmt.Println(\"结构体d独有的函数\")}func (d d) iF() {fmt.Println(\"d结构体实现了i接口\")}func (c c) iF() {fmt.Println(\"c结构体实现了i接口\")}func doIt(i i) {i.iF()v, ok := i.(c)if ok {v.l()}}func main() {// 判断接口实列是否是某一个类型c1 := c{\"展示噶\"}var r i = c1 // 将c1向上转型为接口实列v, ok := r.(c) //判断接口实列是否是d类型,如果是的化就将接口实列转换成d的实列if ok {fmt.Printf(\"r是c类型,值为:%v\\n\", v)fmt.Printf(\"v的类型:%T\\n\", v)} else {fmt.Print(\"r并不是c结构体的实列\")}var s interface{} = \"结婚三大件哈桑\"_, isString := s.(string)fmt.Println(\"是否是字符串:\", isString)// 使用switch进行判别// type是go语言的关键字,switch type固定用法switch r.(type) {case c:c1 := r.(c)c1.l()case d:d1 := r.(d)d1.p()}}

注意:数组和结构体都是值类型,而切片、管道、指针、映射、接口和函数都是引用类型  

package mainimport \"fmt\"func Qie(a []int) {a[0] = 10}func St(s test) {s.x = 10}type test struct {x inty int}func main() {b := []int{1, 2, 3}fmt.Println(b)Qie(b)fmt.Println(b)t := test{1, 2}fmt.Println(t)St(t)fmt.Println(t)}

6.文件操作

文件操作,os包下的type File结构体,

一般使用os包下的Open函数打开只读文件,当要写入文件时可采用os包下的OpenFile函数,os报下的File结构体,实现了io包下的一些函数接口,file结构体本身也具有一些函数,Read函数和Write函数,另外bufio包下还有一些关于利用缓存进行读写的操作。

package mainimport (\"bufio\"\"fmt\"\"io\"\"os\"\"syscall\")func main() {//os// 使用os包进行读写// 使用Open打开的文件一般只用于读取,需要写入时还需要OpenFilef, err := os.Open(\"./test.txt\")if err != nil {fmt.Print(\"打开文件失败:\", err)}readData := make([]byte, 24, 24)n, reErr := f.Read(readData)if reErr != nil {fmt.Print(\"读取文件失败:\", err)}fmt.Printf(\"总共读取%d个字节数据:%q\\n\", n, readData)f.Close()// io/ioutil// 使用os包ReadFile进行读写,不需要Open再Close// re, err := ioutil.ReadFile(\"./test.txt\") //ioutil已经被弃用re, _ := os.ReadFile(\"./test.txt\")fmt.Printf(\"总共读取%d个字节数据:%q\\n\", len(re), re)fmt.Printf(\"总共读取%d个字节数据:%q\\n\", len(re), string(re))// bufio// 当文件较大时,可以采用带缓冲的,使用bufio,带缓冲的io留进行读写bufOut := []string{}readerIn, _ := os.Open(\"./test2.txt\")defer readerIn.Close()bfRead := bufio.NewReader(readerIn)// 当系统无法读入更多内容时候,会返回io.EOFfor line, _, errBuf := bfRead.ReadLine(); errBuf != io.EOF; line, _, errBuf = bfRead.ReadLine() {fmt.Printf(\"line的类型是%T,内容是:%s\\n\", line, string(line))bufOut = append(bufOut, string(line))}// os包//使用os包下的OpenFile打开一个用于写入的文件,再使用io流进行写入,这里的后面参数可以参考指导文献wF, errF := os.OpenFile(\"./test3.txt\", syscall.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)if errF != nil {fmt.Println(errF)}defer wF.Close()// 直接写入// fmt.Println(bufOut)// for _, value := range bufOut {// wF.WriteString(\"\\n\")// wF.WriteString(value)// }wbF := bufio.NewWriter(wF)fmt.Println(bufOut)for _, value := range bufOut {wbF.WriteString(\"\\n\")wbF.WriteString(value)// wbF.Flush() // 将缓冲区的内容写入到底层的io.Writer,刷新缓冲流之后才算写成功}wbF.Flush() // 将缓冲区的内容写入到底层的io.Writer,刷新缓冲流之后才算写成功}

7.协程和管道

7.1前置概念

  • CPU核心:计算机中央处理的一个单元,能够执行计算机程序指令,CPU核心包括: 算数逻辑单元(负责执行所有的算数逻辑运算)、寄存器(数据在CPU的临时中转站)、控制单元(负责协调控制指令执行过程)、缓存(提供寄存器和内存之间临时存储,减少内存访问次数)

  • 单核CPU:单个核心的中央处理器,只能进行并发。

  • 多核CPU:多个核心的CPU,并行处理的关键。

  • 并发:同一时间段内,多个任务在轮流使用单个计算资源(通常指单核CPU),宏观上同时执行,微观上顺序执行

  • 并行:一组程序或者任务按照独立异步的速度执行,多个任务有多个计算资源承担(多核CPU或者多CPU),宏观同时执行,微观上也是同时执行。


  • 程序:为了完成某些任务,使用机器语言编写的一组代码的集合,程序是静态的。

  • 进程:进程是指正在运行的程序,进程是资源分配的基本单位,操作系统会为每个进程实时分配一些资源比如内存、CPU和磁盘读写的通道等。

  • 线程:线程是细分的进程,一个进程同一时间需要完成多个任务执行多段代码,每段代码就代表一个线程,另外说明该进程是支持多线程的。也是后端程序需要的能力(并行),线程是由操作系统进行调度的。

  • 内核态:内核态是操作系统中的一种特权状态或者运行模式,在内核态下,操作系统拥有最高权限和访问资源的能力,可以执行特权指令或者直接访问硬件资源。内核态下操作系统能够对资源进行分配、管理、释放和调度。

  • 用户态:用户态是操作系统中的一种较低特权级别的一种状态,这种状态下,操作系统不能使用特权指令,也不能进行CPU状态改变,只能访问自己的内存资源。不能直接访问一些硬件资源。当程序需要访问一些资源或者执行特权指令时,需要通过系统调用请求操作系统内核来完成这些操作。系统调用是一种特殊的函数调用,它会将应用程序从用户态切换至内核态,并把控制权交给操作系统内核,内核态下的操作系统会执行中断程序,并将执行的结果返回给应用程序,完成操作之后,内核会将控制权交还给应用程序,内核态切换成用户态。

  • 进程切换:进程切换包括进程调度和上下文切换,用于在不同进程之间进行CPU的调度和分配,当一个进程运行时,它占用了CPU和其他资源。当操作系统需要执行其他进程时,就会发生进程切换,进程切换需要保存当前进程的上下文信息和恢复调度构建下一个进程所需的上下文信息,上下文信息包括:CPU寄存器、程序计数器、内存地址映射表、进程控制块信息和栈指针等。

  • PCB:进程控制块,操作系统为了方便进程管理,为每个进程构建的数据结构。

  • 线程切换:为了减少进程切换时上下文信息量,将进程再切分成独立的任务就是线程进行调度,进程切换时只要需要切换内存寄存器,线程控制块信息等,不需要切换完整的进程地址映射。

  • 栈:线程独有的,每个线程被创建时都会获得一个独立的栈空间,用于存放局部变量、函数参数和返回地址等,栈具有先进后出的特点,主要用于函数调用时的上下文管理。每个线程的栈空间有固定大小,超过这个大小就会导致栈溢出。

  • 堆:堆是进程的所有线程共有的空间,通常用于动态内存分配,不同线程可以并发的访问堆,因此堆的访问需要同步控制。


  • 协程:协程是微线程,又称轻量级线程,其调度在用户态上运行,协程执行时能够保持当前的上下文(如局部变量和执行点),并在适当的时候挂起,稍后从挂起的地方继续执行。因为不涉及内核资源,协程切换时上下文信息切换的较少。协程占用的内存比线程少,因此相同的条件下能够运行更多的协程。

  • Go语言的协程调度方式:Go的协程调度是非抢占式调度,基于协作的调度模型,称为M:N调度(多个协程被多个OS线程管理)

7.2 Go语言的协程

package mainimport (\"fmt\"\"sync\"\"time\")func worker(n int, w *sync.WaitGroup) {defer w.Done()for i := 0; i < 3; i++ {time.Sleep(2 * time.Second)fmt.Printf(\"hello worker %d\\n\", n)}}func main() {// 协程创建时使用的是外部变量的引用for l := 0; l <= 5; l++ {// fmt.Printf(\"当前值:%d; 其地址:%p\\n\", l, &l)go func() {fmt.Printf(\"当前值:%d; 其地址:%p\\n\", l, &l)}()}// 最新版本中for循环进行了编译器优化,导致每次l都是不同的// for l := 0; l <= 10; l++ {// fmt.Printf(\"当前值==:%d; 其地址:%p\\n\", l, &l)// }// 但是在主程序中不存在这种// s := 10// fmt.Printf(\"当前值==:%d; 其地址:%p\\n\", s, &s)// s++// fmt.Printf(\"当前值==:%d; 其地址:%p\\n\", s, &s)// 使用sync.WaitGroup进行线程的同步,防止主线程执行完之后直接结束程序var w sync.WaitGroup// 主协程启用一个工作者w.Add(1)worker(0, &w)// 再同时创建五个工作者同时操作for j := 1; j < 4; j++ {w.Add(1)go worker(j, &w)}w.Wait()}

注意:

  • Go语言的协程分主次,主协程执行完之后,就将终止程序的执行,可使用管道或者sync包结构体来同步

  • sync中的WaitGroup方法相当于操作系统中的PV操作

  • 协程作用域:当协程使用外部变量时其使用的是外部变量的引用。与闭包一致

  • 1.22版本之后for循环每次迭代会产生新的迭代变量,防止共享错误的问题。(以前版本中当引用迭代变量并进行操作时可能会导致其无法满足循环条件而继续工作)

  • 当多个协程对共享资源进行访问时,会出现共享资源错乱的问题

package mainimport (\"fmt\"\"sync\"\"time\")var gobalNum int = 0func add(w *sync.WaitGroup) {defer w.Done()//for i := 0; i < 3; i++ {time.Sleep(1 * time.Second)fmt.Printf(\"Add函数第%d轮取到gobalNum的值为:%d;其地址为%p\\n\", i, gobalNum, &gobalNum)gobalNum = gobalNum + 1fmt.Printf(\"Add函数第%d轮运算得到gobalNum的值为:%d;其地址为%p\\n\", i, gobalNum, &gobalNum)}}func sub(w *sync.WaitGroup) {defer w.Done()for i := 0; i < 3; i++ {time.Sleep(1 * time.Second)fmt.Printf(\"Sub函数第%d轮取到gobalNum的值为:%d;其地址为%p\\n\", i, gobalNum, &gobalNum)gobalNum = gobalNum - 1fmt.Printf(\"Sub函数第%d轮运算得到gobalNum的值为:%d;其地址为%p\\n\", i, gobalNum, &gobalNum)}}func main() {// 创建一个sync.waitGroup防止主协程执行完就关闭程序var w sync.WaitGroupw.Add(2)fmt.Printf(\"====初始gobalNum的值为:%d;其地址为%p\\n\", gobalNum, &gobalNum)go add(&w)go sub(&w)w.Wait()fmt.Printf(\"====结束gobalNum的值为:%d;其地址为%p\\n\", gobalNum, &gobalNum)}/*====初始gobalNum的值为:0;其地址为0xc98400Add函数第0轮取到gobalNum的值为:0;其地址为0xc98400Add函数第0轮运算得到gobalNum的值为:1;其地址为0xc98400Sub函数第0轮取到gobalNum的值为:0;其地址为0xc98400 <------------------Sub函数第0轮运算得到gobalNum的值为:0;其地址为0xc98400 <------------------------错误Sub函数第1轮取到gobalNum的值为:0;其地址为0xc98400Sub函数第1轮运算得到gobalNum的值为:-1;其地址为0xc98400Add函数第1轮取到gobalNum的值为:0;其地址为0xc98400 <-----------------Add函数第1轮运算得到gobalNum的值为:0;其地址为0xc98400 <--------------------------错误Sub函数第2轮取到gobalNum的值为:0;其地址为0xc98400Sub函数第2轮运算得到gobalNum的值为:-1;其地址为0xc98400Add函数第2轮取到gobalNum的值为:-1;其地址为0xc98400Add函数第2轮运算得到gobalNum的值为:0;其地址为0xc98400====结束gobalNum的值为:0;其地址为0xc98400*/

 解决办法:

  • 使用sync.Mutex进行互斥访问共享资源

package mainimport (\"fmt\"\"sync\"\"time\")var gobalNum int = 0var lockgobalNum sync.Mutexfunc add(w *sync.WaitGroup) {defer w.Done()//for i := 0; i < 3; i++ {time.Sleep(1 * time.Second)lockgobalNum.Lock() // 加锁 <=========================================fmt.Printf(\"Add函数第%d轮取到gobalNum的值为:%d;其地址为%p\\n\", i, gobalNum, &gobalNum)gobalNum = gobalNum + 1fmt.Printf(\"Add函数第%d轮运算得到gobalNum的值为:%d;其地址为%p\\n\", i, gobalNum, &gobalNum)lockgobalNum.Unlock() //解锁 <=========================================}}func sub(w *sync.WaitGroup) {defer w.Done()for i := 0; i < 3; i++ {time.Sleep(1 * time.Second)lockgobalNum.Lock() // 加锁 <=========================================fmt.Printf(\"Sub函数第%d轮取到gobalNum的值为:%d;其地址为%p\\n\", i, gobalNum, &gobalNum)gobalNum = gobalNum - 1fmt.Printf(\"Sub函数第%d轮运算得到gobalNum的值为:%d;其地址为%p\\n\", i, gobalNum, &gobalNum)lockgobalNum.Unlock() //解锁 <=========================================}}func main() {// 创建一个sync.waitGroup防止主协程执行完就关闭程序var w sync.WaitGroupw.Add(2)fmt.Printf(\"====初始gobalNum的值为:%d;其地址为%p\\n\", gobalNum, &gobalNum)go add(&w)go sub(&w)w.Wait()fmt.Printf(\"====结束gobalNum的值为:%d;其地址为%p\\n\", gobalNum, &gobalNum)}/*====初始gobalNum的值为:0;其地址为0x768400Sub函数第0轮取到gobalNum的值为:0;其地址为0x768400Sub函数第0轮运算得到gobalNum的值为:-1;其地址为0x768400Add函数第0轮取到gobalNum的值为:-1;其地址为0x768400Add函数第0轮运算得到gobalNum的值为:0;其地址为0x768400Add函数第1轮取到gobalNum的值为:0;其地址为0x768400Add函数第1轮运算得到gobalNum的值为:1;其地址为0x768400Sub函数第1轮取到gobalNum的值为:1;其地址为0x768400Sub函数第1轮运算得到gobalNum的值为:0;其地址为0x768400Sub函数第2轮取到gobalNum的值为:0;其地址为0x768400Sub函数第2轮运算得到gobalNum的值为:-1;其地址为0x768400Add函数第2轮取到gobalNum的值为:-1;其地址为0x768400Add函数第2轮运算得到gobalNum的值为:0;其地址为0x768400====结束gobalNum的值为:0;其地址为0x768400*/

 注意:

  • 互斥锁Mutex的效率较低,用于读写次数不确定的场景

这里引入高效率锁 读写锁

  • RWmutex:读写锁常用读多写少的情况

    在读的时候数据之间不产生影响,在写发生的时候才会产生影响

package mainimport (\"fmt\"\"sync\"\"time\")// 定义一个共享的锁// var lock sync.Mutex // 总共消耗: 789.1378msvar lock sync.RWMutex // 总共消耗: 364.2697ms// 构建一个waitGroup防止主程序结束var mainWait sync.WaitGroup// 读工作者的协程func readWorker(n *int) {defer mainWait.Done() lock.RLock() // <========================RLock()=====================fmt.Printf(\"n的值:%d\\t\", *n)time.Sleep(10 * time.Millisecond)lock.RUnlock() // <========================RUnLock()=====================}// 写工作者的协程func writeWorker(n *int) {defer mainWait.Done()lock.Lock()fmt.Print(\"对数据进行写\\t\")*n = *n + 1time.Sleep(100 * time.Millisecond)lock.Unlock()}func main() {// 统计程序运行时间,比较RWMutex和Mutex的性能差距star := time.Now()// 定义一个共享的资源var gobalData int = 100for i := 0; i < 30; i++ {mainWait.Add(1)go readWorker(&gobalData)}for i := 0; i < 3; i++ {mainWait.Add(1)go writeWorker(&gobalData)}mainWait.Wait()final := time.Now()consum := final.Sub(star)fmt.Println(\"总共消耗:\", consum)}

注意: 

  • 使用RLock()来区分是否是读操作,当发现是读操作时则不需要竞争性访问某些资源,当发现是写操作时则要互斥的访问某资源

  • 耗时有差距的原因:在多核CPU比如说本机是8核CPU,同时运行了8个协程,当某一个时刻线程RLock()发现八个协程运行中都是进行读操作则协程之间可以并行的读取临界资源,当8个协程中有一个Lock(),则说明有一个协程是在修改临界区资源,则8个上锁的协程全部进行等待,等待Unlock()函数执行完,再进行判断

 7.3管道

type hchan struct {/* 实现缓冲区 *//* 环形队列 */qcount uint  // 当前环形队列中元素个数dataqsiz uint  // 环形队列可存放元素个数 | 缓冲区长度buf unsafe.Pointer // 环形队列开始指针sendx uint  // 写入的队列下标recvx uint  // 读取的队列下标/* 类型信息 */elemsize uint16 // 存储的元素大小elemtype *_type // 存储的元素类型closed uint32 // 管道关闭状态/* 实现等待阻塞 *//* 等待队列 */recvq waitq // 等待读消息的协程队列sendq waitq // 等待写消息的协程队列/* 互斥锁 */lock mutex // 互斥锁,chan 不允许并发读写}

概念:管道关键字为chan,分为有缓冲管道和无缓冲管道,主要用于协程并发,底层实现原理利用等待队列和环形队列。管道是线程安全的,多个协程访问管道时不会发生资源竞争。

作用:用于并发控制,且写入管道的数据都应该全部读完。

ch1 := make(chan int) //无缓冲管道ch2 := make(chan int,2) //有缓冲管道ch3 := make(chan any) //这里的any是interface{}的别名

注意:

  • 管道关闭之后就不能再往里面写入数据了。但是可以读取数据。读出得数据为0或者nil

  • 到管道没有关闭时,此时管道为空,再取数据会发生死锁导致fatal err产生

package mainimport \"fmt\"func main() {// 定义一个管道var intChan chan int// 不初始化的管道是无法使用。intChan = make(chan int, 3)fmt.Printf(\"管道的值:%v\", intChan) //管道的值:0xc00010a080// 向管道内存入数据intChan <- 5num := 20intChan <- numintChan <- 22// intChan <- 10 存多了 fatal error: all goroutines are asleep - deadlock!fmt.Printf(\"管的实际长度:%v,容量是:%v\\n\", len(intChan), cap(intChan))// 从管道中取出数据fmt.Println(<-intChan)fmt.Println(<-intChan)fmt.Println(<-intChan)// fmt.Println(<-intChan) 取多了 fatal error: all goroutines are asleep - deadlock!fmt.Printf(\"ch1管的实际长度:%v,容量是:%v\\n\", len(intChan), cap(intChan))// 管道的关闭ch2 := make(chan int, 3)ch2 <- 9ch2 <- 8close(ch2)// ch2 <- 5 // panic: send on closed channelfmt.Println(<-ch2)fmt.Println(<-ch2)fmt.Println(<-ch2)fmt.Println(<-ch2) //因为管道已经关闭且值也被取出来完了,随后再被取出来就是nil或者0fmt.Printf(\"ch2管的实际长度:%v,容量是:%v\\n\", len(ch2), cap(ch2))// 管道的遍历ch3 := make(chan int, 6)ch3 <- 1ch3 <- 2ch3 <- 3ch3 <- 4ch3 <- 5// close(ch3) // 或者提前关闭管道// 管道进行for-range时不再创建副本,直接取出管道内的值for vs := range ch3 {// 因此当管道内没有数据时,取管道数据的协程就会阻塞if len(ch3) == 0 {// 主线程进行管道取数据,数据已经取完,该协程阻塞,为防止主线程阻塞程序关闭,必须主动中断循环。break}fmt.Println(vs)fmt.Printf(\"ch3管的实际长度:%v,容量是:%v\\n\", len(ch3), cap(ch3))}defer func() {if i := recover(); i != nil {fmt.Println(\"报错:\", i)}}()fmt.Printf(\"ch3管的实际长度:%v,容量是:%v\\n\", len(ch3), cap(ch3))}

注意:

  • 只有管道关闭时,取管道的协程才能收到false(在管道为空的时候)

  • 管道进行for-range时不再创建副本,直接取出管道内的值

  • 主线程进行管道取数据,数据已经取完,该协程阻塞,为防止主线程阻塞程序关闭,应该主动中断循环。

// 管道关闭&&管道为空&&协程阻塞 = false_, ok := <-ch // 管道为空&&协程阻塞 = fatal err 报致命错误_, ok := <-ch 

7.4协程和管道

特别注意:

  • 管道如果没有关闭,进行空管道读取,超时之后会 报fatal err deadlock异常

  • 只要管道最后带有关闭,依然会持续等待管道产生数据

  • 管道如果没有关闭。进行持续的写入等待超时且没有读取到,协程也会报fatal err deadlock错

  • 管道如果关闭,再进行写入时会 报panic错误

  • 管道中没有数据时读取会返回默认值

  • 使用range循环管道时,不关闭会导致deadlock错误

package mainimport (\"fmt\"\"sync\"\"time\")// 消费者,每50毫秒了消费一个物品,消费100件func Consumer(ch chan any, superVision *sync.WaitGroup) {defer superVision.Done()// for i := 0; i < 6; i++ {// time.Sleep(500 * time.Millisecond)// _, err := <-ch// if err == false {// fmt.Println(\"!!!!没有商品了!!!\")// break// }// fmt.Println(\"<===消耗一件商品\")// fmt.Printf(\"当前仓库还有:%v件商品; 仓库大小%v ;\\n\", len(ch), cap(ch))// }// 上述循环指定了从管道中取出多少,这种可能会导致死锁发生而报错,可使用下面方式for {_, ok := <-ch // 关键语句,只有管道关闭时,才能取出ok关键字fmt.Println(ok)time.Sleep(500 * time.Millisecond)if ok {fmt.Println(\"<===消耗一件商品\")fmt.Printf(\"当前仓库还有:%v件商品; 仓库大小%v ;\\n\", len(ch), cap(ch))} else {fmt.Println(\"!!!!没有商品了!!!\")return}}}// 生产者,每45毫秒生产一件商品,总共生产100件func Produncter(ch chan any, superVision *sync.WaitGroup) {defer superVision.Done()for i := 0; i < 5; i++ {time.Sleep(250 * time.Millisecond)ch \")fmt.Printf(\"当前仓库还有:%v件商品; 仓库大小%v ;\\n\", len(ch), cap(ch))}time.Sleep((1 * time.Second))fmt.Println(\"生产了一件商品===>\")fmt.Printf(\"当前仓库还有:%v件商品; 仓库大小%v ;\\n\", len(ch), cap(ch))ch <- \"Good\"/**********在确保没有生产者再生产时,需要生产者负责关闭仓库,避免死锁的发生****/defer close(ch)}func main() {// 创建一个管道 代表仓库store := make(chan any, 10)// 创建一个监督完整时间完成得锁var superVision sync.WaitGroupsuperVision.Add(2)// 创建两个协程go Consumer(store, &superVision)go Produncter(store, &superVision)superVision.Wait()}

 7.5特殊管道使用

select case从多个管道中随机只取出一个数据,如果都没有的话结束。

time.Tick()返回一个计时器,用于for range遍历管道时,每轮等待一段时间

只读只写的管道声明意义不大,通常声明双向管道,在函数中内限制双向管道为只读或者只写

package mainimport (\"fmt\"\"sync\"\"time\")// 对管道只能进行读取func Reader(ch <-chan int, w *sync.WaitGroup) {defer w.Done()for {j, err := <-chif err {fmt.Println(\"读取数据:\", j)} else {fmt.Println(\"无数据可读\")break}}}// 对管道只能进行写入func Writer(ch chan<- int, w *sync.WaitGroup) {defer w.Done()for i := 1; i < 5; i++ {fmt.Println(\"写入数据:\", i)ch <- i}close(ch)}// 创建多个管道func CreatChan(i int) []chan int {res := []chan int{}for j := 0; j < i; j++ {res = append(res, make(chan int, 10))}return res}// 为多个仓库分配数据func GeneratData(chs []chan int, w *sync.WaitGroup) {defer w.Done()for _, ch := range chs {for i := 1; i < 5; i++ {ch <- i}close(ch)}}func main() {// 创建一个空缓冲取管道ch := make(chan int)// 创建同步var w sync.WaitGroupw.Add(2)// 将双向管道转换成单向管道限制操作go Reader(ch, &w)go Writer(ch, &w)w.Wait()// 可以使用select进行多管道读取lc := CreatChan(4)w.Add(3)go GeneratData(lc, &w)go func() {defer w.Done()select {case data := <-lc[0]:fmt.Println(\"接收多管道的0数据data:\", data)case data := <-lc[1]:fmt.Println(\"接收多管道的1数据data:\", data)case data := <-lc[2]:fmt.Println(\"接收多管道的2数据data:\", data) // 当多个管道都没有数据时避免阻塞,从default中选择一个default:fmt.Println(\"多个管道数据都已经接受完了!\")}}()go func() {defer w.Done()ticker := time.Tick(200 * time.Millisecond)for v := range lc[3] {<-tickerfmt.Println(\"接收多管道的3数据v:\", v)}}()w.Wait()}

8.网络编程

这里仅介绍TCP。

TCP/IP协议:是一种面向连接、可靠的、基于字节流的传输层通信协议,因为是面向连接的协议,数据像流水一样传输,存在粘包问题。

TCP服务端:一个TCP服务段一次可以连接多个客户端,因此,当服务器每监听到有一个连接请求就可以创建一个协程去专门处理该连接请求的数据传输。

TCP客户端:主动呼叫服务器一端。

8.1服务端构建

步骤:

1.使用net.Listen(\"tcp\",\":port\")创建监听某个端口以及某种协议的监听器

2.使用上步返回的监听器使用listener.Accept()建立连接,返回一个con(tcp连接),另外该con实现io.Reader接口,创建bufio流进行读取

3.可以直接对该连接进行con.Read(data),或者转换成bufio流进行读取

4.连接也能写入数据作为返还数据,con.Writer(responData)

package mainimport (\"fmt\"\"net\")func handleConnector(con net.Conn) {defer con.Close()addr := con.RemoteAddr()fmt.Printf(\"===建立了来自 %v ; %v远端的连接===\\n\", addr.Network(), addr.String())// 持续接收来自远端的数据for {// 持续读连接的数据receiveData := make([]byte, 1024)// 读取连接中数据n, _ := con.Read(receiveData)//或者使用缓存的形式进行读取// linkFromClient := bufio.NewReader(con) // 说明con实现了io.Reader接口// linkFromClient.Read(receiveData)if n > 0 {fmt.Printf(\"ReceiveData:%s\\n\", receiveData[:n])// 回复数据端con.Write([]byte(\"Receive data======\\n\"))}}}func main() {// 创建一个监听端口的监听器listenPort, err := net.Listen(\"tcp\", \":8080\")if err != nil {fmt.Println(\"监听端口异常=====\")return} else {fmt.Println(\"监听器创建成功=====\")}// 无异常,持续监听端口for {// 一直接受客户端的连接con, _ := listenPort.Accept()// 对于每个连接器都开启一个go协程去处理go handleConnector(con)}}

8.2客户端的建立

步骤:

1.使用net.Dial(\"tcp\",\"IP:port\")建立对话连接,返回一个con和err(是否呼叫成功)

2.该con和上述TCP服务端监听Accept返回的con是一直的,能够进行读写。

package mainimport (\"bufio\"\"fmt\"\"net\"\"os\")func main() {// 创建一个Dial(呼叫器)连接服务器dialIpPort, err := net.Dial(\"tcp\", \"localhost:8080\")if err != nil {fmt.Println(\"呼叫失败======\", err)return}// 延迟关闭呼叫defer dialIpPort.Close()reader := bufio.NewReader(os.Stdin) // 这里的os.stdin实现了io.Reader接口// writer := bufio.NewWriter(dialIpPort) //也可以建立缓冲 写入连接// 持续向服务端发送数据for {fmt.Println(\"请输入你想要发送的数据:\")postD, _ := reader.ReadString(\'\\n\')// 停止发送数据 断开连接if string(postD) == \"end\" {break}// 向服务端发送数据// pData := []byte(postD)dialIpPort.Write([]byte(postD))receiveBuf := make([]byte, 1024)n, _ := dialIpPort.Read(receiveBuf)fmt.Printf(\"接收成功:%v\", string(receiveBuf[0:n]))}}

9.反射

反射:reflect包下的函数,运行时动态的获取各种变量信息,典型用法是用静态类型interface{}保存一个值,通过调用TypeOf() 获取其动态类型信息,该函数返回一个Type类型的值,调用ValueOf()函数返回Value类型的值。

注意:

  • 结构体得首字母的大小写会影响到其值的设置或者方法的获取

  • Elem()主要是获取原始对象并修改的,但是value必须是指针类型

  • 通过reflect.TypeOf(x) == reflect.int 能够判断x是不是int类型

package mainimport (\"fmt\"\"reflect\")type B struct {// 这里的变量首字母大写,才能通过reflect.ValueOf(interface{}(&b))Elem().Filed(0).SetInt()进行值得修改X intY string}// 这里的方法名首字母需要大写,否则reflect无法识别func (b B) PrintTest() {fmt.Println(\"结构体的值:\", b)}func testReflect(i any) {fmt.Println(\"===================================\")TypeRes := reflect.TypeOf(i)// 类别是一个大类,类型是具体的类,主要体现在结构体上fmt.Printf(`============= TypeRes: %vTypeRes变量类别: %v `, TypeRes, TypeRes.Kind())ValueRes := reflect.ValueOf(i)fmt.Printf(`============= ValueRes: %v`, ValueRes)// value.interface()能够将value进行转换,返回一个接口a := ValueRes.Interface()fmt.Printf(`============= a的类型: %T`, a)_, ok := a.(float32)var is stringif ok {is = \"是\"} else {is = \"不是\"}fmt.Printf(\"%v %s float32类型\\n\", TypeRes, is)fmt.Println(\"===================================\")}func main() {// 基本数据类型反射a := 10testReflect(a)// 结构体类型b := B{1, \"张三\"}testReflect(b)// 通过ValueOf获取value在setValue修改值,这里需要变量的引用i := interface{}(&a)v := reflect.ValueOf(i)v.Elem().SetInt(100)fmt.Println(\"=======\", a)// 通过valueOf获取结构体的valuej := interface{}(b)w := reflect.ValueOf(j)fmt.Println(\"=====w:\", w)fmt.Println(\"=====w.NumFiled:\", w.NumField())fmt.Println(\"=====w.NumMethod:\", w.NumMethod())fmt.Println(\"=====w.Filed1:\", w.Field(0))fmt.Println(\"=====w.Filed2:\", w.Field(1))// 方法索引按照 ASCLL码进行排列,A,B,C…… 索引:0,1,2……w.Method(0).Call(nil) // 调用方法时,无参数函数可以使用nil作为参数// 只有指针类型才能调用Elem()方法// 修改结构体的值仍然需要使用指针变量,vk.Elem()表示vk指针的对象k := interface{}(&b)vk := reflect.ValueOf(k)fmt.Println(\"vk======\", vk.Kind())fmt.Println(\"vk======\", vk.Elem().Kind())fmt.Println(\"vk======\", vk.Elem().Field(0))if vk.Kind() == reflect.Ptr && vk.Elem().Kind() == reflect.Struct {vk.Elem().Field(0).SetInt(10)vk.Elem().Field(1).SetString(\"王五\")fmt.Println(\"b======\", b)} else {fmt.Println(\"vk 并不是指针类型 无法使用Elem()还原对象\")}}

10.Context

表示上下文,常用于多个协程之间相互通信,比如多个协程之间可能需要同时取消,或者需要共享某些特定的值。

// 创建一个空上下文对象ctx := context.Background()// 关闭当前上下文环境ctx.Done()

10.1控制协程的取消

package mainimport (\"context\"\"fmt\"\"sync\"\"time\")var wa sync.WaitGroupfunc main() {// 创建一个空上下文对象ctx := context.Background()wa.Add(3)go func(c context.Context) {defer wa.Done()select {case <-time.After(1 * time.Second):fmt.Println(\"父协程经过一秒结束\")case <-c.Done():fmt.Println(\"父协程被取消\")}}(ctx)/*基于上述空context创建一个带有取消的子context 函数返回:子context:函数:该函数的执行能够终止子context*/ctxWithCancle, cancle := context.WithCancel(ctx)go func(c context.Context) {defer wa.Done()select {case <-time.After(1 * time.Second):fmt.Println(\"子协程经过一秒结束\")case <-c.Done():fmt.Println(\"子协程被取消:\", time.Now())}}(ctxWithCancle)go func(c context.Context) {defer wa.Done()select {case <-time.After(1 * time.Second):fmt.Println(\"子协程经过一秒结束\")case <-c.Done():fmt.Println(\"子协程被取消:\", time.Now())}}(ctxWithCancle)time.Sleep(900 * time.Millisecond) // 调用上下文取消函数,直接终结包含该上下文对象的协程cancle()fmt.Println(\"context 创建成功===\")defer wa.Wait()}

 10.2设定Context终结时间

package mainimport (\"context\"\"fmt\"\"time\")func main() {// 创建一个空上下文ctx := context.Background()/*创建一个特定终结时间的上下文*/ctxWithTimeOut, cancle := context.WithTimeout(ctx, 2*time.Second)go func(ct context.Context) {select {case <-time.After(3 * time.Second):fmt.Println(\"协程1号==经过三秒任务完成===\")case <-ct.Done():fmt.Println(\"协程1号==上下文环境结束===\")}}(ctxWithTimeOut)go func(ct context.Context) {select {case <-time.After(1 * time.Second):fmt.Println(\"协程2号==经过一秒任务完成===\")case <-ct.Done():fmt.Println(\"协程2号==上下文环境结束===\")}}(ctxWithTimeOut)time.Sleep(800 * time.Millisecond)cancle()time.Sleep(4 * time.Second)fmt.Println(\"主任务结束===\")}

注意:当设置了timeOut的context,当没有超时时先遇到了cancle()则提前取消

10.3对context设置一些值

package mainimport (\"context\"\"fmt\"\"time\")func main() {// 创建一个空上下文ctx := context.Background()// 在上下文中设置一些值go func(c context.Context) {time.Sleep(1 * time.Second)ctx = context.WithValue(c, \"key\", \"zhangsan\")}(ctx)time.Sleep(2 * time.Second)// 取出一些值go func(c context.Context) {time.Sleep(2 * time.Second)fmt.Println(ctx.Value(\"key\"))}(ctx)// 取出一些值go func(c context.Context) {time.Sleep(2 * time.Second)fmt.Println(ctx.Value(\"key\"))}(ctx)time.Sleep(3 * time.Second)}

 10.4Http中的应用

package mainimport (\"context\"\"fmt\"\"net/http\"\"time\")func handlRequest(w http.ResponseWriter, r *http.Request) {// 获取请求体的contextrctx := r.Context()rctx, cancle := context.WithTimeout(rctx, 2*time.Second)// 及时释放资源defer cancle()select {case <-time.After(2200 * time.Millisecond):fmt.Println(\"2.2秒请求处理完成\")w.Write([]byte(\"Hello world\"))case <-rctx.Done():fmt.Println(\"请求处理超时==\")}}func main() {http.HandleFunc(\"/\", handlRequest)http.ListenAndServe(\":8080\", nil)}