【Go】Go语言学习笔记--Go语言基础【中】

294 阅读16分钟

运算符

Go语言内置的运算符:

  1. 算术运算符
  2. 关系运算符
  3. 逻辑运算符
  4. 位运算符
  5. 赋值运算符

算数运算符

运算符描述
+相加
-相减
*相乘
/相除
%取余

++--在Go语言里是单独的语句,不是运算符,与JavaScript不同。

关系运算符

运算符描述
==两个值是否相等,如果相等返回 True 否则返回 False
!=两个值是否不相等,如果不相等返回 True 否则返回 False
左边是否大于右边
>=左边是否大于等于右边
<左边是否小于右边
<=左边是否小于等于右边

逻辑运算符

运算符描述
&&AND
||OR
!NOT

位运算符

运算符描述
&参与运算的两数各对应的二进位相与
|参与运算的两数各对应的二进位相或
参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1

赋值运算符

运算符描述
=简单的赋值
+=相加赋值
-=相减赋值
*=相乘赋值
/=相除赋值

练习题

有一堆数字,如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字?

// 我的解法
numarr := [...]int{1,1,2,2,3,3,4}
for i:=0;i<len(numarr);i++ {
    flag := true
    for j:=0;j<len(numarr);j++ {
        if numarr[i]==numarr[j] && i!=j {
            flag = false
            break
        }
    }
    if flag {
        fmt.Println(numarr[i], ",只出现了一次")
        break
    }
}
// 比较好的解法
numarr := [...]int{1,1,2,2,3,3,4}
res := 0
for _,item := range numarr{
   res ^= item
}
fmt.Println(res)

流程控制

if else

Go语言中if语句格式:

if 表达式1 {
    分支1
} else if 表达式2 {
    分支2
} else {
    分支3
}

switch case

基本格式

switch 值 {
    case1:
        分支1
    case2:
        分支2
    default:
        默认分支
}

多个case值

一个分支可以有多个值,多个case值之间用,隔开

switch 值 {
    case1,值2,值3:
        分支1
    case4,值5,值6:
        分支2
    default:
        默认分支
}
### 分支表达式
switch {
    case 表达式1:
        分支1
    case 表达式2:
        分支2
    default:
        默认分支
}

fallthrough

fallthrough可以在满足一个case的条件下,继续向下执行case分支。

for循环

基本格式

for 初始语句;表达式;结束语句 {
    循环体语句
}

无限循环

for {
    循环体语句
}

for循环可以通过breakgotoreturnpanic语句强制退出循环。

for range键值循环

Go语言中可以使用for range遍历数组切片字符串map通道(channel),返回值有以下规律:

  1. 数组、切片、字符串返回索引和值(即index和item)
  2. map返回键和值
  3. channel只返回通道内的值 类似JavaScript中的map、forEach等循环。

goto(跳转到指定标签)

goto语句可以快速跳出循环,避免重复退出。

break(跳出循环)

break语句可以结束forswitchselect的代码块。

continue(继续下一次循环)

continue可以结束当前循环,立即开始执行下一次循环,只能在for循环内使用。

练习题

打印9*9乘法表

// 我的解法
for i:=1;i<=9;i++ {
    for j:=1;j<=i;j++ {
        fmt.Printf("%v*%v=%v ", j, i, j*i)
    }
    fmt.Println()
}

数组(array)

数组定义

var 变量名称 [元素数量]类型

举个栗子🌰

// 定义一个长度为3,元素类型为int的数组a
var a [3]int

与JavaScript不同的是,数组的长度是必须常量,一旦定义,不能改变。比如[5]int[3]int就是不同的类型。

var a [5]int
var b [3]int
a = b // 不可以这样

数组可以通过下标进行访问,从0开始,到len-1结束。

数组初始化

方法一

var arr1 [3]int // [0 0 0]
var arr2 [4]int{1,2,3} // [1 2 3 0]
var arr3 [5]string{"北京","上海","广州","深圳","杭州"} // [北京 上海 广州 深圳 ]

方法二

可以让编译器根据初始值的个数自行推断数组的长度。

var arr1 = [...]int{1,2,3,4} //[1 2 3 4]
var arr2 = [...]string{"男","女"} //[男 女]
var arr3 = [...]bool{true, false} // [true false]

方法三

指定索引值初始化数组。

