Go语言基础笔记
Go语言的起源与发展
开发团队
罗伯特·格瑞史莫(Robert Griesemer),罗勃·派克(Rob Pike)及肯·汤普逊(Ken Thompson)于2007年9月开始设计Go,稍后 lan Lance Taylor、Russ Cox加入项目
Go语言发展简史
- 2007年,谷歌工程师Rob Pike、Ken Thompson和Robert Griesemer开发设计一门全新的语言,这是Go语言的最初原型
- 2009年11月,谷歌将Go语言以开放源代码的方式向全球发布
- 2015年8月,Go 1.5版本发布,本次更新中移除了"最后残余的c代码"
- 2017年2月,Go 1.8版本发布
- 2017年8月,Go 1.9版本发布
- 2018年2月,Go 1.10版本发布
- 2018年8月,Go 1.11版本发布
- 2019年2月,Go 1.12版本发布
- 2019年9月,Go 1.13版本发布
- 2020年2月,Go 1.14版本发布
- 2020年8月,Go 1.15版本发布
……一直迭代
Go语言的吉祥物 - 金花鼠Gordon
开发工具
visual studio code:Microsoft产品(简称VSCode),默认提供Go语言的语法高亮,安装Go语言插件,还可以支持智能提示,编译运行等功能
开发环境搭建
SDK简介
SDK(Software Development Kit)软件开发工具包
SDK是提供给开发人员使用的,其中包含了对应开发语言的工具包
下载SDK
Go语言官网(可能访问失败)
由于访问第一个网站需要一点技术所以我们推荐使用第二个下载地址
安装SDK
下载完成后解压即可,建议不要放在C盘即可
验证SDK是否安装成功
配置Golang环境变量
上图是配置好后的效果,配置过程不做详细记录。
第一个Go程序(入门案例)
Go程序开发的基本结构
HelloWorld案例
package main // 声明文件所在的包
import "fmt" // 导入 fmt 包
// 主函数,程序入口
func main() {
fmt.Println("Hello World!") // 在控制台打印输出一句话
}
编译执行go文件
我们还可以直接运行go文件使用 go run
命令即可
真实的工作中我们依然先编译在运行,go run
命令不常用,且速度较慢
执行流程分析
如果是对源码编译后,再执行,Go的执行流程如下图
如果是对源码直接执行 go run
指令,Go的执行流程如下图
上述两种执行流程的方式区别
- 在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件变大了很多
- 如果我们先编译生成了可执行文件,那么我们可以将该可执行文件拷贝到没有go开嘎环境的机器上,仍然可以运行
- 如果我们直接
go run
源码,那么如果要在另一个机器上这么运行,也需要go开发环境,否则无法执行
编译注意事项
编译后的文件可以另外指定名字,方式如下:
语法注意事项
- 源文件以 “go” 为扩展名
- 程序的执行入口是main函数
- 严格区分大小写
- 方法由一条条语句构成,每个语句后不需要分号
- Go编译器是一行行进行编译的,因此我们一行就写一条语句,不要把多条语句写在同一行
- 定义的变量或者import的包如果没有使用到,代码不能编译通过
- 大括号都是成对出现的,缺一不可
Go语言的转义字符
常用的转义字符如下:
- \t:表示一个制表符,通常使用它可以排版
- \n:换行符
- \\:一个\
- \“:一个”
- \r:一个回车
package main
import "fmt"
func main() {
// \t
fmt.Println("tom\tjack")
// \n
fmt.Println("hello\nworld")
// \\
fmt.Println("\\")
// \"
fmt.Println("\"")
// \r
fmt.Println("你是最帅的\r我")
}
Go语言的注释
-
行注释//:VSCode快捷键:crtl+/ 在按一次取消注释
-
块注释(多行注释)/**/:VSCode快捷键:shift+alt+a 在按一次取消注释
注意:块注释中不可以嵌套块注释
规范Go语言的代码风格
正确的注释和注释风格
Go官方推荐使用行注释来注释整个方法和语句
正确的缩进和空白
VSCode缩进快捷键
向后缩进:tab
向前取消缩进:shift+tab
通过命令完成格式化操作:
运算符两边习惯性各加一个空格
行长约定
一行最长不超过80个字符,超过的请使用换行展示,尽量保持格式优雅
Golang标准库 API 文档
变量
变量的定义
变量相当于内存中一个数据存储空间的表示,通过变量名可以访问到变量值,变量是程序的基本组成单位
变量的使用步骤
第一步:声明变量
第二步:给变量赋值
第三步:使用变量
package main
import "fmt"
func main() {
// 变量的声明
var age int
// 变量的赋值
age = 18
// 变量的使用
fmt.Println("age = ", age)
// 变量的声明和赋值可以合成一句
var age2 int = 19
fmt.Println("age2 = ", age2)
}
变量的4种使用方式
package main
import "fmt"
func main() {
// 第一种:指定变量的类型并且赋值
var num int = 18
fmt.Println(num)
// 第二种:指定变量的类型但是不赋值
var num2 int
fmt.Println(num2) // 打印默认值
// 第三种:指定变量值但是不指定类型
var num3 = 10 // 自动推断变量类型
fmt.Println(num3)
// 第四种:省略var关键字,注意 := 不能写为 =
sex := "男"
fmt.Println(sex)
}
注意:第二种为声明变量,第一种和第三种是初始化变量,给变量赋值是先声明变量再给值才是给变量赋值
一次性声明多个变量
package main
import "fmt"
func main() {
// 一次性声明多个变量
var n1, n2, n3 int
fmt.Println(n1)
fmt.Println(n2)
fmt.Println(n3)
var n4, name, n5 = 10, "jack", 7.8
fmt.Println(n4)
fmt.Println(name)
fmt.Println(n5)
n6, height := 1.9, 100.6
fmt.Println(n6)
fmt.Println(height)
}
全局变量的声明
package main
import "fmt"
// 全局变量的声明
var n7 = 100
var n8 = 1.2
// 一次性声明全局变量
var (
n9 = 300
n10 = "netty"
)
func main() {
fmt.Println(n7)
fmt.Println(n8)
fmt.Println(n9)
fmt.Println(n10)
}
数据类型
数据类型示意图
整数类型
整数类型:用于存放整数值
有符号整数类型
无符号整数类型
其他整数类型
Go语言的整数类型默认声明为 int
类型
下面的代码为实例,说明Go语言的变量默认整数类型为 int,同时如何查看变量占用的字节数,使用到了 unsafe
包
package main
import (
"fmt"
"unsafe"
)
func main() {
num1 := 28
fmt.Printf("num1的数据类型是: %T", num1)
fmt.Println()
// 变量占用的字节数
fmt.Println(unsafe.Sizeof(num1))
}
效果如下:
如何选择整数类型
Golang程序中整数变量在使用时,遵守保小不保大的原则
即:在保证程序正确运行下,尽量使用占用空间小的数据类型
浮点类型
浮点类型:用于存放小数值
浮点类型种类
package main
import "fmt"
func main() {
// 定义浮点类型的数据
num1 := 3.14
fmt.Println(num1) // 3.14
num2 := -3.14
fmt.Println(num2) // -3.14
num3 := 314E+2
fmt.Println(num3)// 31400
num4 := 314E-2
fmt.Println(num4) // 3.14
num5 := -314e-2
fmt.Println(num5) // -3.14
}
演示精度丢失问题
package main
import "fmt"
func main() {
// 浮点数可能会有精度的损失,所以通常情况下建议使用float64
// Go中默认的浮点类型为:float64
var num1 float32 = 256.000000916
var num2 float64 = 256.000000916
fmt.Println(num1)
fmt.Println(num2)
}
字符类型
package main
import "fmt"
func main() {
// Go的字符使用的是UTF-8方案
// 定义字符类型的数据
// 字符类型本质是一个整数,可以参与运算,输出字符的时候会将对应的码值进行输出
// 字母,数字,标点等字符,底层是按照ASCII进行存储
var c1 byte = 'a'
fmt.Println(c1)// 97
var c2 byte = '6'
fmt.Println(c2)// 54
var c3 byte = '('
fmt.Println(c3)// 40
// 汉字字符,底层对应的是Unicode码值
var c4 int = '中'
fmt.Println(c4)// 20013
// 显示对应的字符
var c5 byte = 'A'
fmt.Printf("c4对应应的具体字符为: %c", c5)
}
布尔类型
布尔类型也叫 bool 类型,bool 类型数据只允许取值 true 和 false
bool 类型占一个字节
bool 类型适用于逻辑运算,一般用于程序流程控制
字符串类型
字符串的定义
字符串就是一串固定长度的字符连接起来的字符序列
字符串的使用
package main
import "fmt"
func main() {
// 定义一个字符串
var s1 string = "全面拥抱Golang"
fmt.Println(s1)
// 字符串是不可变的,指的是字符串一旦定义好,其中的字符的值不能改变
var s2 string = "abc"
s2 = "def"
fmt.Println(s2) // def
// 字符串的表示形式:
// 第一
var s3 string = "ascllinelgn"
// 第二
var s4 string = `"zd\/`
fmt.Println(s3)
fmt.Println(s4)
// 字符串的拼接
var s5 string = "def" + "func"
fmt.Println(s5)
// 当一个字符串过长的时候注意+号保留在上一行的最后
var s6 string = "abc" + "abc" + "abc" + "abc" +
"abc" + "abc" + "abc" + "abc" + "abc" + "abc" +
"abc" + "abc"
fmt.Println(s6)
}
基本数据类型默认值
在Golang中数据类型都有一个默认值,当没有赋值时,就会保留默认值
代码验证
package main
import "fmt"
func main() {
var a int
var b float64
var c bool
var d string
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
}
基本数据类型之间的转换
Go在不同类型的变量之间赋值时需要显式转换,并且只有显式转换
语法
表达式 T(v) 将值v转换为类型T
T:就是数据类型
v:就是需要转换的变量
案例展示
package main
import "fmt"
func main() {
// 进行类型转换
var n1 int = 100
var n2 float64 = float64(n1) // 必须强制转换
fmt.Println(n2)
// 将 int64 转为 int8 的时候,编译不会出错,但是会有数据溢出
var n3 int64 = 888888
var n4 int8 = int8(n3)
fmt.Println(n4)
// 一定要匹配=左右的数据类型,否则会有错误
var n5 int32 = 12
var n6 int64 = int64(n5) + 30
fmt.Println(n6)
var n7 int64 = 12
var n8 int8 = int8(n7) + 127 // 编译通过,但是结果可能会溢出
// var n9 int8 = int8(n7) + 128 // 编译不会通过
fmt.Println(n8)
// fmt.Println(n9)
}
基本数据类型和 string
(基本数据类型)的转换
在程序开发中,我们经常需要将基本数据类型转为 string 类型,或者将 string 类型转为基本数据类型
基本类型转 string 类型
package main
import (
"fmt"
"strconv"
)
func main() {
var n1 int = 19
var n2 float32 = 4.78
var n3 bool
var n4 byte = 'a'
// 第一种转换方式(推荐使用)
var s1 string = fmt.Sprintf("%d", n1)
fmt.Printf("s1对应的类型是: %T, s1 = %v \n", s1, s1)
var s2 string = fmt.Sprintf("%f", n2)
fmt.Printf("s2对应的类型是: %T, s2 = %v \n", s2, s2)
var s3 string = fmt.Sprintf("%t", n3)
fmt.Printf("s3对应的类型是: %T, s3 = %v \n", s3, s3)
var s4 string = fmt.Sprintf("%c", n4)
fmt.Printf("s4对应的类型是: %T, s4 = %v \n", s4, s4)
// 第二种转换方式,相对来说麻烦,可以查文档即可
var s5 string = strconv.FormatInt(int64(n1), 10)
fmt.Printf("s5对应的类型是: %T, s5 = %v \n", s5, s5)
}
string 类型转基本类型
package main
import (
"fmt"
"strconv"
)
func main() {
// strig -> bool
var s1 string = "true"
var b bool
// ParseBool的返回值有两个: (value bool, err error)
// value 就是我们得到的 bool 类型的数据,err可能出现的错误
// 我们只关注 value, err 可以用_直接忽略
b, _ = strconv.ParseBool(s1)
fmt.Printf("b的类型是: %T, b = %v \n", b, b)
// string -> int64
var s2 string = "19"
var num1 int64
num1, _ = strconv.ParseInt(s2, 10, 64)
fmt.Printf("num1的类型是: %T, num1 = %v \n", num1, num1)
// string -> float
var s3 string = "3.14"
var f1 float64
f1, _ = strconv.ParseFloat(s3, 64)
fmt.Printf("f1的类型是: %T, f1 = %v \n", f1, f1)
// 注意:string向基本数据类型转的时候,一定要确保string类型转成有效的数据类型,否则最后得到的结果就是按照对应类型的默认值输出
}
指针
package main
import (
"fmt"
)
func main() {
var age int = 18
// &+变量 就可以获取这个变量内存的地址
fmt.Println(&age)
// 定义一个指针变量
// *int 是一个指针类型
// &age就是一个地址,是ptr变量的具体的值
var ptr *int = &age
fmt.Printf("ptr的类型是: %T, ptr = %v \n", ptr, ptr)
// 获取ptr这个指针或者这个地址指向的数据
fmt.Println(*ptr)
}
- 可以通过指针改变指针值
package main
import (
"fmt"
)
func main() {
var num int = 10
fmt.Println(num)
var ptr *int = &num
*ptr = 20
fmt.Println(num)
}
- 指针变量接收的一定是地址值
- 指针变量的地址不可以不匹配
- 基本数据类型都有对应的指针类型,形式为 *数据类型
标识符
标识符的定义
变量,方法等只要是起名字的地方,那个名字就是标识符
标识符定义规则
-
由数字,字母,下划线组成
-
不可以以数字开头,严格区分大小写,不能包含空格,不可以使用Go中的保留关键字
-
见名知意,增加可读性
-
"_"本身在Go中是一个特殊的标识符,称为空标识符,可以代表任何其他的标识符,但是它对应的值会被忽略,所以仅能被作为占位符使用,不能单独作为标识符使用。
-
可以使用如下形式但是不建议
var int int = 10
(int,float32,float64等不算是保留关键字,但是也尽量不要使用) -
长度无限制,但是不建议非常长,太长可读性较差
-
起名规则
- 包名:尽量保持package的名字和目录保持一致,尽量采取有意义的包名,不要和标准库冲突
- 变量名、函数名、常量名:采用驼峰法
- 如果变量名、函数名、常量名首字母大写,则可以被其他的包访问;
如果首字母小写,则只能在本包中使用
注意:
import导入语句通常放在文件开头包声明语句的下面
导入的包名需要使用双引号包裹起来
包名是从$GOPATH/src/后开始计算的,使用/进行路径分隔
main包是一个程序的入口包,所以main函数所在的包建议定义为main包,如果不定义main包,那么就不能得到可执行文件
util.go文件如下:
package test
var StuNo int = 20034 // 定义学生的学号
main.go文件如下:
package main // 声明为main包,程序的入口包
import (
"fmt"
"gocode/godemo1/unit2/demo13/test"
)
func main() {
var age int = 19
fmt.Println(age)
fmt.Println(test.StuNo)
}
关键字和预定义标识符
预定义标识符:一共36个预定标识符,包含基础数据类型和系统 内嵌函数
运算符
运算符示意图
算术运算符
package main
import (
"fmt"
)
func main() {
// + 正数 相加 字符串拼接
var n1 int = +10
fmt.Println(n1) // 10
var n2 int = 4 + 7
fmt.Println(n2) // 11
var s1 string = "abc" + "def"
fmt.Println(s1) // abcdef
// -,* 减号和乘号与加号同理
// /
fmt.Println(10 / 3) // int 3
fmt.Println(-10 / 3) // int -3
fmt.Println(10.0 / 3) // float64 3.333333333333333335
// % 取模 a % b = a - a / b * b
fmt.Println(10 % 3) // 10 - 10 / 3 * 3 = 1
fmt.Println(-10 % 3)// -10 - -10 / 3 * 3 = -1
fmt.Println(10 % -3)// 10 - 10 / -3 * -3 = 1
fmt.Println(-10 % -3)// -10 - -10 / -3 * -3 = -1
// ++ --
var a int = 10
a++
fmt.Println(a) // 11
// Go语言里++,-- 非常简单,只能单独使用,不能参与到运算中去
// Go语言里,++,-- 只能在变量的后面,不能写在变量的前面
}
赋值运算符
package main
import "fmt"
func main() {
// =
var n1 int = 10
fmt.Println(n1)
// +=
var n2 = 20
n2 += 20 //n2 = n2 + 20
fmt.Println(n2)
// 交换两个变量的值
var a int = 1
var b int = 2
a, b = b, a
fmt.Printf("a = %v, b = %v", a, b)
}
关系运算符
关系运算符的结果都是 bool 型,也就是要么是 true,要么是false
关系表达式经常用在流程控制中
package main
import (
"fmt"
)
func main() {
// 判断左右两侧的值是否相等
fmt.Println(6 == 3) // false
// 判断左右两侧的值是否不相等
fmt.Println(6 != 3) // true
fmt.Println(6 > 3) // true
fmt.Println(6 < 3) // false
fmt.Println(6 >= 3) // true
fmt.Println(6 <= 3) // false
}
逻辑运算符
package main
import (
"fmt"
)
func main() {
var t bool = true
var f bool
// 与逻辑 都是 true 则为 true 否则 false
// 短路:当左侧为 false 右侧不执行
fmt.Println(t && t) // true
fmt.Println(t && f) // false
fmt.Println(f && t) // false
fmt.Println(f && f) // false
fmt.Println()
// 或逻辑 都是 false 则为 false 否则 true
// 短路:当左侧为 true 右侧不执行
fmt.Println(t || t) // true
fmt.Println(t || f) // true
fmt.Println(f || t) // true
fmt.Println(f || f) // false
fmt.Println()
// 非逻辑 取相反的结果
fmt.Println(!t) // false
fmt.Println(!f) // true
}
位运算符
Go语言中,位运算符用于对整数进行位级操作,它们允许对整数的二进制表示进行操作
package main
import (
"fmt"
)
func main() {
// 按位与 对两个整数的二进制表示进行按位与操作
// 只有当两个对应的位都为 1 时,结果的相应位才为 1
// 否则,结果的相应位为 0
result := 5 & 3 // 5 -> 101, 3 -> 011
fmt.Println(result) // 1
// 按位或 对两个整数的二进制表示进行按位或操作
// 只要两个对应的位中有一个为 1,结果的相应位就为 1
// 否则,结果的相应位为 0
result = 5 | 3
fmt.Println(result) // 7
// 按位异或
// 当两个对应的位不同时,结果的相应位为 1
// 否则,结果的相应位为 0
result = 5 ^ 3
fmt.Println(result) // 6
// 符号取反
result = ^ 5 + 1
fmt.Println(result) // -5
// 左移位
// 将二进制数的每一位向左移动指定的位数,右侧补 0
a := 5 // 101
result = a << 2 // 10100 -> 20
fmt.Println(result)
// 右移位
// 将二进制数的每一位向右移动指定的位数,左侧补 0 或 符号位
b := -5
result = b >> 2
fmt.Println(result) // -2
}
获取用户终端输入
package main
import (
"fmt"
)
func main() {
// 实现键盘录入学生的年龄,姓名,成绩,是否是VIP
var age int
fmt.Println("请录入学生的年龄:")
fmt.Scanln(&age)
var name string
fmt.Println("请录入学生的姓名:")
fmt.Scanln(&name)
var score float64
fmt.Println("请录入学生的成绩:")
fmt.Scanln(&score)
var isVip bool
fmt.Println("请录入学生是否为VIP:")
fmt.Scanln(&isVip)
fmt.Printf("学生的年龄为:%v, 姓名为:%v, 成绩为:%v, 是否为VIP: %v", age, name, score, isVip)
}
package main
import (
"fmt"
)
func main() {
var age int
var name string
var score float64
var isVip bool
fmt.Println("请录入学生的年龄,姓名,成绩,是否是VIP,使用空格进行分隔")
fmt.Scanf("%d %s %f %t", &age, &name, &score, &isVip)
fmt.Printf("学生的年龄为:%v, 姓名为:%v, 成绩为:%v, 是否为VIP: %v", age, name, score, isVip)
}
流程控制
分支结构
if分支
单分支
基本语法:
if 条件表达式 {
逻辑代码
}
当条件表达式为 true 时,执行逻辑代码,否则不执行
条件表达式左右的 () 可以不写,也建议不写
if 和表达式中间,一定要有空格
在Golang中,{} 是必须有的,就算你只写一行代码
package main
import "fmt"
func main() {
// 如果口罩的库存小于 30 个,提示库存不足
var count int = 20
if count < 30 {
fmt.Println("对不起,口罩存量不足")
}
}
在Golang里,if 后面可以并列的加入变量的定义
package main
import "fmt"
func main() {
// 如果口罩的库存小于 30 个,提示库存不足
if count := 20; count < 30 {
fmt.Println("对不起,口罩存量不足")
}
}
多分支
package main
import (
"fmt"
)
func main() {
// 根据学生的分数给出学生的等级
var score int = 100
if score >= 90 { // >=90 -----A
fmt.Println("您的成绩为A级别")
} else if score >= 80 { // >=80 -----B
fmt.Println("您的成绩为B级别")
} else if score >= 70 { // >=70 -----C
fmt.Println("您的成绩为C级别")
} else if score >= 60 { // >=60 -----D
fmt.Println("您的成绩为D级别")
} else {
// <60 -----E
fmt.Println("您的等级为E级别")
}
}
双分支
基本语法:
if 条件表达式 {
逻辑代码1
} else {
逻辑代码2
}
当条件表达式成立,即执行逻辑代码1,否则执行逻辑代码2,{} 也是必须有的
package main
import "fmt"
func main() {
// 如果口罩的库存小于 30 个,提示库存不足,否则提示:库存充足
// 定义口罩的数量
var count int = 40
if count < 30 {
fmt.Println("口罩库存不足")
} else {
fmt.Println("口罩库存充足")
}
}
switch
基本语法:
switch 表达式 {
case 值1, 值2:
代码块1
case 值3, 值4:
代码块2
default:
代码块
}
package main
import (
"fmt"
)
func main() {
// 定义学生的成绩
var score int = 99
// 根据分数判断等级
switch score / 10 {
case 10 :
fmt.Println("您的等级为S级")
case 9 :
fmt.Println("您的等级为A级")
case 8 :
fmt.Println("您的等级为B级")
case 7 :
fmt.Println("您的等级为C级")
case 6 :
fmt.Println("您的等级为D级")
case 5 :
fmt.Println("您的等级为E级")
case 4 :
fmt.Println("您的等级为E级")
case 3 :
fmt.Println("您的等级为E级")
case 2 :
fmt.Println("您的等级为S级")
case 1 :
fmt.Println("您的等级为E级")
case 0 :
fmt.Println("您的等级为E级")
default :
fmt.Println("您的成绩有误")
}
}
- switch 后是一个表达式(即:常量值、变量、一个有返回值的函数等都可以)
- case 后面的表达式如果是一个常量值,则要求不能重复
- case 后的各个值的数据类型,必须和 switch 的表达式数据类型一致
- case 后面可以带多个值,使用逗号间隔
- case 后面不需要带 break
- default 语句不是必须的,位置也是随意的
- switch 后也可以不带表达式,当做 if 分支来使用
package main
import (
"fmt"
)
func main() {
var a int = 2
switch {
case a == 1 :
fmt.Println("a = 1")
case a == 2 :
fmt.Println("a = 2")
}
}
- switch 后也可以直接声明 / 定义一个变量,分号结束,不推荐
package main
import (
"fmt"
)
func main() {
switch b := 7; {
case b > 6 :
fmt.Println("b > 6")
case b <= 8 :
fmt.Println("b <= 6")
}
}
- switch 穿透,利用 fallthrough 关键字,如果在 case 语句块后增加 fallthrough,则会继续执行下一个 case,也叫 switch 穿透
循环结构
for循环
基本语法:
for (初始表达式; 布尔表达式; 迭代因子) {
循环体;
}
for 循环语句是支持迭代的一种通用结构,是最有效,最灵活的循环结构,for 循环在第一次反复之前要进行初始化,即执行初始表达式,随后,对布尔表达式进行判定,若判定结果为true,则执行循环体,否则,终止循环,最后在每一次反复的时候,进行某种形式的 “步进”,即执行迭代因子。
- 初始表达式设置循环变量的初始值
- 条件判断部分为任意布尔表达式
- 迭代因子控制循环变量的增减
for 循环在执行条件判断后,先执行循环体部分,在执行步进
for 循环结构的流程图如图所示
格式灵活:
package main
import "fmt"
func main() {
i := 1 // 变量的初始化
for i < 6 { // 条件表达式,判断条件
fmt.Println("你好,Golang") // 循环体
i++ // 迭代
}
}
死循环:
package main
import "fmt"
func main() {
for {
fmt.Println("你好, Golang")
}
}
for range 循环
package main
import "fmt"
func main() {
// 遍历字符串
var str string = "Hello Golang"
// 方式一
for i := 0; i < len(str); i++ {
fmt.Printf("%c \n", str[i])
}
// 方式二
for i, val := range str {
fmt.Printf("索引为:%d, 具体的值为:%c \n", i, val)
}
// 对 str 进行遍历,遍历的每个结果的索引值被 i 接收,每个结果的具体数值被 val 接收
}
break关键字
作用
package main
import "fmt"
func main() {
// 求 1-100 的和,当和第一次超过 300 的时候,停止程序
var sum int
for i := 1; i < 101; i++ {
sum += i
if sum > 300 {
// 停止正在执行的这个循环
break // 退出整个循环
}
}
fmt.Println(sum)
}
深入理解
package main
import "fmt"
func main() {
for i := 1; i < 6; i++ {
for j := 2; j < 5; j++ {
fmt.Printf("i: %v, j: %v \n", i, j)
if i == 2 && j == 2 {
break
}
}
}
}
标签的使用
package main
import "fmt"
func main() {
lable2:
for i := 1; i < 6; i++ {
for j := 2; j < 5; j++ {
fmt.Printf("i: %v, j: %v \n", i, j)
if i == 2 && j == 2 {
break lable2
}
}
}
}
总结:
- break 可以结束正在执行的循环,结束离它最近的循环(就近原则)
- 添加标签可以指定停止哪个循环
continue关键字
作用
package main
import "fmt"
func main() {
// 输出 1-100 中被 6 整除的数
for i := 1; i <= 100; i++ {
if i % 6 != 0 {
continue // 结束本次循环,继续下一次循环
}
fmt.Println(i)
}
}
同样 continue 也遵循就近原则,同时也可以和标签一同使用
goto关键字
goto 语句可以无条件地转移到程序中指定的行
package main
import "fmt"
func main() {
fmt.Println("你好, golang1")
fmt.Println("你好, golang2")
fmt.Println("你好, golang3")
goto lable
fmt.Println("你好, golang4")
fmt.Println("你好, golang5")
fmt.Println("你好, golang6")
lable:
fmt.Println("你好, golang7")
fmt.Println("你好, golang8")
fmt.Println("你好, golang9")
// 不推荐使用 goto, 以免造成程序流程的混乱
}
return关键字
package main
import "fmt"
func main() {
for i := 1; i < 101; i++ {
fmt.Println(i)
if i == 14 {
return // 结束当前的函数
}
}
// 不会输出这句话
fmt.Println("hello golang")
}
函数
基本语法
func 函数名 (形参列表) (返回值类型列表) {
执行语句……
return + 返回值列表
}
package main
import "fmt"
func main() {
// 求和
var n1 int = 10
var n2 int = 20
var sum int
sum += n1
sum += n2
fmt.Println(sum)
var n3 = Sum(10, 20)
fmt.Println(n3)
}
// 定义一个求和函数
func Sum(n1 int, n2 int) (int) {
var sum int
sum += n1
sum += n2
return sum
}
-
如果没有返回值,那么返回值类型列表不写就可以
-
如果返回值是一个,那么 () 可以省略不写
-
如果返回值是多个,那么返回值类型列表就写多个,同时用逗号分隔,有几个就接收几个,如果想要忽略某个返回值,用
_
接收即可 -
Golang中函数不支持重载
-
Golang中支持可变参数(函数带有可变数量的参数)
package main
import "fmt"
func main() {
test(11)
fmt.Println("--------")
test(11, 11)
fmt.Println("--------")
test(11, 11, 11, 11)
fmt.Println("--------")
}
// 定义一个函数,函数的参数数量可变
func test(args...int) {
// 函数内部处理可变参数的时候,将可变参数当中切片来处理
for i := 0; i < len(args); i++ {
fmt.Println(args[i])
}
}
- 在Golang中,基本数据类型和数组默认都是值传递的,即进行值拷贝,在函数内修改,不会影响到原来的值
package main
import "fmt"
func main() {
var num int = 10
test(num)
fmt.Println("main----", num)
}
func test(num int) {
num = 30
fmt.Println("test----", num)
}
- 以值传递方式的数据类型,如果希望在函数内的变量能修改函数外的变量,可以传入变量的地址,函数内以指针的方式操作变量,从效果来看类似引用传递
package main
import "fmt"
func main() {
var num int = 10
// 传入 num 的地址
test(&num)
fmt.Println("main----", num)
}
// 参数类型为指针
func test(num *int) {
// 对地址对应的变量进行改变数值
*num = 30
fmt.Println("test----", *num)
}
- 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了,通过该变量可以对函数调用
package main
import "fmt"
func test(num int) {
fmt.Println(num)
}
func main() {
// 函数也是一种数据类型,可以赋值给一个变量
var f = test
fmt.Printf("f对应的类型是: %T \n", f)
// 通过该变量可以对函数进行调用
f(10)
}
- 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用
package main
import "fmt"
func test(num int) {
fmt.Println(num)
}
func test2(num1 int, num2 float32, testFunc func(int)) {
fmt.Println("----- test2")
}
func main() {
// 函数也是一种数据类型,可以赋值给一个变量
var f = test // 该变量就是一个函数类型的变量
fmt.Printf("f对应的类型是: %T \n", f)
// 通过改变量可以对函数进行调用
f(10)
// 调用 test2 函数
test2(10, 3.14, f)
}
- 为了简化数据类型定义,Go 支持自定义数据类型
基本语法:type 自定义数据类型名 数据类型
可以理解为:相当于起了一个别名
例如:type myInt int ----》这是 myInt 就等价于 int 来使用了
例如:type mySum func(int, int) int ----》这时 mySum 就等价于一个函数类型 func(iint, int) int
package main
import "fmt"
func main() {
// 自定义数据类型
type myInt int
var n1 myInt = 30
fmt.Println("n1", n1)
var n2 int = 30
n2 = int(n1) // Go 编译识别的时候还是认为 myInt 和 int 不是一种数据类型
fmt.Println("n2", n2)
}
- 支持对函数返回值命名
package main
import "fmt"
func main() {
sum, sub := test(1,2)
sum1, sub1 := test2(1,2)
fmt.Println(sum, sum1, sub, sub1)
}
// 求两个数的和和差
func test(n1 int, n2 int) (int, int) {
result1 := n1 + n2
result2 := n1 - n2
return result1, result2
}
// 求两个数的和和差
func test2(n1 int, n2 int) (sum int, sub int) {
sum = n1 + n2
sub = n1 - n2
return
}
内存分析
package main
import "fmt"
func main() {
// 调用函数,交换 10 和 20
var n1 int = 10
var n2 int = 20
fmt.Printf("交换前的两个数:n1 = %v, n2 = %v \n", n1, n2)
exchangeNum(n1, n2)
fmt.Printf("交换后的两个数:n1 = %v, n2 = %v \n", n1, n2)
}
// 自定义函数,交换两个数
func exchangeNum(n1 int, n2 int) {
var t int
t = n1
n1 = n2
n2 = t
}
包
使用包的原因
我们不可能把所有的函数放在同一个源文件中,可以分门别类的把函数放在不同的源文件中
解决同名问题:两个人都想定义一个同名的函数,在同一个文件中是不可以定义相同名字的函数的,此时可以使用包来区分
包的深入理解
- 包的声明建议和所在文件夹同名
- main 包是程序的入口包,一般 main 函数会放在这个包下
- 如果有多个包,建议一次性导入格式如下:
import (
"fmt"
"gocode/godemo1/unit2/demo13/test" // 导入文件夹的路径
)
- 在函数调用的时候,前面要定位到所在的包
- 首字母大写,函数可以被其他包访问
- 一个目录下不能有重复的函数
- 包名和文件夹的名字可以不一样
- 一个目录下的同级文件归属一个包(同级别的源文件的包的声明必须一致)
- 可以给包起别名,取别名后,原来的包名就不能使用了
import (
"fmt"
// 给包起别名
test "gocode/godemo1/unit2/demo13/test" // 导入文件夹的路径
)
init 函数
初始化函数,可以用来进行一些初始化的操作
每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前被Go 调用
package main
import "fmt"
func init() {
fmt.Println("init函数被执行了")
}
func main() {
fmt.Println("main函数被执行了")
}
全局变量定义,init 函数,main 函数的执行流程如下
package main
import "fmt"
var num int = test()
func test() int {
fmt.Println("test函数被执行")
return 10
}
func init() {
fmt.Println("init函数被执行了")
}
func main() {
fmt.Println("main函数被执行了")
}
多个源文件都有 init 函数的时候执行流程如下
package main
import (
"fmt"
"gocode/godemo1/unit5/demo9/testutils"
)
var num int = test()
func test() int {
fmt.Println("test函数被执行")
return 10
}
func init() {
fmt.Println("init函数被执行了")
}
func main() {
fmt.Println("main函数被执行了")
fmt.Println("Age =", testutils.Age, "Sex =", testutils.Sex, "Name =", testutils.Name)
}
package testutils
import "fmt"
var Age int
var Sex string
var Name string
func init() {
fmt.Println("testutils中的 init函数被执行了")
Age = 19
Sex = "女"
Name = "李丽"
}
匿名函数
Go 支持匿名函数,如果我们某个函数只希望使用一次,可以考虑使用匿名函数
匿名函数语法如下:
在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次
package main
import "fmt"
func main() {
// 定义匿名函数 定义的同时调用
result := func (num1 int, num2 int) int {
return num1 + num2
}(10, 20)
fmt.Println(result)
}
将匿名函数赋值给变量,再通过该变量来调用匿名函数
package main
import "fmt"
func main() {
// 将匿名函数赋给一个变量,这个变量实际就是函数类型的变量
// sum 等价与匿名函数
sum := func (num1 int, num2 int) int {
return num1 + num2
}
// 调用
result := sum(10, 20)
fmt.Println(result)
}
将匿名函数给一个全局变量可以让匿名函数在整个程序中有效
闭包
闭包就是一个函数和与其相关的引用环境组合的一个整体
案例展示
package main
import "fmt"
func getSum() func (int) int {
var sum int = 0
return func (num int) int {
sum += num
return sum
}
}
// 闭包是返回的匿名函数 + 匿名函数以外的变量num
func main() {
f := getSum()
fmt.Println(f(1)) // 1
fmt.Println(f(2)) // 3
}
匿名函数中引用的那个变量会一直保存在内存中,可以一直使用
闭包的本质
闭包的本质依旧是一个匿名函数,只是这个函数引入外界的变量 / 参数
匿名函数 + 引用的变量 / 参数 = 闭包
闭包的特点
返回的是一个匿名函数,但是这个匿名函数引用到函数外的变量 / 参数,因此这个匿名函数就和变量 / 参数形成一个整体,构成闭包
闭包中使用的变量 / 参数会一直保存在内存中,所以会一直使用,意味着闭包不能随便用
闭包的使用场景
闭包可以保留上次引用的某个值,我们传入一次就可以反复使用了
defer 关键字
作用
在函数中,程序员经常要创建资源,为了在函数执行完毕后,及时的释放资源,Go 的设计者提供了 defer 关键字
案例展示
package main
import "fmt"
func main() {
fmt.Println(add(30, 60))
}
func add(num1 int, num2 int) int {
defer fmt.Println("num1 =", num1)
defer fmt.Println("num2 =", num2)
var sum int = num1 + num2
fmt.Println("sum =", sum)
return sum
}
代码改变一下,在看效果如何
package main
import "fmt"
func main() {
fmt.Println(add(30, 60))
}
func add(num1 int, num2 int) int {
defer fmt.Println("num1 =", num1)
defer fmt.Println("num2 =", num2)
num1 += 90
num2 += 50
var sum int = num1 + num2
fmt.Println("sum =", sum)
return sum
}
遇到 defer 关键字,会将后面的代码语句压入栈中,也会将相关的值同时拷贝到栈中,不会随着函数后面的变化而变化
defer 应用场景
比如你想关闭某个使用的资源,将关闭操作写在 defer 后面,比较放心,不用考虑关闭时机
系统函数
字符串相关函数
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
// 统计字符串长度
str := "你好, Golang"
fmt.Println(len(str))
// 对字符串进行遍历方式一
for _, value := range str {
fmt.Printf("%c \n", value)
}
// 对字符串进行遍历方式二
r := []rune(str)
for i := 0; i < len(r); i++ {
fmt.Printf("%c \n", r[i])
}
// 字符串转整数
num1, _ := strconv.Atoi("666")
fmt.Printf("num1的类型是: %T, num的值是: %v \n", num1, num1)
// 整数转字符串
str1 := strconv.Itoa(88)
fmt.Printf("str1的类型是: %T, str1的值是: %v \n", str1, str1)
// 统计字符串中有几个指定的子串
count := strings.Count("golangfdgolang", "golang")
fmt.Println(count)
// 不区分大小写的字符串比较
flag := strings.EqualFold("go", "Go")
fmt.Println(flag)
// 区分大小写的字符串比较
fmt.Println("hello" == "Hello")
// 返回子串在字符串第一次出现的索引值,如果没有返回 -1
index := strings.Index("golangfdgolang", "golang")
fmt.Println(index)
// 字符串的替换
str2 := strings.Replace("golangfdgolang", "golang", "Go", -1)
fmt.Println(str2)
// 按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组
arr := strings.Split("go-python-java", "-")
fmt.Println(arr)
// 将字符串的字母进行大小写的转换
fmt.Println(strings.ToLower("Go"))
fmt.Println(strings.ToUpper("go"))
// 字符串左右两边的空格去掉
fmt.Println(strings.TrimSpace(" go and java "))
// 将字符串左右两边指定的字符去掉
fmt.Println(strings.Trim("~golang~", "~"))
// 将字符串右边指定的字符去掉
fmt.Println(strings.TrimRight("~golang~", "~"))
// 将字符串左边指定的字符去掉
fmt.Println(strings.TrimLeft("~golang~", "~"))
// 判断字符串是否以指定的字符串开头
flag1 := strings.HasPrefix("http://java.sun.com/jsp/jstl/fmt", "http")
fmt.Println(flag1)
// 判断字符串是否以指定的字符串结束
flag2 := strings.HasSuffix("demo.png", ".png")
fmt.Println(flag2)
}
时间日期相关函数
package main
import (
"fmt"
"time"
)
func main() {
// 获取当前时间
now := time.Now()
fmt.Printf("%v, 对应的类型为: %T \n", now, now)
fmt.Println(now)
fmt.Printf("年: %v \n", now.Year())
fmt.Printf("月: %v \n", now.Month())
fmt.Printf("月: %v \n", int(now.Month()))
fmt.Printf("日: %v \n", now.Day())
fmt.Printf("时: %v \n", now.Hour())
fmt.Printf("分: %v \n", now.Minute())
fmt.Printf("秒: %v \n", now.Second())
// 将日期以年月日时分秒按照格式输出为字符串
fmt.Println("----------------")
// 将字符串直接输出
fmt.Printf("当前年月日: %d-%d-%d 时分秒: %d:%d:%d \n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
// 得到字符串以便后续使用
dateStr := fmt.Sprintf("当前年月日: %d-%d-%d 时分秒: %d:%d:%d \n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
fmt.Println(dateStr)
// 这个参数字符串的各个数字必须是固定的,必须这样写
dateStr2 := now.Format("2006/01/02 15/04/05")
fmt.Println(dateStr2)
}
内置函数
package main
import "fmt"
func main() {
// 定义一个字符串
str := "golang"
// 统计字符串的长度,按字节进行统计
fmt.Println(len(str))
// new 是分配内存,new 函数返回值是对应类型的指针
num := new(int)
fmt.Printf("num的类型是: %T, num的值是: %v, num的地址是: %v, num指针指向的值是: %v", num, num, &num, *num)
}
错误处理
defer + recover 机制处理错误
package main
import "fmt"
func main() {
test()
fmt.Println("上面的除法操作执行成功")
fmt.Println("正常执行下面的逻辑")
}
func test() {
// 利用 defer + recover 来捕获错误
defer func() {
// 调用recover内置函数,可以捕获错误
err := recover()
// 如果没有捕获错误,返回值为 nil 值
if err != nil {
fmt.Println("错误已经捕获")
fmt.Println("err是:", err)
}
}()
num1 := 10
num2 := 0
result := num1 / num2
fmt.Println(result)
}
自定义错误
package main
import (
"fmt"
"errors"
)
func main() {
err := test()
if err != nil {
fmt.Println("自定义错误:", err)
}
fmt.Println("上面的除法操作执行成功")
fmt.Println("正常执行下面的逻辑")
}
func test() (err error) {
num1 := 10
num2 := 0
if num2 == 0 {
// 抛出错误
return errors.New("除数不能为0")
} else {
result := num1 / num2
fmt.Println(result)
return nil
}
}
有时,程序出现错误后,后面的代码没有必要执行,想让程序中断,退出程序
package main
import (
"fmt"
"errors"
)
func main() {
err := test()
if err != nil {
fmt.Println("自定义错误:", err)
panic(err)
}
fmt.Println("上面的除法操作执行成功")
fmt.Println("正常执行下面的逻辑")
}
func test() (err error) {
num1 := 10
num2 := 0
if num2 == 0 {
// 抛出错误
return errors.New("除数不能为0")
} else {
result := num1 / num2
fmt.Println(result)
return nil
}
}
数组
数组可以存储相同类型的数据
入门案例
package main
import "fmt"
func main() {
// 给出 5 个学生的成绩,求出总和和平均数
// 定义一个数组
var scores [5]int
// 将成绩存入数组
scores[0] = 95
scores[1] = 91
scores[2] = 39
scores[3] = 60
scores[4] = 21
// 定义一个变量接收成绩的和
var sum int
for i := 0; i < len(scores); i++ {
sum += scores[i]
}
// 平均数
avg := sum / len(scores)
// 输出
fmt.Printf("总和为: %v, 平均数为: %v", sum, avg)
}
内存分析
package main
import "fmt"
func main() {
// 声明数组
var arr [3]int16
// 获取数组的长度
fmt.Println(len(arr))
// 打印数组
fmt.Println(arr)
// 证明 arr 中存储的是地址值
fmt.Printf("arr的地址值为: %p \n", &arr)
// 第一个空间的地址
fmt.Printf("arr的地址值为: %p \n", &arr[0])
// 第二个空间的地址
fmt.Printf("arr的地址值为: %p \n", &arr[1])
// 第三个空间的地址
fmt.Printf("arr的地址值为: %p \n", &arr[2])
}
赋值内存分析
优点:访问 / 查询 / 读取速度快
数组的遍历
package main
import "fmt"
func main() {
// 给出 5 个学生的成绩,求出总和和平均数
// 定义一个数组
var scores [5]int
// 将成绩存入数组
for i := 0; i < len(scores); i++ {
fmt.Printf("请录入第%d个学生的成绩", i+1)
fmt.Scanln(&scores[i])
}
// 展示每个学生的成绩
// 数组的遍历方式一
for i := 0; i < len(scores); i++ {
fmt.Printf("第%d个学生的成绩为: %v \n", i + 1, scores[i])
}
fmt.Println("---------------")
// 数组的遍历方式二
for index, val := range scores {
fmt.Printf("第%d个学生的成绩为: %v \n", index + 1, val)
}
}
数组的初始化方式
package main
import "fmt"
func main() {
// 第一种
var arr1 [3]int = [3]int{3, 6, 9}
fmt.Println(arr1)
// 第二种
var arr2 = [3]int{1, 4, 7}
fmt.Println(arr2)
// 第三种
var arr3 = [...]int{4, 5, 6, 7}
fmt.Println(arr3)
// 第四种
var arr4 = [...]int{2: 66, 0: 33, 1: 99, 3: 88}
fmt.Println(arr4)
}
二维数组的定义和内存分析
package main
import "fmt"
func main() {
// 二维数组
var arr [2][3]int16
fmt.Println(arr)
fmt.Printf("arr的地址是: %p \n", &arr)
fmt.Printf("arr[0]的地址是: %p \n", &arr[0])
fmt.Printf("arr[0][0]的地址是: %p \n", &arr[0][0])
fmt.Printf("arr[1]的地址是: %p \n", &arr[1])
fmt.Printf("arr[1][0]的地址是: %p \n", &arr[1][0])
}
二维数组的遍历
package main
import "fmt"
func main() {
var arr = [3][3]int{{1, 4, 7}, {2, 5, 8}, {3, 6, 9}}
fmt.Println(arr)
// 方式一:普通 for 循环
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[i]); j++ {
fmt.Print(arr[i][j], "\t")
}
fmt.Println()
}
// 方式二:for range 循环
for key, val := range arr {
for k, v := range val {
fmt.Printf("arr[%v][%v] = %v \t", key, k, v)
}
fmt.Println()
}
}
切片
入门案例
切片(slice)是 Golang 中一种特有的数据类型
数组有特定的用处,但是数组长度固定不可变,所有在 Go 语言的代码里并不是特别常见,相对的切片却是随处可见的,切片是一种建立在数组类型之上的抽象,它构建在数组之上并且提供更强大的能力和便捷。
切片是对数组一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引包括的项不包括在切片内,切片提供了一个相关数组的动态窗口
package main
import "fmt"
func main() {
// 定义数组
var arr [6]int = [6]int{3, 6, 9, 1, 4, 7}
// 定义一个切片
var slice []int = arr[1: 3]
// 输出数组
fmt.Println(arr)
// 输出切片
fmt.Println(slice)
// 获取切片元素的个数
fmt.Println(len(slice))
// 获取切片的容量
fmt.Println(cap(slice))
}
切片的内存分析
切片的定义
package main
import "fmt"
func main() {
// 方式一
// 定义数组
var arr [6]int = [6]int{3, 6, 9, 1, 4, 7}
// 定义一个切片
var slice []int = arr[1: 3]
fmt.Println(slice)
// 方式二
// 切片对应的类型
// 切片的长度
// 切片的容量
// make 底层创建一个数组,对外不可见,所以不可以直接操作这个数组,要通过 切片 去间接的访问各个元素
slice1 := make([]int, 4, 20)
fmt.Println(slice1)
slice1[0] = 66
slice1[1] = 88
slice1[2] = 99
slice1[3] = 33
fmt.Println(slice1)
// 方式三
// 数组对外不可见
slice2 := []int{1, 4, 7}
fmt.Println(slice2)
}
切片的遍历
package main
import "fmt"
func main() {
// 定义数组
var arr [6]int = [6]int{3, 6, 9, 1, 4, 7}
// 定义一个切片
var slice []int = arr[1: 6]
fmt.Println(slice)
// 方式一
for i := 0; i < len(slice); i++ {
fmt.Printf("slece[%v] = %v \t", i, slice[i])
}
fmt.Println("\n----------")
// 方式二
for i, val := range slice {
fmt.Printf("slece[%v] = %v \t", i, val)
}
}
切片的细节
- 切片可以动态增长
package main
import "fmt"
func main() {
// 定义数组
var arr [6]int = [6]int{3, 6, 9, 1, 4, 7}
// 定义一个切片
var slice []int = arr[1: 6]
fmt.Println(slice)
// 末尾追加元素,创建一个新数组,将老数组中的元素复制到新数组中,在新数组中追加新数据
// 底层数组指向新数组
// 往往我们在使用追加的时候其实是想要在slice上直接追加
// 底层新数组不能访问
slice = append(slice, 88, 500)
fmt.Println(slice)
// 末尾加切片
slice2 := []int{99, 33}
slice = append(slice, slice2...)
fmt.Println(slice)
}
切片的拷贝
package main
import "fmt"
func main() {
// 定义数组
var arr [6]int = [6]int{3, 6, 9, 1, 4, 7}
// 定义一个切片
var slice []int = arr[1: 6]
fmt.Println(slice)
var slice_1 = make([]int, 5)
// 拷贝
// 将slice中对应数组中元素内容复制到slice_1中对应的数组中
copy(slice_1, slice)
fmt.Println(slice_1)
}
映射
映射(map)Go语言中内置的一种数据类型,它将键值对相关联,我们可以通过键 key 来获取对应的值 value,类似其他语言的集合
key value的类型:bool、数字、string、指针、channel、还可以是只包含前面几个类型的接口、结构体、数组
key 通常为 int、string 类型,value 通常为数字(整数、浮点数)、string、map、结构体
key:不可以是 slice、map、function
入门案例
package main
import "fmt"
func main() {
// 定义map变量
// 只声明map没有分配内存空间
var a map[int]string
// 必须通过make函数进行初始化,才会分配空间
a = make(map[int]string, 10) // map可以存放 10 个键值对
// 将键值对存入map中去
a[20202323] = "张三"
a[20202324] = "李四"
a[20202325] = "王五"
fmt.Println(a)
}
- map 在使用时一定要初始化
- map 的 key-value 是无序的
- key 是不可以重复的,如果重复,会覆盖前面的 value 值
- 值(value)是可以重复的
map三种创建方式
package main
import "fmt"
func main() {
// 方式一
// 定义map变量
var a map[int]string
// 初始化
// 可以存放10个键值对
a = make(map[int]string, 10)
// 将键值对存入map中去
a[20202323] = "张三"
a[20202324] = "李四"
a[20202325] = "王五"
// 输出
fmt.Println(a)
// 方式二
b := make(map[int]string)
// 将键值对存入map中去
b[20202323] = "张三"
b[20202324] = "李四"
b[20202325] = "王五"
// 输出
fmt.Println(b)
// 方式三
c := map[int]string{
20202323: "张三",
20202324: "李四",
20202325: "王五",
}
fmt.Println(c)
}
map的增删改查操作
package main
import "fmt"
func main() {
// 创建map变量
b := make(map[int]string)
// 将键值对存入map中去(增加)
b[20202323] = "张三"
b[20202324] = "李四"
b[20202325] = "王五"
fmt.Println(b)
// 修改操作
b[20202323] = "赵云"
fmt.Println(b)
// 删除操作
delete(b, 20202324)
fmt.Println(b)
// 查询操作
val, flag := b[20202323]
fmt.Println(val)
fmt.Println(flag)
// 清空操作
b = make(map[int]string)
fmt.Println(b)
}
map的遍历
package main
import "fmt"
func main() {
a := make(map[int]string)
a[20202323] = "张三"
a[20202324] = "李四"
a[20202325] = "王五"
// 获取长度
fmt.Println(len(a))
// 遍历
for key, val := range a {
fmt.Println(key, val)
}
fmt.Println("--------------")
b := make(map[string]map[int]string)
b["班级1"] = make(map[int]string, 3)
b["班级1"][20202323] = "张三"
b["班级1"][20202324] = "李丽"
b["班级1"][20202325] = "王宇"
b["班级2"] = make(map[int]string, 3)
b["班级2"][20202326] = "久林"
b["班级2"][20202327] = "王源"
b["班级2"][20202328] = "宋林"
fmt.Println(b)
for key, val := range b {
fmt.Println(key)
for k, v := range val {
fmt.Println(k, v)
}
}
}
面向对象
面向对象的引入
package main
import "fmt"
func main() {
// 第一名老师
// 姓名
var name string = "蒋志刚"
// 年龄
var age int = 33
// 性别
var sex string = "女"
// 第二名老师
// 姓名
var name1 string = "赵曦"
// 年龄
var age1 int = 23
// 性别
var sex1 string = "女"
// 输出第一位老师
fmt.Println(name, age, sex)
// 输出第二位老师
fmt.Println(name1, age1, sex1)
}
这样,只要有一名老师就要定义3个变量,是相对麻烦的,因此面向对象思想呼之欲出
结构体的定义
package main
import "fmt"
// 定义老师的结构体
type Teacher struct{
// 变量名字大写外界可以访问这个属性
Name string
Age int
School string
}
func main() {
// 创建老师结构体实例
var t1 Teacher
fmt.Println(t1)
t1.Name = "赵龙"
t1.Age = 22
t1.School = "山东大学"
fmt.Println(t1)
}
结构体内存分析
结构体实例创建方式
package main
import "fmt"
// 定义老师的结构体
type Teacher struct{
// 变量名字大写外界可以访问这个属性
Name string
Age int
School string
}
func main() {
// 创建老师结构体实例
// 方式一
var t1 Teacher
t1.Name = "赵龙"
t1.Age = 22
t1.School = "山东大学"
fmt.Println(t1)
// 方式二
// 必须依照顺序给全部字段赋值
var t2 Teacher = Teacher{"赵龙", 22, "山东大学"}
fmt.Println(t2)
// 方式三
// 方式三使用 new 关键字来创建实例对象
// 返回一个指针
var t3 *Teacher = new(Teacher)
// 可以对赋值进行简写
t3.Name = "王五"
t3.Age = 33
t3.School = "清华大学"
fmt.Println(*t3)
// 方式四,同样可以直接初始化
// var t4 *Teacher = &Teacher{"王五", 33, "清华大学"}
var t4 *Teacher = &Teacher{}
t4.Name = "王五"
t4.Age = 33
t4.School = "清华大学"
fmt.Println(*t4)
}
结构体之间的转换
结构体是用户单独定义的类型,和其他类型进行转换时需要有完全相同的字段(名字,个数和类型)
package main
import "fmt"
type Person struct{
Name string
Age int
}
type Teacher struct{
Person
Class string
}
type Student struct{
Person
Class string
}
func main() {
var p Person
var s1 Student
var s Student
var t Teacher
// 字段相同,可以强制转换
s = Student(t)
// 结构体嵌套
s1 = Student{p, ""}
fmt.Println(s)
fmt.Println(s1)
}
package main
import "fmt"
type Person struct{
Name string
Age int
}
type Per Person
func main() {
var p1 Person
var p2 Per
// 必须强制转换
p1 = Person(p2)
fmt.Println(p1)
}
方法的引入和案例
方法是一种特殊的函数,它与特定的类型相关联,可以直接访问该类型的字段和方法,案例如下:
package main
import "fmt"
// 定义一个人的结构体
type Person struct{
// 姓名
Name string
}
// 绑定一个方法
func (p Person) test() {
fmt.Println(p.Name)
}
func main() {
// 创建Person 实例对象
var p Person
p.Name = "赵云"
p.test()
fmt.Println(p)
}
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式。如果程序员希望在方法中改变结构体变量的值,可以通过结构体指针的方式来处理
package main
import "fmt"
// 定义一个人的结构体
type Person struct{
// 姓名
Name string
}
// 绑定一个方法
func (p *Person) test() {
(*p).Name = "张飞"
fmt.Println(p.Name)
}
func main() {
// 创建Person 实例对象
var p Person
p.Name = "赵云"
(&p).test()
fmt.Println(p)
}
可以简化书写
package main
import "fmt"
// 定义一个人的结构体
type Person struct{
// 姓名
Name string
}
// 绑定一个方法
func (p *Person) test() {
p.Name = "张飞"
fmt.Println(p.Name)
}
func main() {
// 创建Person 实例对象
var p Person
p.Name = "赵云"
p.test()
fmt.Println(p)
}
- 方法的访问范围控制的规则和函数一样,方法名首字母小写,只能在本包访问,方法首字母大写可以在本包和其他包访问
- 如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出
package main
import "fmt"
type Student struct{
Name string
Age int
}
func (s *Student) String() (str string) {
str = fmt.Sprintf("Name = %v, Age = %v", s.Name, s.Age)
return
}
func main() {
stu := Student{
Name : "龙傲天",
Age : 23,
}
// 传入地址: 如果绑定了String方法就会自动调用
fmt.Println(&stu)
}
方法和函数的区别
-
绑定指定类型:
方法:需要绑定指定数据类型
函数:不需要绑定数组类型
-
调用方式:
函数的调用方式: 函数名(实参列表)
方法的调用方式:变量.方法名(实参列表)
-
对于函数来说,参数类型对应是什么就要传入什么
-
对于方法来说,接收者为值类型,可以传入指针类型。接收者为指针类型,可以传入值类型
创建结构体实例时指定字段值
package main
import "fmt"
type Student struct{
Name string
Age int
}
func main() {
// 方式一: 按照顺序赋值操作
var s1 = Student{"小王", 19}
fmt.Println(s1)
// 方式二: 按照指定类型
var s2 = Student{
Name: "李丽丽",
Age: 20,
}
fmt.Println(s2)
// 方式三
var s3 = &Student{"明明", 18}
fmt.Println(*s3)
// 方式四
var s4 = &Student{
Name: "李丽丽",
Age: 20,
}
fmt.Println(*s4)
}
跨包创建结构体实例
Student.go文件内容如下:
package model
// 学生结构体定义
type Student struct{
// 姓名
Name string
// 年龄
Age int
}
main.go文件如下:
package main
import (
"fmt"
"gocode/godemo1/unit10/demo9/model"
)
func main() {
// 跨包创建结构体实例
// var s model.Student = model.Student{"李丽",19}
s := model.Student{"李丽", 19}
fmt.Println(s)
}
上面的代码中,定义结构体时首字母大写以方便其他包的访问,但是,如果首字母小写,则无权被其他包访问,为了解决此问题我们引入工厂模式
工厂模式
Student.go的文件改动如下:
package model
// 学生结构体定义
type student struct{
// 姓名
Name string
// 年龄
Age int
}
// 工厂模式
func NewStudent(name string, age int) (stu *student) {
stu = &student{name, age}
return
}
main.go文件改动如下:
package main
import (
"fmt"
"gocode/godemo1/unit10/demo9/model"
)
func main() {
// 跨包创建结构体实例
// var s model.Student = model.Student{"李丽",19}
s := model.NewStudent("李丽", 19)
fmt.Println(*s)
}
封装
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数组被保护在内部,程序的其他包只有通过被授权的操作方法,才能对字段进行操作。
封装的好处:隐藏实现细节、可以对数组进行验证,保证安全合理
在Golang中如何实现封装:
- 建议将结构体,字段(属性)的首字母小写(其他包不能使用,实际开发不小写也可能,因为封装没那么严格)
- 给结构体所在的包提供一个工厂模式的函数,首字母大写
- 提供一个首字母大写的 Set 方法,用于对属性判断并赋值
- 提供一个首字母大写的 Get 方法,用于获取属性的值
代码案例如下:
Person.go 文件如下:
package model
import (
"fmt"
)
// 首字母小写
type person struct{
name string
age int
}
// 定义 Set 和 Get 方法对字段进行封装
func (p *person) SetName(name string) {
p.name = name
}
func (p *person) GetName() string {
return p.name
}
func (p *person) SetAge(age int) {
if age >= 0 && age < 150 {
p.age = age
} else {
fmt.Println("输入有误")
}
}
func (p *person) GetAge() int {
return p.age
}
// 工厂模式
func NewPerson(name string, age int) (per *person) {
per = &person{
name: name,
age: age,
}
return
}
main.go 文件如下:
package main
import (
"fmt"
"gocode/godemo1/unit10/demo10/model"
)
func main() {
// 创建一个Person结构体实例
p := model.NewPerson("李丽", 19)
fmt.Println(*p)
p.SetName("李雪")
p.SetAge(18)
fmt.Println(*p)
}
继承
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法,其他结构体不需要定义这些属性和方法,只需要嵌套一个匿名结构体即可,即实现继承特性。
入门案例
package main
import (
"fmt"
)
// 定义动物的结构体
type Animal struct{
Age int
Weight float64
}
func (an *Animal) Shout() {
fmt.Println("我可以大声说话了")
}
func (an *Animal) ShowInfo() {
fmt.Printf("动物的年龄是: %v, 动物的体重是: %v \n", an.Age, an.Weight)
}
// 定义猫的结构体
type Cat struct{
// 复用
Animal
}
// 特有方法
func (c *Cat) Scratch() {
fmt.Println("我是小猫,我可以挠人")
}
func main() {
// 创建猫的结构体实例
cat := &Cat{}
cat.Animal.Age = 3
cat.Animal.Weight = 10.7
cat.Animal.Shout()
cat.Animal.ShowInfo()
cat.Scratch()
}
继承注意事项
- 结构体可以使用嵌套匿名结构体所有的字段和方法,即首字母大写或者小写的字段、方法都可以使用
- 匿名结构体字段访问可以简化
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如果希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
package main
import (
"fmt"
)
// 定义动物的结构体
type Animal struct{
Age int
Weight float64
}
func (an *Animal) Shout() {
fmt.Println("我可以大声说话了")
}
func (an *Animal) ShowInfo() {
fmt.Printf("动物的年龄是: %v, 动物的体重是: %v \n", an.Age, an.Weight)
}
// 定义猫的结构体
type Cat struct{
// 复用
Animal
Age int
}
func (c *Cat) ShowInfo() {
fmt.Printf("动物的年龄是: %v, 动物的体重是: %v \n", c.Age, c.Weight)
}
// 特有方法
func (c *Cat) Scratch() {
fmt.Println("我是小猫,我可以挠人")
}
func main() {
// 创建猫的结构体实例
cat := &Cat{}
cat.Weight = 9.4 // cat.Animal.Weight的简写
cat.Age = 10 // 就近原则
cat.Animal.Age = 20 // 通过匿名结构体名来区分
cat.ShowInfo() // 就近原则
cat.Animal.ShowInfo() // 通过匿名结构体名来区分
}
-
Golang中支持多继承,如一个结构体嵌套了 多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多继承,为了保证代码的简洁,建议大家尽量不使用多重继承
-
结构体的匿名字段可以是基本数据类型
- 嵌套匿名结构体后,亦可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
- 嵌入匿名结构体的指针也是可以的
- 结构体的字段可以是结构体类型的(组合模式)
接口
入门案例
package main
import "fmt"
// 定义接口
type SayHello interface{
// 声明要实现的方法
sayHello()
}
type Chinese struct{
}
// 实现方法
func (chinese *Chinese) sayHello() {
fmt.Println("你好")
}
type American struct{
}
// 实现方法
func (american *American) sayHello() {
fmt.Println("Hi")
}
// 定义一个函数
func greet(s SayHello) {
s.sayHello()
}
func main() {
c := &Chinese{}
a := &American{}
greet(c)
greet(a)
}
接口中可以定义一组方法,但不需要实现,不需要方法体,并且接口中不能包含任何变量,到某个自定义类型要使用的时候,再根据具体情况把这些方法具体实现出来
实现接口要实现所有的方法才是实现
Golang中实现接口是基于方法的,不是基于接口
接口的目的是为了定义规范,具体由别人来实现即可
接口注意事项
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
- 一个自定义类型可以实现多个接口
package main
import "fmt"
type AInterface interface{
a()
}
type BInterface interface{
b()
}
type Stu struct{
}
func (s Stu) a() {
fmt.Println("aaaa")
}
func (s Stu) b() {
fmt.Println("bbbb")
}
func main() {
var s Stu
var a AInterface = s
var b BInterface = s
a.a()
b.b()
}
- 一个接口可以继承多个别的接口,这时如果要实现这个接口则需将父接口一并实现
package main
import "fmt"
type CInterface interface{
c()
}
type BInterface interface{
b()
}
type AInterface interface{
BInterface
CInterface
a()
}
type Stu struct{
}
func (s Stu) a() {
fmt.Println("aaaa")
}
func (s Stu) b() {
fmt.Println("bbbb")
}
func (s Stu) c() {
fmt.Println("cccc")
}
func main() {
var s Stu
var a AInterface = s
a.a()
a.b()
a.c()
}
-
接口类型默认是一个指针类型,如果没有对接口初始化就使用,那么会输出nil
-
空接口没有任何方法,所有可以理解为所有类型都实现了空接口,也可以理解为我们可以把任何一个变量赋值给空接口
断言
什么是断言
在Go语言里面有一个语法,可以直接判断是否是该类型的变量:value, ok = element.(T)
,这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型
案例展示
package main
import "fmt"
// 定义接口
type SayHello interface {
sayHello()
}
type Chinese struct {
name string
}
// 实现方法
func (chinese *Chinese) sayHello() {
fmt.Println("你好")
}
// 特有方法
func (chinese *Chinese) Say() {
fmt.Println("我是中国人")
}
type American struct {
name string
}
// 实现方法
func (american *American) sayHello() {
fmt.Println("Hi")
}
// 定义一个函数
func greet(s SayHello) {
s.sayHello()
// 断言:检查是否由 Chinese 实现的 SayHello
if chinese, ok := s.(*Chinese); ok {
chinese.Say()
}
}
func main() {
c := &Chinese{}
a := &American{}
greet(c)
greet(a)
}
上面的代码详细说明了断言语法与用途,而下面的代码则更加实用
package main
import "fmt"
// 定义接口
type SayHello interface {
sayHello()
}
type Chinese struct {
name string
}
// 实现方法
func (chinese *Chinese) sayHello() {
fmt.Println("你好")
}
// 特有方法
func (chinese *Chinese) Say() {
fmt.Println("我是中国人")
}
type American struct {
name string
}
// 实现方法
func (american *American) sayHello() {
fmt.Println("Hi")
}
// 特有方法
func (american *American) Disco() {
fmt.Println("disco")
}
// 定义一个函数
func greet(s SayHello) {
s.sayHello()
switch s.(type) {// type属于GO语言关键字
case *Chinese:
ch := s.(*Chinese)
ch.Say()
case *American:
am := s.(*American)
am.Disco()
}
}
func main() {
c := &Chinese{}
a := &American{}
greet(c)
greet(a)
}
文件操作
文件的打开和关闭
package main
import (
"fmt"
"os"
)
func main() {
// 打开文件
file, err := os.Open("d:/text.txt")
if err == nil {
// 没有出错,输出文件
fmt.Printf("文件=%v \n", file)
} else {
// 出错
fmt.Println("文件打开出错, 对应的错误为: ", err)
}
// 关闭文件
err2 := file.Close()
if err2 == nil {
fmt.Println("关闭成功")
} else {
fmt.Println("关闭失败")
}
}
IO流的引入
读取文件(输入流)
读取文件的内容并显示在终端(使用ioutil一次将整个文件读入到内存中),这种方式适用于文件不大的情况,案例如下:
package main
import (
"fmt"
"io/ioutil"
)
func main() {
// 备注:在下面的程序中不需要进行 Open/Close 操作,因为文件的打开和关闭操作被封装在ReadFil函数内部了
content, err := ioutil.ReadFile("d:/text.txt")// 返回内容为 []byte, err
if err != nil {
fmt.Println("读取错误,错误为:", err)
}
// 如果读取成功将内容显示在终端即可
fmt.Printf("%v \n", string(content))
}
读取文件的内容并显示在终端(带缓冲区的方式),适合读取比较大的文件,案例如下:
package main
import (
"fmt"
"os"
"bufio"
"io"
)
func main() {
// 打开文件
file, err := os.Open("d:/text.txt")
if err != nil {// 打开失败
fmt.Println("文件打开失败, err = ", err)
}
// 当函数退出时,让file关闭,防止内存泄漏
defer file.Close()
// 创建一个流
reader := bufio.NewReader(file)
// 读取操作
for {
// 读取到一个换行就结束
str, err1 := reader.ReadString('\n')
if err1 == io.EOF {
break
}
fmt.Println(str)
}
fmt.Println("文件读取成功,且读取完毕")
}
写入文件(输出流)
package main
import (
"fmt"
"os"
"bufio"
)
func main() {
// 写入文件操作
// 打开文件
file, err := os.OpenFile("d:/text.txt", os.O_RDWR | os.O_APPEND | os.O_CREATE, 0666)
if err != nil {
fmt.Println("文件打开失败, err = ", err)
return
}
// 及时关闭文件
defer file.Close()
// 写入文件
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString("你好, 马上兵\n")
}
// 刷新数据
writer.Flush()
}
复制文件
package main
import (
"fmt"
"io/ioutil"
)
func main() {
// 定义源文件
file1Path := "d:/text.txt"
// 定义目标文件
file2Path := "d:/go.txt"
// 对文件进行读取
content, err := ioutil.ReadFile(file1Path)
if err != nil {// 文件读取失败
fmt.Println("文件读取失败, 错误为: ", err)
}
// 写出文件
err = ioutil.WriteFile(file2Path, content, 0666)
if err != nil {
fmt.Println("文件写出失败, 错误为: ", err)
}
}
协程和管道
程序,进程,线程,协程
程序(program)
是为完成特定任务,用某种语言编写的一组指令的集合,是一段静态代码(程序是静态的)
进程(process)
是程序的一次执行过程,正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域(进程是动态的)是一个动的过程,进程的生命周期:有它自身的产生,存在和消亡的过程
线程(thread)
进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的。
协程(coroutine)
又称为微线程,纤程,协程是一种用户态的轻量级线程
作用:在执行A函数的时候,可以随时中断,去执行B函数,然后中断继续执行A函数(可以自动切换),注意这一切换过程并不是函数调用(没有调用语句),过程很像多线程,然而协程中只有一个线程在执行(协程的本质是一个单线程)
协程入门案例
请编写一个程序完成如下功能:
在主线程中开启一个协程,该协程每隔一秒输出"hello golang"
在主线程中也每隔一秒输出"hello msb",输出10次后退出程序
要求主线程和协程同时进行
package main
import (
"fmt"
"strconv"
"time"
)
// 协程函数
func test() {
for i := 0; i < 10; i++ {
fmt.Println("hello golang" + strconv.Itoa(i))
// 阻塞一秒
time.Sleep(time.Second)
}
}
func main() {// 主线程
go test()// 开启一个协程
for i := 0; i < 10; i++ {
fmt.Println("hello msb" + strconv.Itoa(i))
// 阻塞一秒
time.Sleep(time.Second)
}
}
主线程和协程执行流程
主死从随:主线程死亡则程序结束与协程无关
启动多个协程
package main
import (
"fmt"
"time"
"strconv"
)
func main() {
// 启动多个协程
// 使用匿名函数,直接调用匿名函数
for i := 1; i <= 5; i++ {
go func (n int) {
fmt.Println("协程" + strconv.Itoa(n))
}(i)
}
// 阻塞2秒
time.Sleep(time.Second * 2)
}
使用WaitGroup控制协程退出
package main
import (
"fmt"
"strconv"
"sync"
)
var wg sync.WaitGroup // 只定义无需赋值
func main() {
// 启动5个协程
for i := 1; i <= 5; i++ {
wg.Add(1) // 协程开始的时候加一
go func (n int) {
fmt.Println("协程" + strconv.Itoa(n))
wg.Done() // 协程执行完成减一
}(i)
}
// 主线程阻塞,什么时候wg减为0时就停止
wg.Wait()
}
多个协程操作同一个数据
互斥锁
package main
import (
"fmt"
"sync"
)
// 定义一个变量
var totalNum int
var wg sync.WaitGroup
// 加入互斥锁
var lock sync.Mutex
func add() {
defer wg.Done()
for i := 0; i < 100000; i++ {
// 加锁
lock.Lock()
totalNum = totalNum + 1
// 解锁
lock.Unlock()
}
}
func sub() {
defer wg.Done()
for i := 0; i < 100000; i++ {
// 加锁
lock.Lock()
totalNum = totalNum - 1
// 解锁
lock.Unlock()
}
}
func main() {
wg.Add(2)
// 启动协程
go add()
go sub()
// 阻塞
wg.Wait()
fmt.Println(totalNum)
}
读写锁
package main
import (
"fmt"
"sync"
"time"
)
// 加入读写锁
var lock sync.RWMutex
var wg sync.WaitGroup
func read() {
defer wg.Done()
// 如果只是读取数据那么这个锁不产生影响,但是如果读写同时发生则生效
lock.RLock()
fmt.Println("开始读取数据")
time.Sleep(time.Second)
fmt.Println("读取成功")
lock.RUnlock()
}
func write() {
defer wg.Done()
lock.Lock()
fmt.Println("开始写入数据")
time.Sleep(time.Second * 10)
fmt.Println("写入成功")
lock.Unlock()
}
func main() {
wg.Add(6)
// 启动协程
for i := 0; i < 5; i++ {
go read()
}
go write()
// 阻塞
wg.Wait()
}
管道特性介绍
- 管道本质就是一个数据结构-队列
- 数据是先进先出
- 自身线程安全,多协程访问时,不需要加锁,管道本身就是线程安全的
- 管道有类型的,一个 string 类型的管道只能存放 string 类型数据
管道的定义和入门案例
管道的定义
var 变量名 chan 数据类型
- chan管道关键字
- 数据类型指的是管道的类型,里面放入数据的类型,管道是有类型的,intChan只能写入整数int
- 管道是引用类型,必须初始化才能写入数据,即make后才能使用
入门案例
package main
import (
"fmt"
)
func main() {
// 定义管道
var intChan chan int
// 通过make初始化, 管道可以存放3个int类型的数据
intChan = make(chan int, 3)
// 证明管道是引用类型
fmt.Printf("intChan的值: %v \n", intChan)
// 存放数据
intChan <- 10
intChan <- 20
intChan <- 30
fmt.Printf("管道的实际长度是: %v,管道的容量是: %v \n", len(intChan), cap(intChan))
// 读取数据
num1 := <-intChan
num2 := <-intChan
num3 := <-intChan
fmt.Println(num1)
fmt.Println(num2)
fmt.Println(num3)
}
管道的关闭
package main
import (
"fmt"
)
func main() {
// 定义管道
var intChan chan int
// 初始化
intChan = make(chan int, 3)
// 在管道中存放数据
intChan <- 10
intChan <- 20
intChan <- 30
// 关闭管道
close(intChan)// 关闭管道后不能再存放数据
// 可以读取数据
fmt.Println(<-intChan)
fmt.Println(<-intChan)
fmt.Println(<-intChan)
}
管道的遍历
管道支持for-range的方式进行遍历,请注意两个细节:
- 在遍历时,如果管道没有关闭,则会出现deadlock的错误
- 在遍历时,如果管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
package main
import (
"fmt"
)
func main() {
// 定义管道
var intChan chan int
// 初始化
intChan = make(chan int, 100)
// 在管道中存放数据
for i := 0; i < 100; i++ {
intChan <- i
}
// 关闭管道
// 如果不关闭管道则会报deadlock错误
close(intChan)
// 遍历
for v := range intChan {
fmt.Println(v)
}
}
协程和管道协同工作案例
package main
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
// 写
func writeData(intChan chan int) {
defer wg.Done()
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Println("写入的数据为: ", i)
time.Sleep(time.Second)
}
// 管道关闭
close(intChan)
}
// 读
func readData(intChan chan int) {
defer wg.Done()
// 遍历
for v := range intChan {
fmt.Println("读取的数据为: ", v)
time.Sleep(time.Second)
}
}
func main() {
// 写协程和读协程共同操作同一个管道
intChan := make(chan int, 50)
wg.Add(2)
go writeData(intChan)
go readData(intChan)
wg.Wait()
}
声明只读只写管道
package main
import (
"fmt"
)
func main() {
// 默认情况下管道是可读可写的
// var intChan1 chan int
// 只写
var intChan2 chan<- int
intChan2 = make(chan int, 3)
intChan2 <- 10
// num := <-intChan2 报错
fmt.Println("intChan2: ", intChan2)
// 只读
var intChan3 <-chan int
if intChan3 != nil {
num1 := <-intChan3
fmt.Println(num1)
}
}
管道的阻塞
当管道只写入数据,没有读取数据,就会出现阻塞
package main
import (
"fmt"
_"time"
"sync"
)
var wg sync.WaitGroup
// 写
func writeData(intChan chan int) {
defer wg.Done()
for i := 1; i <= 10; i++ {
intChan <- i
fmt.Println("写入的数据为: ", i)
// time.Sleep(time.Second)
}
// 管道关闭
close(intChan)
}
// 读
func readData(intChan chan int) {
defer wg.Done()
// 遍历
for v := range intChan {
fmt.Println("读取的数据为: ", v)
// time.Sleep(time.Second)
}
}
func main() {
// 写协程和读协程共同操作同一个管道
intChan := make(chan int, 10)
wg.Add(2)
go writeData(intChan)
// go readData(intChan)
wg.Wait()
}
select功能
解决多个管道的选择问题,也可以叫做多路复用,可以从多个管道中随机公平的选择一个来执行
- case后面必须进行的是io操作,不能是等值,随机去选择一个io操作
- default防止select被阻塞住,加入default
package main
import (
"fmt"
"time"
)
func main() {
// 定义两个管道
intChan := make(chan int, 1)
strChan := make(chan string, 1)
go func () {
time.Sleep(time.Second * 5)
intChan <- 10
}()
go func () {
time.Sleep(time.Second * 2)
strChan <- "10"
}()
// fmt.Println(<-intChan)本身取数据就是阻塞的
select {
case v := <-intChan:
fmt.Println("intChan: ", v)
case v := <-strChan:
fmt.Println("strChan: ", v)
default:
fmt.Println("防止select被阻塞")
}
}
defer + recover机制处理错误
多个协程工作,其中一个协程出现panic,导致程序崩溃
利用 defer + recover 捕获panic进行处理,即使协程出现问题,主线程仍然不受影响可以继续执行
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
// 输出数字
func printNum() {
defer wg.Done()
for i := 0; i <= 10; i++ {
fmt.Println(i)
}
}
// 做除法操作
func division() {
defer wg.Done()
// 利用 defer + recover 来捕获错误
defer func() {
// 调用recover内置函数,可以捕获错误
err := recover()
// 如果没有捕获错误,返回值为 nil 值
if err != nil {
fmt.Println("错误已经捕获")
fmt.Println("err是:", err)
}
}()
num1 := 10
num2 := 0
result := num1 / num2
fmt.Println(result)
}
func main() {
wg.Add(2)
go printNum()
go division()
wg.Wait()
}
反射(reflect)
在Go语言标准库中reflect包提供了运行时反射,程序运行过程中动态操作结构体
当变量存储结构体属性名称,想要对结构体这个属性赋值或查看时,就可以使用反射,反射还可以用作判断变量类型
Type和Value
整个reflect包中最重要的两个类型
reflect.Type类型、reflect.Value值
获取Type和Value的函数
入门案例
package main
import (
"fmt"
"reflect"
)
func main() {
a := 1.5
// 获取 a 的类型信息
fmt.Println(reflect.TypeOf(a))
// 获取 a 的值
fmt.Println(reflect.ValueOf(a))
}
获取结构体属性的值
package main
import (
"fmt"
"reflect"
)
type People struct {
Name string `xml:"name"`
Address string
}
func main() {
/*
获取结构体属性的值
*/
peo := People{"小明", "湖南长沙"}
// 获取 peo 的值
v := reflect.ValueOf(peo)
fmt.Println(v)
// 获取属性个数
fmt.Println(v.NumField())
// 获取索引为 1 的属性的值
fmt.Println(v.FieldByIndex([]int{1}))
content := "Name"
// 获取该名称属性的值
fmt.Println(v.FieldByName(content))
}
设置结构体属性的值
package main
import (
"fmt"
"reflect"
)
type People struct {
Name string `xml:"name"`
Address string
}
func main() {
/*
设置结构体属性的值
*/
// 指针
peo := new(People)
// 获取指针 peo 的值
// Elem 方法获取指针所指向的元素的反射值
v := reflect.ValueOf(peo).Elem()
// 判断 Name 属性的值是否可以被修改
fmt.Println(v.FieldByName("Name").CanSet())
// 为 Name 属性设置值
v.FieldByName("Name").SetString("小米")
// 为 Address 属性设置值
v.FieldByName("Address").SetString("湖南常德")
// 打印 peo
fmt.Println(peo)
}
获取标记
package main
import (
"fmt"
"reflect"
)
type People struct {
Name string `xml:"name"`
Address string
}
func main() {
/*
获取标记
*/
// 得到类型信息
t := reflect.TypeOf(People{})
fmt.Println(t)
// 返回值有两个: Name 字段的类型信息和是否找到 Name 字段
name, _ := t.FieldByName("Name")
// 得到 Name 属性的类型信息并打印
fmt.Println(t.FieldByName("Name"))
// 获取标记
fmt.Println(name.Tag)
// 获取标记中的内容
fmt.Println(name.Tag.Get("xml"))
}