Go基础语法
记录Go的基础语法,配合使用例子,加深对Go语言基础语法的熟悉,并作为自己的Go基础语法查询手册
变量与常量
- 标识符
Go语言的标识符由字母(AZ, az)、数字(0~9)、下划线(_)组成,区分大小写,不能以数字开头,只能以字母和下划线开头,Go语言中使用驼峰命名法命名变量,例如使用ListenAndServe和ReadFromFile
- 数据类型
func main () {
// 复数型
var c1 complex128
c1 = 3 + 4i
// 构造复数
c := complex(1.2, 3.4)
// 获取实部
fc1 := real(c)
// 获取虚部
fc2 := imag(c)
// 数字字面量
v1 := 0b00101101 // 二进制101101,相当于十进制45
v2 := 0o755 // 八进制755,相当于十进制493
v3 := 0x2d // 十六进制2d,相当于十进制45
// 定义一个多行字符串
str := `
这是一个多行字符串
这是第二行
这是第三行
`
}
- 字符串修改
字符串的底层实现是一个结构体,包含指向底层字节数组的指针和字节数组的长度
type stringStruct struct {
str unsafe.Pointer
len int
}
要修改字符串,需要先将其转换成[]rune或[]byte,然后转换成string,无论哪种转换,都会重新分配内存,并复制字节数组
func main () {
s1 := "big"
byteS1 := []byte(s1)
byteS1[0] = 'p'
s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
}
组成每个字符串的元素是字符,用单引号包裹,Go中字符有两种类型,byte代表ASCII码字符,rune代表UTF-8字符,这两个本质上都是整型,只是为了方便编程,Go语言才给出这两个别名,其中byte是uint8的别名,rune是int32的别名
- 类型转换
数值类型之间可以互相转换,但字符串类型不能与布尔类型、数值类型相互转换
强制类型转换存在溢出情况,当字节长度大的数值转换为字节长度小的数值时,大数值的高位被截位,导致数据精度丢失;浮点数转换为整数时,小数部分会被截掉;可以使用strconv包进行转换,该包实现了基本数据类型和其字符串表示的相互转换
func main () {
var a = 1
c := float32(a)
// 整数转换成字符串
i := 123
// 使用fmt.Sprintf返回一个格式化的字符串
s1 := fmt.Sprintf("%d", i)
// 使用strconv.Itoa返回一个字符串
s2 := strconv.Itoa(i)
// 使用strconv.FormatInt返回一个字符串
s3 := strconv.FormatInt(int64(i), 10)
// 使用strconv.FormatUint返回一个字符串
s4 := strconv.FormatUint(uint64(i), 10)
// 字符串转换成整数
s := "123"
// 使用strconv.Atoi返回一个整数
i1, _ := strconv.Atoi(s)
// 使用strconv.ParseInt返回一个整数
i2, _ := strconv.ParseInt(s, 10, 64)
// 使用strconv.ParseUint返回一个无符号整数
i3, _ := strconv.ParseUint(s, 10, 64)
// 字符串转换成浮点数
s = "123.456"
// 使用strconv.ParseFloat返回一个浮点数
f, _ := strconv.ParseFloat(s, 64)
}
- 基本使用
- 变量声明
func main() {
// 变量声明
var name string
var age int
var isOk bool
// 批量声明
var (
name1 string
age1 int
isOk1 bool
)
}
- 变量初始化
func main() {
// 声明时指定初始值
var name string = "zhangsan"
// 一次初始化多个变量
var name, age = "zhangsan", 20
// 省略类型,编译器自动推导类型
var name = "zhangsan"
var age = 20
// 语法糖,简短声明并初始化方式
name := "zhangsan"
age := 20
}
- 常量
声明后恒定不变的值
func main() {
// 常量声明
const pi = 3.1415
// 一次性声明多个常量
const (
pi = 3.1415
e = 2.71828
)
// 可省略值
const (
n1 = 100
n2 // 100
n3 // 100
)
// 使用预声明标识符
const (
leve10 = iota //0
leve11 = iota //1
// 可省略
leve12 //2
// 可跳过
_
leve13 //4
// 可插队
leve14 = 100 //100
leve15 //6
// 一行定义多个
leve16, leve17 = iota, iota //7, 8
leve18, leve19 //8, 9
// 定义数量级
_ = iota
KB = 1 << (10 * iota) //1 << (10 * 1)
MB = 1 << (10 * iota) //1 << (10 * 2)
GB = 1 << (10 * iota) //1 << (10 * 3)
TB = 1 << (10 * iota) //1 << (10 * 4)
)
}
条件语句
Go中规定与if匹配的左括号必须与if在同一行,与else匹配的左括号必须与else在同一行,同时,else必须与上一个if或else if右边的右括号在同一行
if或else if其后的条件语句不需要括号
特殊用法:可以在if表达式之前添加一个执行语句,再根据变量值进行判断
func main() {
score := 100 // score作用域是整个函数
if score >= 90 {
fmt.Println("优秀")
} else if score >= 80 {
fmt.Println("良好")
} else {
fmt.Println("及格")
}
// 特殊用法
if score := 90; score >= 90 { // score作用域是if语句块
fmt.Println("优秀")
} else if score >= 80 {
fmt.Println("良好")
} else {
fmt.Println("及格")
}
}
循环语句
Go中只有for一种循环语句,所有循环类型均可以使用for来实现
for后无需括号
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// 省略初始语句
i := 10
for ; i < 20; i++ {
fmt.Println(i)
}
// 省略初始语句和结束语句,实现while循环
for i < 30 {
fmt.Println(i)
i++
}
// 使用break、goto、return、panic语句强制退出for循环,或者通过continue跳出本次循环进入下一次循环
for i := 0; i < 10; i++ {
if i == 5 {
break
}
if i == 3 {
continue
}
if i == 7 {
panic("panic")
}
if i == 8 {
goto label
}
}
label:
fmt.Println("hello")
for {
fmt.Println("无限循环")
}
}
switch分支
switch可用于多条件判断,化简条件判断分支过多的情况
Go中的fallthrough语法可以在执行完满足条件的case分支后继续执行下一个分支,且fallthrough语句只能作为case分支的最后一个语句出现
func main() {
score := 100
switch score {
// 可使用多值判断
case 10, 20, 30:
fmt.Println("10, 20, 30")
// 使用fallthrough
case 40:
fmt.Println("40")
fallthrough // 此后的语句都会执行,输出50、60、default
case 50:
fmt.Println("50")
fallthrough
case 60:
fmt.Println("60")
default:
fmt.Println("default")
}
// 使用表达式做判断
switch {
case score == 100:
fmt.Println("100")
case score == 200:
fmt.Println("200")
default:
fmt.Println("default")
}
}
标签
Go中标签被定义后必须被使用,标签名允许与变量名重复,但标签名不能重复
- goto
goto语句通过标签在代码间无条件跳转,能快速跳出循环、避免重复退出,能简化一些代码的实现过程
func main() {
// 使用goto跳出双层for循环
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 20 {
// 跳转到退出标签
goto breakTag
}
fmt.Println(i, j)
}
}
return // 若不加return,则后面breakTag标签没有goto跳转也会被执行
breakTag:
fmt.Println("breakTag")
}
- break
break语句可以结束for、switch和select语句,但不能用于goto语句,可以在break语句后面添加标签,表示退出某个标签对应的代码块,标签名必须定义在对应的for、switch和select语句块之前
func main() {
breakLabel:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
fmt.Println(j)
break breakLabel // 跳出,不再执行breakLabel下的循环语句,即只会输出2
}
}
fmt.Println(i)
}
}
- continue
continue语句可以结束当前循环,开始下一次循环,但是仅限在for循环中使用,在continue语句后添加标签,表示继续标签对应的下一次循环
func main() {
breakLabel:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
fmt.Println(j)
continue breakLabel //跳出内层循环,继续执行外层循环,即只会打印十个2
}
}
fmt.Println(i)
}
}
数组
数组的大小从声明时就确定了,使用时可以修改数组成员,但是不能修改数组大小,因此平常不常使用数组,而更多使用切片,其长度可以变化
数组是占用一块连续内存的值类型,赋值操作和函数传参会复制整个数组得到一个副本,修改副本的值不会改变原始变量的值
func testArray(a [2]int) {
a[0] = 100
fmt.Println(a) // [100 2]
}
func main() {
a := [2]int{1, 2}
testArray(a)
fmt.Println(a) // [1 2]
}
- 数组的声明
[3]int和[4]int是不同类型,不能直接进行比较和赋值
func main() {
// 声明一个数组
var a [3]int
var b [4]int
var n = 5
// 数组的长度必须是常量或常量表达式
// var c [n]int // error
// 可以通过索引访问数组成员
fmt.Println(a[1], b[2])
// 使用内置len函数获取数组中元素数量
fmt.Println(len(a), len(b))
}
- 数组的初始化
func main() {
var a [3]int // 初始化为int类型的零值
var b = [3]int{1,2,3} // 使用指定初始值初始化数组
var c = [...]int{1,2,3,4,5} // 编译器根据初始值的数量自行推断数组的长度
d := [...]int{1: 1, 3: 5} // 只初始化部分值
}
- 数组的遍历
func main() {
array := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// for循环遍历
for i := 0; i < len(array); i++ {
fmt.Println(array[i])
}
// for range遍历
for index, value := range array {
fmt.Println(index, value)
}
}
- 多维数组
func main() {
a := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}
b := [2][4]string{
{"1", "2", "3", "4"},
{"5", "6", "7", "8"},
}
for _, v1 := range b { // 获取内层数组
for _, v2 := rangge v1 { // 遍历内层数组
fmt.Println(v2)
}
// 多维数组只有第一层可以让编译器自动推导数组长度
c := [...][2]string{
{"1", "2"},
{"3", "4"},
{"5", "6"}
}
}
切片
切片是可以存储相同类型元素的长度可变的序列,是基于数组类型封装的,就像一个长度可变的滑动窗口在底层数组上移动,可以包含数组的全部或部分元素
多个切片可能对应同一个底层数组,切片之间可以重叠,因此修改切片时,有可能影响拥有相同底层数组的其他切片
一个完整切片需要包含三个要素
-
起始元素的地址:指切片中的底层数组中第一个元素的地址
-
长度len:指切片中元素的数量,内置len函数获取切片长度
-
容量cap:指切片中第一个元素到可能达到的最后一个元素的数量,内置cap函数获取切片容量
- 创建切片
- 切片字面量
func main() {
a := []int{1, 2, 3}
b := []string{"1", "2", "3"}
c := []int{1, 2, 3, 99: 100} // 使用索引方式指定初始值
fmt.Println(len(c), cap(c)) // 100 100
}
- 切片表达式
基于数组通过切片表达式得到切片,索引low和high必须满足0 <= low <= high <= len(array),否则会索引越界,左闭右开
由于切片的底层是数组,因此对数组取切片得到的是数组,也即是一个切片,因此存在容量cap的概念;而对字符串取切片得到的是字符串,没有容量cap的概念
func main() {
a := [5]int{1, 2, 3, 4, 5}
s1 := a[1:3] // [2 3]
fmt.Printf("s1: %v type: %T len: %v cap: %v\n", s1, s1, len(s1), cap(s1)) // [2 3] []int 2 4,长度为2容量为4
// 可省略
a[2:] // a[2:len(a)]
a[:3] // a[0:3]
a[:] // a[0:len(a)]
b := "hello world"
s2 := b[1:3] // "el"
fmt.Printf("s2: %v type: %T len: %v cap: %v\n", s2, s2, len(s2), cap(s2)) // cap(s2)报错,字符串类型没有容量的概念
}
对切片取切片得到的是切片,此时high的上限是切片的容量cap(a)
func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // [2, 3]
s1 := s[3:4] // 切片的切片,high可以到切片的容量
fmt.Printf("s1: %v len: %v cap: %v\n", s1, len(s1), cap(s1)) // s1: [5] len: 1 cap: 1
}
完整切片表达式,除了low和high,增加一个max用来表示结果的容量,此时会将结果切片的容量设置为max-low,字符串不支持完整切片表达式,需要满足0 <= low <= high <= max <= len(array),否则会索引越界
func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3:4] // 额外指定max,控制切片容量
fmt.Printf("s: %v len: %v cap: %v\n", s, len(s), cap(s)) // [2, 3] len: 2 cap: 3
s2 := s[3:4]
fmt.Printf("s2: %v len: %v cap: %v\n", s2, len(s2), cap(s2)) // 报错,由于s指定了容量3,此时s已经没有包含5这个元素了
s1 := a[1:3]
fmt.Printf("s1: %v len: %v cap: %v\n", s1, len(s1), cap(s1)) // [2, 3] len: 2 cap: 4
}
- make创建切片
用于动态创建一个切片,使用make函数可以将切片所需容量一次申请到位,切片的元素数量小于或等于该切片的容量
func main() {
s1 := make([]string, 2) // cap默认与长度相同,len2,cap2
s2 := make([]string, 2, 10) // len2, cap10
}
- 空切片
nil切片不仅没有大小和容量,也没有指向任何底层的数组
empty切片没有大小和容量,但是指向了一个特殊的内存地址zerobase,即所有0字节分配的基地址
要判断一个切片是否为空,只能用len(s) == 0,
切片中的元素不是直接存储的值,因此不允许切片之间直接使用==进行比较,切片唯一合法的比较操作是和nil比较
func main() {
var s1 []int // nil切片
s2 := []int{} // empty切片
s3 := make([]int, 0) // empty切片
}
- 切片操作
- 切片遍历
func main() {
s := []int{1, 2, 3}
// 索引遍历
for i := 0; i < len(s); i++ {
fmt.Println(s[i])
}
// range遍历
for index, value := range s {
fmt.Println(index, value)
}
}
- 切片追加
append函数支持在切片末尾追加一个,也可以一次性追加多个,也可以追加另一个切片中的元素,但是需要配合...符号将被添加的切片展开
Go编译器不允许调用append函数后不适用其返回值,因此通常使用原变量接收append函数的返回值
func main() {
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
s1 = append(s1, 7) // [1 2 3 7]
s2 = append(s2, 8, 10) // [4 5 6 8 10]
s1 = append(s1, s2...) // [1 2 3 7 4 5 6 8 10]
// append函数中可以直接使用nil slice
var s []int // nil slice
s = append(s, 1, 2, 3)
// 使用make时要注意是否初始化时指定初始大小
s3 := make([]int, 3)
s3 = append(s3, 1, 2, 3) // [0 0 0 1 2 3]
s4 := make([]int, 0, 3)
s4 = append(s4, 1, 2, 3) // [1 2 3]
}
- 切片扩容策略
向切片中添加元素时,若底层数组不能容纳新增的元素,切片会自动按照一定的策略进行扩容,此时切片指向的底层数组会更换
-
Go1.21.1版本的切片扩容策略
- 若newLen > doublecap,则newcap = newLen
- newLen <= doublecap,并且oldCap < 256,则newcap = doublecap
- oldCap >= 256,则最终容量newcap开始循环增加(newcap + 3 * threshold) / 4,直到newcap >= newLen
- 切片复制拷贝
简单的复制会导致两个切片共享同一个底层数组,此时对一个切片进行修改会影响另一个切片的内容
func main() {
s1 := make([]int, 3)
s2 := s1 // 将s1赋值给s2,s1和s2共用一个底层数组
s2[0] = 100 // 修改s2的值,s1的值也会变
fmt.Println(s1, s2) // [100 0 0] [100 0 0]
}
可以使用copy函数将一个切片的数据复制到另一个切片空间,此时两个切片不会共享同一个底层数组,因此可以安全的对一个切片进行修改而不会影响另一个切片
func main() {
s1 := []int{1, 2, 3, 4, 5}
s2 := make([]int, 5, 5)
copy(s2, s1) // 将切片s1中的元素复制到切片s2中
}
- 切片删除元素
Go语言没办法删除切片中间某个元素并保持剩余元素顺序的办法,只能通过切片操作将删除的元素前后的元素顺序连接起来
func main() {
s := []int{1, 2, 3}
// 删除索引1元素
s1 := append(s[:1], s[2:]...)
s2 := copy(s[:1], s[2:])
}
映射map
Go的映射是一种无序的键值对类型,一个映射的所有键都必须是相同的类型,值也必须是相同的类型,但是键和值的类型可以不同
只有能使用==操作符进行比较的类型才能作为映射的键,任何类型都可以作为映射的值,不建议使用浮点型作为映射的键
- 创建映射
- 使用映射字面量的方式创建携带初始值的映射变量
默认初始值为nil,不能直接对nil映射添加元素,因为还没有初始化
- 使用make函数创建映射
使用make函数创建映射时,不管有没有指定容量,初始值都不再是nil
使用make函数创建映射时,容量参数不是必须的,映射会根据需要自动扩容,但是我们应该在初始化映射时就指定一个合适的容量,这样能有效减少运行时的内存分配,提高代码的执行效率
func main() {
// 默认初始值为nil
var m1 map[string]string
m1["zhangsan"] = "zhangsan" // 报错
m2 := map[string]string{
"zhangsan": "zhangsan",
"lisi": "lisi",
}
// 默认初始值不为nil
m3 := make(map[string]string)
m3["zhangsan"] = "zhangsan" // 正常
}
- 映射的基本使用
访问、删除、判断键是否存在、遍历
可以直接使用map[key]的方式访问元素,若key不存在,返回值类型对应的零值
可以使用len函数来获取映射中的元素数量
Go内置delete函数用来从映射中删除一组键值对
虽然通过map[key]访问映射中的元素时,若键不在映射中会返回对应类型的零值,但是这样不太友好,Go语言中有专门的写法来判断映射中是否存在某个键value, ok := map[key]
同样可以使用range来遍历映射的键和值,但是映射的遍历结果是不固定的,若要按照指定的顺序遍历映射,则可以先将映射的所有键取出放到一个切片中,再对切片进行排序,将遍历该切片的元素作为键去访问映射,间接实现按顺序访问的效果
func main() {
m := map[string]string{
"zhangsan": "zhangsan",
"lisi": "lisi",
}
fmt.Println(len(m)) // 2
// 添加元素
m["zhangsi"] = "zhangsi"
// 访问元素
fmt.Println(m["zhangsan"])
fmt.Println(len(m)) // 3
// 删除元素
delete(m, "zhangsi")
// 判断键值对是否存在
value, ok := m["zhangsi"]
// 按顺序遍历映射
// 先取出所有key存入切片中
keys := make([]string, 0, len(m))
for key, _ := range m {
keys = append(keys, key)
}
// 对切片进行排序
sort.Strings(keys)
// 按顺序遍历映射
for _, key := range keys {
fmt.Println(key, m[key])
}
}
- 切片作为映射的值
感觉非常适合用来存储一些人员信息,比如一个切片就是一个人员,这个切片中可以存放很多个映射的数据
func main() {
// 映射类型的切片
m := make([]map[string]string, 0, 10)
// 对切片中的映射元素进行初始化
m[0] = make(map[string]string, 2)
m[0]["a"] = "a"
m[0]["b"] = "b"
}
- 映射类型的切片
func main() {
// 值为切片类型的映射
// 键为字符串,值为字符串切片的映射
m := make(map[string][]string, 3)
key := "zhangsan"
value, ok := m[key]
if !ok {
fmt.Println("not found")
}
// 找到的话,值就是一个切片
value = append(value, "lisi")
m[key] = value
}
函数
函数使用func关键字声明,func 函数名 (参数) (返回值)Go语言支持多个返回值
Go中函数调用过程传参是按值传递,函数接收到的是复制的实参副本,而当传入函数的实参为某些特定类型(指针、切片、映射、函数、通道)时,函数接收到的是实参的引用,此时在函数中修改形参将对实参造成影响
- 函数的一些特殊用法
- 类型简写
当参数中相邻变量的类型相同时可以省略参数的类型,func Sum(x, y int) int {}
- 可变参数
指函数的参数数量不固定,Go中可变参数通过在形参后添加...来标识,通常作为函数的最后一个参数,可变参数本质上是一个切片
// 有多个返回值时,在函数声明中必须用括号将两个返回值的类型括起来
func sum(x int, y ...int) (int, int) {
summ := 0
for _, v := range y { // y是一个切片
summ += v
}
return x, summ
}
res1 := sum(10)
res2 := sum(10, 20)
res3 := sum(10, 20, 30)
fmt.Println(res1, res2, res3) // 10 30 60
- 命名返回值与特殊返回值
函数定义阶段可以直接给返回值命名,并在函数体中直接使用这些变量
在某些场景中,nil可以作为特殊返回值返回
func sum(x int, y int) (sum int) {
sum = x + y
return
}
func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
return nil
}
- 变量作用域
全局变量定义在函数外部,在整个程序运行周期都有效
局部变量又分为:函数内局部变量和语句块局部变量
若局部变量与全局变量重名,则优先访问局部变量(就近原则)
- 函数类型与变量
函数的类型通常称为函数签名,拥有相同参数列表和返回值列表的函数类型相同
// 以下三个函数,函数签名相同
func f1(x int, y int) int {}
func f2(x, y int) int {}
func f3(x, y int) (res int) {}
Go语言中函数也可以是一种类型,可以将函数类型的值保存在变量中,函数类型的零值为nil
func sayHello(name string) {
fmt.Println("Hello,", name)
}
func main() {
var f func(string) // 声明函数类型变量f
f = sayHello // 将sayHello赋值给f
f("Go") // 输出:Hello, Go
}
可以使用type关键字为函数类型定义类型别名
如,定义了一个calculation类型,他是func(int, int) int类型的别名,只要函数签名满足func(int, int) int,那么该函数就可以赋值给calculation类型的变量,并进行调用
type calculation func(int, int) int
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
func main() {
var c calculation
c = add
fmt.Println(c(1, 2)) // 相当于调用add(1, 2)
s := sub
fmt.Println(s(1, 2)) // 相当于调用sub(1, 2)
}
- 高阶函数
指以其他函数为参数,或返回一个函数作为结果的函数
- 函数作为参数
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
// 将两个整数传入给定op函数进行计算并返回结果
func calc(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
addres := calc(10, 20, add)
subres := calc(10, 20, sub)
}
- 函数作为返回值
func do(s string) (func(int, int) int, error) {
switch s {
case "+":
return func(i, j int) int { return i + j }, nil
case "-":
return func(i, j int) int { return i - j }, nil
case "*":
return func(i, j int) int { return i * j }, nil
default:
err := errors.New("invalid operator")
return nil, err
}
}
func main() {
f, err := do("+")
if err != nil {
fmt.Println(err)
return
}
res := f(1, 2)
fmt.Println(res)
}
- 匿名函数和闭包
匿名函数可以看作没有函数名的函数,因此没办法像普通函数那样被调用,需要保存到某个变量中或者作为立即执行函数
匿名函数多用于实现回调函数和闭包
func main() {
// 将匿名函数保存到变量中
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20)
// 自执行函数:匿名函数定义玩笔加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}
闭包指一个函数和与其相关的引用环境组合而成的实体,即闭包=函数+引用环境,即闭包是一个引用了外层函数局部变量的函数
func addrer() func(int) int {
var x int
return func(y int) int {
// 将y的值累加到其外层函数的局部变量x
x += y
return x
}
}
// 闭包的x变量也可以是外层函数的参数,在函数调用时由外部传入,更加灵活
func addrer2(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
var f = addrer() // 在f的作用域中,闭包中的x也一直有效
fmt.Println(f(1)) // 1
fmt.Println(f(2)) // 3
fmt.Println(f(3)) // 6
}
一个闭包使用的实际性例子,理解上来说,可以实现对一个函数的复用,更常见的做法是将外部变量传入闭包中,从而使得该闭包在其生命周期内都都与该外部变量有关
// makeSuffixFunc, 返回一个给文件名添加指定后缀的函数
func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
// 判断文件名是否包含指定后缀
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
// 创建一个添加.jpg后缀的函数
jpgFunc := makeSuffixFunc(".jpg")
// 创建一个添加.txt后缀的函数
txtFunc := makeSuffixFunc(".txt")
// 给文件名avatar添加.jpg后缀
fmt.Println(jpgFunc("avatar")) // avatar.jpg
// 给文件名readme添加.txt后缀
fmt.Println(txtFunc("readme")) // readme.txt
}
- 内置函数
- defer函数
defer语句能够延迟函数调用,这个被延迟调用的函数的所有返回值都需要被丢弃
被延迟处理的语句将按defer定义的逆序执行,即先被defer的语句最后被执行,最后被defer的语句最先被执行,类似于栈先进后出
defer函数常用于在读取文件内容时,延迟执行f.Close(),能够保证在函数运行玩笔后妥善地关闭文件,释放相关资源
func readFromFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // 函数结束时关闭文件
return nil
}
defer函数也经常用来做一些调试工作,如在函数的入口和出口执行特定的语句,记录执行时间和相关变量等
func recordTime(funcName string) func() {
start := time.Now()
fmt.Printf("enter %s\n", funcName)
return func() {
fmt.Printf("exit %s, spend %v\n", funcName, time.Since(start))
}
}
func readFromFile(filename string) error {
defer recordTime("readFromFile")() // defer延迟调用,虽然recordTime函数return的没有变量进行接收,但是也会执行打印输出
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // 函数结束时关闭文件
return nil
}
return语句不是原子操作,包含两步,首先是返回赋值,再执行RET指令进行返回,而defer的执行处于这两个步骤之间,因此可以更新函数的返回值变量
// 首先将返回值赋给返回变量,接着修改x的值,最后返回,因此不会更新返回值
func f1() int {
x := 5
defer func () {
x++ // 只修改x的值,不会更新返回值
}()
return x // 返回值=5
}
// 首先x=5,然后执行defer会更新x的值,最后返回,因此此时会更新返回值,因为提前声明好了返回值变量
func f2() (x int) {
defer func() {
x++ // 会更新返回值
}()
return 5 // 返回值 x = 5
}
- panic函数
当一个函数调用发生panic时,会立即进入退出阶段,开始执行所有延迟调用函数,然后程序会异常退出并输出相应的日志,日志中会包含函数调用信息和具体的panic值
panic函数可以接收任意值作为参数,但通常使用相关错误信息作为参数
panic会导致程序异常退出,因此一般只在程序出现不应该出现的逻辑错误时才会使用,应该使用Go语言提供的错误机制,而不是滥用panic机制
对于一个函数其非法的情况是不可能出现的,因此通常我们没必要去处理这种不可能出现的错误,此时就会用panic,并约定熟成的使用Muust作为函数名称的前缀
// Compile函数是regexp包中用于将字符串参数编译成正则表达式的函数
// 正常情况下我们不会在代码中添加一个非法的正则表达式,因此对于错误情况直接使用panic进行处理
func MustCompile(str string) *Regexp {
regexp, err := Compile(str)
if err != nil {
panic(`regexp: Compile(`+ quote(str) +`): ` + err.Error())
}
return regexp
}
- recover函数
程序可以使用recover函数从panic状态恢复,通过在延迟调用函数中调用recover函数,可以消除当前goroutine中的一个panic,回到正常状态;recover函数旨在defer调用的函数中有效
在一个处于panic状态的goroutine退出之前,其中的panic不会蔓延到其他的goroutine;如果一个goroutine在panic状态下退出,则会使整个程序崩溃
func funA() {
fmt.Println("funA")
}
func funB() {
// 延迟调用一个自执行的函数,必须得是panic了之后再恢复,因此recover必须配合defer
defer func() {
err := recover()
// 如果程序出现了panic,那么可以通过recover恢复
if err != nil {
fmt.Printf("recover from panic: %v\n", err)
}
}()
panic("funB panic")
}
func funC() {
fmt.Println("funC")
}
func main() {
funA()
funB()
funC()
/*
funA
recover from panic: funB panic
funC
*/
}
使用recover函数从panic中恢复时需要特别注意以下几点:
①一定要在可能引发panic的语句之前调用recover函数
②recover函数必须搭配defer使用,并且不能直接使用defer延迟调用
③recover函数只能恢复当前goroutine的panic
除非明确知道程序应该使用panic和recover,否则一般不要使用它们
- new和make函数
当我们声明一个值类型的变量时,Go语言会默认已经为其分配内存,而当我们声明指针或包含指针类型的变量时,不仅要声明类型,还要为它申请内存空间,否则没办法直接存储值,此时就要用到new和make函数
new函数不太常用,其分配内存后得到的是一个类型的指针,该指针对应的值就是该类型的零值
func main() {
a := new(int) // 得到的是int类型的指针
b := new(bool) // 得到的是bool类型的指针
}
make函数更常用,但是他只用于切片、映射和通道三个类型,并且返回值也是这三个类型本身,而不是他们的指针类型
在使用切片、映射、通道时,都需要先使用make函数对他们进行初始化
func main() {
var s []int
s = make([]int, 1) // make函数的返回值就是一个切片,分配一个内存
s[0] = 100
s = append(s, 200) // 不会报错,切片会自动扩容
fmt.Println(s[0], s[1])
var m map[string]int
m = make(map[string]int, 1) // make函数的返回值就是一个映射,分配两个内存
m["zhangsan"] = 100
m["zhangsi"] = 100 // 不会报错,映射会自动扩容
for k, v := range m {
fmt.Println(k, v)
}
}
指针
Go的指针不能偏移和运算,是安全指针
func main() {
// 使用取地址方式创建指针
v := 10
p := &v
// 使用new创建指针
p1 := new(int)
// 指针取值
*p1 = 10
}