var arr1 = [...]int{2:22,5:55} // [0 0 22 0 0 55]

数组的遍历

遍历数组有两种方法:

// 方法一
var arr = [...]int{1,2,3,4,5}
for i:=0; i<len(arr); i++ {
    fmt.Printf("当前索引:%v; 当前项:%v \n", i, arr[i])
}
// 方法二
for index,item := range arr {
    fmt.Printf("当前索引:%v; 当前项:%v \n", index, item)
}

多维数组

二维数组的定义

var arr = [3][2]string{
    {"北京","上海"},
    {"广州","深圳"},
    {"杭州","西安"},
}
fmt.Print(arr)//[[北京 上海] [广州 深圳] [杭州 西安]]

二维数组的遍历

var arr = [3][2]string{
    {"北京","上海"},
    {"广州","深圳"},
    {"杭州","西安"},
}
// 方法一
for i:=0;i<len(arr);i++ {
    for j:=0; j<len(arr[i]);j++ {
        fmt.Println(arr[i][j])
    }
}

// 方法二
for _,item1 := range arr {
    for _,item2 := range item1 {
        fmt.Println(item2)
    }
}

多维数组只有第一层可以使用...来让编译器推导长度。

数组是值类型

数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。(即JavaScript中的Array为引用数据类型)

func main()  {
   var arr = [3]int{10,20,30}
   modifyArr(arr)
   var arr2 = [3][2]int{
      {1,1},
      {1,1},
      {1,1},
   }
   modifyArr2(arr2)
   fmt.Println("main函数里的arr",arr2)
}

func modifyArr(x [3]int)  {
   x[0] = 100
}
func modifyArr2(x [3][2]int)  {
   x[2][0] = 100
   fmt.Println("modifyArr2中的arr",x)
}

练习题

  1. 求数组[1, 3, 5, 7, 8]所有元素的和
// 我的解法
func reduceArr(arr [5]int) {
   var count int = 0
   for _, c := range arr{
      count += c
   }
   fmt.Print(count)
}
  1. 找出数组中和为指定值的两个元素的下标,比如从数组[1, 3, 5, 7, 8]中找出和为8的两个元素的下标分别为(0,3)(1,2)
// 我的解法
func findIndex(num int, arr [5]int) {
   for i:=0;i<len(arr);i++ {
      for j:=i+1; j<len(arr);j++ {
         if arr[i]+arr[j] == num {
            fmt.Printf("(%v,%v)", i, j)
         }
      }
   }
}

切片(slice)

数组有很多局限性,长度固定,不能添加新的元素到数组里,所以有了切片(slice),一个切片,可以看作是由:一个左指针和一个右指针以及capacity组成。

基本概念

切片是一个拥有相同类型元素的可变长度的序列,是基于数组的一层封装,非常灵活。
切片是引用类型的,内容结构包含了地址长度容量。切片一般用于快速的操作一块数据集合。

基本语法

var 变量名 []元素类型

举个栗子🌰

var a []string
var b = []int{}
var c = []bool{false, true}
var d = []bool{false, true}

切片的长度和容量

  • len()求长度
  • cap()求容量

切片表达式

切片表达式中lowhigh表示一个索引范围(包头不包尾),举个栗子🌰

var arr = [5]int{1,2,3,4,5}
var arr2 = arr[0,4]
fmt.Printf("arr2:%v;len:%v;cap:%v", arr2, len(arr2), cap(arr2))
//arr2:[1 2 3 4];len:4;cap:5

切片表达式中的索引可以省略,low默认为0high默认为len(数组)

arr[2:0] //等于 arr[2:len(arr)]
arr[:3] // 等于 arr[0:3]
arr[:] // 等于 arr[0:len(arr)]

注意
对于数组和字符串的切片,0 <= low <= high <= len(arr),索引合法。
对于切片切片,high的上限边界是切片的容量,而不是长度。

var arr = [5]int{1,2,3,4,5}
var s1 = arr[1:3]
fmt.Printf("s1:%v;len:%v;cap:%v", s1, len(s1), cap(s1))
fmt.Println()
var s2 = s1[1:4] // 索引的上限是cap(s1)而不是len(s1)
fmt.Printf("s2:%v;len:%v;cap:%v", s2, len(s2), cap(s2))

cap函数

从数组中取

cap函数只计算从左指针原数组最后的值,也就是lowlen(arr)的个数,即len(arr) - low

从切片中取

举个栗子🌰

var arr = [9]int{1,2,3,4,5,6,7,8,9}
var s1 = arr[2:7] // 此时左指针偏移两位,cap(s1)是7,len(s1)是5, s1是[3 4 5 6 7]
var s2 = s1[3:4] // 此时左指针再次偏移三位,cap(s2)是4,len(s2)是1,s2是[6]
var s3 = s2[2:4] // 此时左指针再次偏移两位,cap(s3)是2,len(s3)是2,s3是[8 9]

使用make函数构造切片

不基于数组,动态创建一个切片,基本格式:

make([]元素类型,元素数量,切片容量)

举个栗子🌰

var s1 = make([]int, 2, 10)
//s1:[0 0] len(s1):2,cap(s1):10

判断切片是否为空

始终使用len(s1) == 0来判断

切片的赋值拷贝

s1 := make([]int,3)
s2 := s1 // s1与s2共用一个底层数组
s1[0] = 100
fmt.Println(s1) // [100 0 0]
fmt.Println(s2) // [100 0 0]

切片的遍历

var s1 = []int{1,2,3,4}
for index,item := range s1 {
    fmt.Println(index, item)
}

append()添加元素到切片

var s1 = []int{1,2,3} 
s1 = append(s1, 4) // [1 2 3 4]
s1 = append(s1, 5,6,7) // [1 2 3 4 5 6 7]
var s2 = []int{8,9}
s1 = append(s1, s2...) // [1 2 3 4 5 6 7 8 9]

每个切片都会指向一个底层数组,数组容量够用就添加新元素。底层数组不能容纳新增元素时,切片会自动按照一定的策略“扩容”,此时,该切片指向的底层数组就会更换。
举个栗子🌰

var s1 = []int
for i=0;i<10;i++ {
    s1 = append(s1, i)
    fmt.Printf("%v len:%d cap:%d ptr:%p\n", s1, len(s1), cap(s1), s1)
}

输出:

[0] len:1 cap:1 ptr:0xc00010c008
[0 1] len:2 cap:2 ptr:0xc00010c030
[0 1 2] len:3 cap:4 ptr:0xc00011e020
[0 1 2 3] len:4 cap:4 ptr:0xc00011e020
[0 1 2 3 4] len:5 cap:8 ptr:0xc000120040
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc000120040
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc000120040
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc000120040
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc000122000
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc000122000

结论:

  1. append() 函数将元素追加到切片的最后,并返回切片
  2. s1的容量每次扩容后都是扩容前的2倍

copy()函数复制切片

切片直接赋值指向同一底层数组,利用copy可以将一个切片的数据复制到另外一个切片空间中。

copy(目标切片, 源切片 []元素类型)

举个栗子🌰

var s1 = []int{1,2,3,4,5}
var s2 = make([]int, 5)
copy(s2, s1)
fmt.Println(s1,s2) // [1 2 3 4 5] [1 2 3 4 5]
s1[0] = 100
s2[0] = 999
fmt.Println(s1,s2) // [100 2 3 4 5] [999 2 3 4 5]

注意:

  1. copy函数 目标切片需要先初始化长度
  2. 源切片中的元素类型为引用类型时,拷贝的是引用 举个栗子🌰
// 1.
var s1 = []int{1,2,3,4,5}
var s2 []int
copy(s2, s1)
fmt.Print(s1, s2) // [1 2 3 4 5] []

// 2.
var s1 = [][]int{
    {1, 1},
    {1, 1},
    {1, 1}
}
var s2 = make([][]int, 5)
copy(s1, s2)
s1[0][0] = 100
fmt.Print(s1, s2) // [[100 1] [1 1] [1 1]] [[100 1] [1 1] [1 1] [] []]

正确的拷贝二维数组的方法为:

// 先初始化,再拷贝
var s1 = [][]int{
    {1,1},
    {1,1},
    {1,1},
}
var s2 = make([]int, 3)
for index,item := range s1 {
    s2[index] = make([]int, len(item))
    copy(s2[index], item)
}

从切片中删除元素

Go语言中没有专门删除切片元素的方法......不像JavaScript,有splicepopshift。但是可以使用切片本身的特性来删除元素。

var s1 = []int{1,3,4,5,6,7}
// 要删除索引为2的元素
s1 = append(a[:2], a[3:]...)
fmt.Println(s1)

总结一下就是:要从切片s1中删除索引为index的元素,操作方法是s1 = append(s1[:index], s1[index+1:]...)

练习题

  1. 请写出下面代码的输出结果
var s1 = make([]string, 5, 10) // s1:["" "" "" "" ""] len(s1):5 cap(s1):10
for i:=0; i < 10; i++ {
    s1 = append(s1, fmt.Sprintf("%v", i))
}
fmt.Println(s1) // ["" "" "" "" "" 0 1 2 3 4 5 6 7 8 9]
  1. 请使用内置的sort包对数组var s1 = [...]int{3, 7, 8, 9, 1}进行排序。
package main
import (
    "fmt"
    "sort"
)
func main() {
    var arr = [...]int{3,7,8,9,1}
    var s1 = arr[:]
    sort.Ints(s1)
    fmt.Println(s1)
}
  1. 请写出下面代码的输出结果
s := []int{5} 
s = append(s, 7)
s = append(s, 9) 
x := append(s, 11) 
y := append(s, 12)
fmt.Println(s, x, y)

这个题目非常的有意思,以下是我的个人理解:

  1. 首先定义切片s:[5]len:1cap: 1sP: 0x001...
  2. append操作向切片s中添加一项7,此时len 2 > cap 1,触发扩容,产生新的数组,切片s:[5 7]len:2cap: 2sP: 0x002...
  3. append操作向切片s中添加一项9,此时len 3 > cap 2,触发扩容,产生新的数组,切片s:[5 7 9]len:3cap: 4sP: 0x003...
  4. 定义切片xappend操作向切片s中添加一项11,此时len 4 == cap 4,未触发扩容,不产生新的数组,切片s:[5 7 9]len:3cap: 4sP: 0x003...。 切片x:[5 7 9 11]len:4cap: 4sP: 0x003...
  5. 定义切片yappend操作向切片s中添加一项12,此时len 4 == cap 4,未触发扩容,不产生新的数组,切片s:[5 7 9]len:3cap: 4sP: 0x003...。 切片x:[5 7 9 12]len:4cap: 4sP: 0x003... 重点:此时!len:4 cap4 sp:0x003变成了[5 7 9 12],所以切片x与切片y都是[5 7 9 12]

演变一下:

s := []int{5} // s[5] slen:1 scap:1 sp:0x001
s = append(s, 7) // s[5 7] slen:2 scap:2 sp: 0x002
s = append(s, 9) // s[5 7 9] slen:3 scap:4 sp: 0x003
s = append(s, 11) // s[5 7 9 11] slen: 4 scap:4 sp: 0x003
x := append(s, 13) // s[5 7 9 11] slen:4 scap:4 scp: 0x003; x[5 7 9 11 13] xlen:5 xcap:8 xp: 0x101
y := append(s, 15) // s[5 7 9 11] slen:4 scap:4 scp: 0x003; y[5 7 8 11 15] ylen: 4 ycap:8 yp: 0x102

总结: 切片是切片,数组是数组,切片是数组一个片段的描述,决定性因素有三个:长度容量指针,其中:

  1. 长度指的是切片引用的数目
  2. 容量指的是底层数组的元素数目
  3. 指针指的是切片指向哪一个底层数组 对切片的操作还有一个点:
  4. 某次操作未超过该切片的cap,此次会更新改变原数组
  5. 某次操作超过了该切片的cap,此次操作会新建数组,不会改变原数组

map

map是一种无序的基于key-value的数据结构,类似JavaScript中的object,是引用类型,必须初始化才能使用。

map的定义

语法如下:

map[键的类型]值的类型

map类型的变量初始值为nil,需要使用make()函数为其分配内存,语法如下:

make(map[键的类型]值的类型,[cap])

其中cap是map的容量,不必传。

map的基本使用

🌰

m1 := make(map[string]int, 8)
m1["李四"] = 12
m1["张三"] = 29
fmt.Println(m1, reflect.TypeOf(m1)) // [张三:12 李四:29] map[string]int

map也支持在声明时填充元素,举个🌰

var m1 = map[string]string{
    "username": "HoMeTown",
    "passowrd": "123456",
}
fmt.Println(m1)

判断某个键是否存在

Go语言中判断map中是否存在某个键的写法比较特殊,不像JavaScript或者python中可以使用in关键字来判断,写法如下:

value, ok = map[key]

举个🌰

var m1 = map[string]string{
    "username": "admin",
    "password": "123456",
}
var v,ok = m1["username"]
if ok {
    fmt.Println(v)
} else {
    fmt.Println("不存在username")
}

map的遍历

使用for range遍历map,写法如下:

var m1 = map[string]string{
    "username": "admin",
    "password": "123456",
    "gender": "0",
    "email": "admin123@gmail.com",
}
for key,val := range m1 {
    fmt.Println(key,val)
}

只遍历key的话,可以这样:

for key := range m1 {
    fmt.Println(key)
}

delete()函数删除键值对

格式如下:

delete(map, key)

举个🌰

var m1 = map[string]string{
    "username": "admin",
    "password": "123456",
    "gender": "0",
    "email": "admin123@gmail.com",
}
fmt.Println(m1)
delete(m1, "email")
fmt.Println(m1)

元素为map类型的切片

var mapS1 = make([]map[string]string,3)//make一个元素为map的切片
mapS1[0] = make(map[string]string,10)
mapS1[0]["name"] = "小王"
mapS1[0]["age"] = "12"
mapS1[0]["address"] = "北京市"
fmt.Println(mapS1)

值为切片的map

var sliceM1 = make(map[string][]string, 4)
sliceM1["names"] = make([]string, 4)
sliceM1["names"] = append(sliceM1["names"], "小王","小黄","小李")
fmt.Println(sliceM1)

练习题

  1. 写一个程序,统计一个字符串中每个单词出现的次数。比如:”how do you do”中how=1 do=2 you=1。
// 我的解法
var words string = "how do you do"
var wordsSlice []string = strings.Split(words, " ")

var wordCountMap map[string]int = make(map[string]int)
for _, word := range wordsSlice{
   var _, isIn = wordCountMap[word]
   if isIn {
      wordCountMap[word]++
   } else {
      wordCountMap[word] = 1
   }
}
var printMsg = "在"how do you do"中"
for key,val := range wordCountMap {
   str:= key + "=" + strconv.Itoa(val) + ";"
   printMsg += str
}
fmt.Print(printMsg)
  1. 观察下面代码,写出最终的打印结果。
type Map map[string][]int
m := make(Map) //[]
s := []int{1,2} // [1 2] len 2 cap 2 x001
s = append(s,3) [1 2 3] len 3 cap 4 x002
fmt.Printf("%+v \n", s) //[1 2 3] len3 cap4 x002
m["q1mi"] = s // [q1mi: [1 2 3] len3 cap4 x002]
s = append(s[:1], s[2:]...) //[1 3] len 2 cap4 x002
fmt.Printf("%+v\n",s) //len2 cap4 x002 [1 3]
fmt.Printf("%+v\n",m["q1mi"])// len3 cap4 //[1 3 3] x002

[1 2 3] [1 3] [1 3 3]

函数

Go语言中支持函数、匿名函数和闭包。

函数定义

格式如下:

func 函数名(参数)(返回值){
    函数体
}

举个🌰

func main(){
    fmt.Println(intSum(1,2)) //3
}
func intSum(x int, y int)(int) {
    return x + y
}

参数

类型简写

函数参数中相邻变量的类型相同,则可以省略类型,举个🌰

func intSum(x,y int) int {
    return x + y
}

可变参数

指函数的参数数量不固定,通过在参数名后面加...来表示。
注意:可变参数通常要作为函数的最后一个参数,举个🌰

func intSum(x ...int) int {
    sum := 0
    for _,v := range x {
        x += sum
    }
    return sum
}

也可以与固定参数搭配使用

func intSum(x int, y ...int) int {
    sum := x
    for _,v := range x {
        x += sum
    }
    return sum
}

本质上,函数的可变参数是通过切片来实现的。

返回值

Go语言通过return关键字向外输出返回值,这一点与JavaScript相同,但也有不同点。

多个返回值

func calc(x, y int) (int, int) {
    sum := x + y
    sub := x - y
    return sub, sum
}

返回值命名

函数在定义时可以给返回值命名,并在函数体重使用这些变量,最后用return返回

func calc(x, y int) (sub, sum int) {
    sum = x + y
    sub = x - y
    return
}

返回值补充

当函数返回值类型为slice是,nil可以看做是一个有效的slice,没必要返回一个长度为0的切片。

func someFunc(x string) []int {
    if x == "" {
        return nil
    } else {
        res := make([]int,4)
        return res
    }
}

变量作用域

全局变量

全局变量指的是定义在函数外部的变量,程序运行期间一直都可以有效访问,与JavaScript的全局变量类似。

var commonNum int = 100 // 全局变量 commonNum
func main() {
    fmt.Println("num:", num) //100
}
func testCommonVar() {
    fmt.Println("num": num) //100
}

局部变量

如果全局变量与局部变量重名,那么优先访问局部变量。


var num = 1
func main() {
 num := 10
 fmt.Println(num) // 10
}

语句块定义的变量(类似JavaScript中的块级作用域),比如forif等,举个🌰

func testLocalVar(x, y int) {
    fmt.Println(x,y)// 函数的参数也只在函数内生效
    if x>0 {
      z := 10
      fmt.Println(z)// z只能在此处生效
    }
    fmt.Println(z) //不生效
}

函数类型与变量

定义函数类型

可以使用type关键字定义一个函数的类型,格式如下:

type calculation func(int, int) int

上面语句定义了一个calculation类型,是一种函数类型,这种函数接受两个int类型的参数并返回一个int类型的返回值。
也就是说,凡是满足这个条件的函数,都是calculation类型的函数,举个🌰

func add(x, y int) int {
    return x + y
}
func sub(x, y int) int {
    return x - y
}

add和sub都能赋值给calculation类型的变量。

var c calculation
c = add
c(3,4) //7

函数类型变量

可以声明函数类型的变量并且为该变量赋值

var c calculation
c = add
fmt.Printf("type of c: %T\n", c) // main.calculation
fmt.Println(c(1,2))

f := add
fmt.Printf("type of f: %T\n", f) // func(int, int) int
fmt.Println(c(2,3))

高阶函数

高阶函数分为函数作为参数函数作为返回值两部分

函数作为参数

func add(x, y int) int {
    return x+y
}
func calc(x, y int, callback func(int, int) int) int {
    return callback(10, 20)
}
func main() {
    res := calc(10, 20, add)
    fmt.Println(res) //30
}

函数作为返回值

func do(s string) (func(int, int) int, error) {
   switch s {
   case "+":
      return add, nil
   case "-":
      return sub, nil
   default:
      err := errors.New("无法识别的操作符")
      return nil, err
   }
}

匿名函数和闭包

匿名函数

格式如下:

func(参数)(返回值){
    函数体
}

匿名函数因为没有函数名,所以不能直接调用,需要保存到某个变量里,或者作为立即执行函数。

add := func(x, y) int {
    return x + y
}
add(10, 20)

func(x, y) {
    fmt.Println(x + y)
}(1, 2)

匿名函数多用于实现回调函数或者闭包

闭包

闭包值得是一个函数与其相关的引用环境组合而成的实体。简单来说就是:闭包=函数+引用环境,举个🌰

func add() func(int) int {
   var x int
   return func(y int) int {
       x += y
       return x
   }
}

f := add()
fmt.Println(f(10))//10
fmt.Println(f(20))//30

f1 := add()
fmt.Println(f1(20))//20
fmt.Println(f1(40))//60

与JavaScript相似,函数体内的变量如果被函数体以外的环境引用,就会形成闭包,该变量不会销毁。
闭包示例2:

func makeSuffixFunc(suffix string) func(string) string {
    return func(fileName string) string {
        if !strings.HasSuffix(fileName, suffix) {
            return fileName + suffix
        }
        return fileName
    }
}

闭包示例3:

func calc(base int) (func(int) int, func(int) int) {
    add := func(num int) int {
        base += num
        return base
    }
    sub := func(num int) int {
        base -= num
        return base
    }
    return add, sub
}
add, sub := calc(10)
fmt.Println(add(1)) // 11
fmt.Println(add(2)) // 9

defer语句

defer语句会将其后面跟孙的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先defer的后执行,后defer的先执行,举个🌰

fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println(4)
fmt.Println("end")
// start 4 end 3 2 1

多用于处理资源释放问题,例如:资源清理文件关闭解锁记录时间等。

defer的执行时机

返回值=x --> 运行defer --> ret指令

牢记defer的三条规则

  1. 延迟函数的参数在defer语句出现的时候就已经确定下来了
  2. 后进先出的规则
  3. 延迟函数可能会影响主函数的具名返回值

defer案例

// 主函数拥有匿名返回值,返回变量
//一个主函数拥有一个匿名的返回值,返回使用本地或全局变量,这种情况下defer语句可以引用到返回值,但不会改变返回值。
// 返回5
func f1() int {
	x := 5
	defer func() {
		x++
	}()
	return x 
}
// 主函数拥有具名返回值
// 主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果defer语句操作该返回值,可能会改变返回结果。
// x =5
// x++
// return
func f2() (x int) {
	defer func() {
		x++
	}()
	return 5
}
// 主函数拥有具名返回值
// x = 5
// y = x
// x++
// return
func f3() (y int) {
	x := 5
	defer func() {
		x++
	}()
	return x
}

// 主函数拥有具名返回值,但是延迟函数的参数从defer开始就确定了,相当于拷贝一份,所以后面的x++是自执行函数x的++
func f4() (x int) {
	defer func(x int) {
		x++
	}(x)
	return 5
}
func main() {
	fmt.Println(f1()) // 5
	fmt.Println(f2()) // 6
	fmt.Println(f3()) // 5
	fmt.Println(f4()) // 5
}

defer面试题

func calc(index string, a, b int) int {
	ret := a + b
	fmt.Println(index, a, b, ret)
	return ret
}

func main() {
	x := 1
	y := 2
	defer calc("AA", x, calc("A", x, y))
	x = 10
	defer calc("BB", x, calc("B", x, y))
	y = 20
}

// A 1 2 3
// B 10 2 12
// BB 10 12 22
// AA 1 3 4

内置函数

函数名介绍
close用来关闭channel
len用来求长度,比如string array slice map channel
new用来分配内存,主要用来分配值类型,比如int, struct。返回的是指针
make用来分配内存,主要用来分配引用类型,比如chan map slice
append用来追加元素到数组中、slice中
panic和recover用来做错误处理

panic/recover

panic用来处理错误,可以在任何地方引发,但recover只有在defer调用的函数中有效,举个🌰

func funcA() {
    fmt.Println("func A")
}
func funcB() {
    panic("func B error")
}
func funcC() {
    fmt.Println("func C")
}

func main() {
    funcA()
    funcB()
    funC()
}

这时候程序就会崩溃,因为funcB中引发了panic,但是,我们可以通过recover让程序起死回生!只需要对funcB做一下修改举个🌰

func funcB() {
    defer func(){
        err := recover()
        if err != nil {
            fmt.Println("func B")
        }
    }()
    panic("func B error")
}

注意

  1. recover必须搭配defer使用
  2. defer一定要再可能发panic的语句之前定义

练习题

  1. 分金币
/*
你有50枚金币,需要分配给以下几个人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。
分配规则如下:
a. 名字中每包含1个'e'或'E'分1枚金币
b. 名字中每包含1个'i'或'I'分2枚金币
c. 名字中每包含1个'o'或'O'分3枚金币
d: 名字中每包含1个'u'或'U'分4枚金币
写一个程序,计算每个用户分到多少金币,以及最后剩余多少金币?
程序结构如下,请实现 ‘dispatchCoin’ 函数
*/
var (
	coins = 50
	users = []string{
		"Matthew", "Sarah", "Augustus", "Heidi", "Emilie", "Peter", "Giana", "Adriano", "Aaron", "Elizabeth",
	}
	distribution = make(map[string]int, len(users))
)

func main() {
	left := dispatchCoin()
	fmt.Println("剩下:", left)
}

// 我的解法
func dispatchCoin() int {
   coinSum := coins
   for _,person := range users{
      distribution[person] = 0
      nameLower := strings.ToLower(person)
      nameSlice := strings.Split(nameLower, "")
      for _,nameWord := range nameSlice {
         switch nameWord {
            case "e":
               distribution[person] += 1
               coinSum-=1
            case "i":
               distribution[person] += 2
               coinSum-=2
            case "o":
               distribution[person] += 3
               coinSum-=3
            case "u":
               distribution[person] += 4
               coinSum-=4
            default:
               break
         }
      }
   }
   for key,value := range distribution{
      fmt.Printf("%v分到了:%v个\n",key, value)
   }
   return coinSum
}