Golang 基本语法(2)

198 阅读10分钟

Go的数组

Array数组介绍

数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素(element),这种类型可以是意的原始类型,比如int、string等,也可以是用户自定义的类型。一个数组包含的元素个数被称为数组的长度。在Golang中数组是一个长度固定的数据类型,数组的长度是类型的一部分,也就是说[5]int和[10]int是两个不同的类型。Golang中数组的另一个特点是占用内存的连续性,也就是说数组中的元素是被分配到连续的内存地址中的,因而索引数组元素的速度非常快。

和数组对应的类型是Slice(切片),Slice是可以增长和收缩的动态序列,功能也更灵活,但是想要理解slice工作原理的话需要先理解数组,所以本节主要为大家讲解数组的使用。

数组定义

var 数组变量名 [元素数量] T

示例

// 数组的长度是类型的一部分
var arr1 [3]int
var arr2 [4]string
fmt.Printf("%T, %T \n", arr1, arr2)
​
// 数组的初始化 第一种方法
var arr3 [3]int
arr3[0] = 1
arr3[1] = 2
arr3[2] = 3
fmt.Println(arr3)
​
// 第二种初始化数组的方法
var arr4 = [4]int {10, 20, 30, 40}
fmt.Println(arr4)
​
// 第三种数组初始化方法,自动推断数组长度
var arr5 = [...]int{1, 2}
fmt.Println(arr5)
​
// 第四种初始化数组的方法,指定下标
a := [...]int{1:1, 3:5}
fmt.Println(a)

遍历数组

方法1

// 第四种初始化数组的方法,指定下标
a := [...]int{1:1, 3:5}
for i := 0; i < len(a); i++ {
    fmt.Print(a[i], " ")
}

方法2

// 第四种初始化数组的方法,指定下标
a := [...]int{1:1, 3:5}
for _, value := range a {
    fmt.Print(value, " ")
}

数组的值类型

数组是值类型,赋值和传参会赋值整个数组,因此改变副本的值,不会改变本身的值

// 数组
var array1 = [...]int {1, 2, 3}
array2 := array1
array2[0] = 3
fmt.Println(array1, array2)

例如上述的代码,我们将数组进行赋值后,该改变数组中的值时,发现结果如下

[1 2 3] [3 2 3]

这就说明了,golang中的数组是值类型,而不是和java一样属于引用数据类型

切片定义(引用类型)

在golang中,切片的定义和数组定义是相似的,但是需要注意的是,切片是引用数据类型,如下

// 切片定义
var array3 = []int{1,2,3}
array4 := array3
array4[0] = 3
fmt.Println(array3, array4)

我们通过改变第一个切片元素,然后查看最后的效果

[3 2 3] [3 2 3]

二维数组

Go语言支持多维数组,我们这里以二维数组为例(数组中又嵌套数组):

var 数组变量名 [元素数量][元素数量] T

示例

// 二维数组
var array5 = [2][2]int{{1,2},{2,3}}
fmt.Println(array5)

数组遍历

二维数据组的遍历

// 二维数组
var array5 = [2][2]int{{1,2},{2,3}}
for i := 0; i < len(array5); i++ {
    for j := 0; j < len(array5[0]); j++ {
        fmt.Println(array5[i][j])
    }
}

遍历方式2

for _, item := range array5 {
    for _, item2 := range item {
        fmt.Println(item2)
    }
}

类型推导

另外我们在进行数组的创建的时候,还可以使用类型推导,但是只能使用一个 ...

// 二维数组(正确写法)
var array5 = [...][2]int{{1,2},{2,3}}

错误写法

// 二维数组
var array5 = [2][...]int{{1,2},{2,3}}

完整代码

package main
​
import "fmt"func main() {
    // 数组的长度是类型的一部分
    var arr1 [3]int
    var arr2 [4]string
    fmt.Printf("%T, %T \n", arr1, arr2)
​
    // 数组的初始化 第一种方法
    var arr3 [3]int
    arr3[0] = 1
    arr3[1] = 2
    arr3[2] = 3
    fmt.Println(arr3)
​
    // 第二种初始化数组的方法
    var arr4 = [4]int {10, 20, 30, 40}
    fmt.Println(arr4)
​
    // 第三种数组初始化方法,自动推断数组长度
    var arr5 = [...]int{1, 2}
    fmt.Println(arr5)
​
    // 第四种初始化数组的方法,指定下标
    a := [...]int{1:1, 3:5}
    fmt.Println(a)
​
    for i := 0; i < len(a); i++ {
        fmt.Print(a[i], " ")
    }
​
    for _, value := range a {
        fmt.Print(value, " ")
    }
​
    fmt.Println()
    // 值类型 引用类型
    // 基本数据类型和数组都是值类型
    var aa = 10
    bb := aa
    aa = 20
    fmt.Println(aa, bb)
​
    // 数组
    var array1 = [...]int {1, 2, 3}
    array2 := array1
    array2[0] = 3
    fmt.Println(array1, array2)
​
    // 切片定义
    var array3 = []int{1,2,3}
    array4 := array3
    array4[0] = 3
    fmt.Println(array3, array4)
​
    // 二维数组
    var array5 = [...][2]int{{1,2},{2,3}}
    for i := 0; i < len(array5); i++ {
        for j := 0; j < len(array5[0]); j++ {
            fmt.Println(array5[i][j])
        }
    }
​
    for _, item := range array5 {
        for _, item2 := range item {
            fmt.Println(item2)
        }
    }
}

Go的切片

为什么要使用切片

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。 它非常灵活,支持自动扩容。

切片是一个引用类型,它的内部结构包含地址、长度和容量。

声明切片类型的基本语法如下:

var name [] T

其中:

  • name:表示变量名
  • T:表示切片中的元素类型

举例

// 声明切片,把长度去除就是切片
var slice = []int{1,2,3}
fmt.Println(slice)

关于nil的认识

当你声明了一个变量,但却还并没有赋值时,golang中会自动给你的变量赋值一个默认的零值。这是每种类型对应的零值。

  • bool:false
  • numbers:0
  • string:""
  • pointers:nil
  • slices:nil
  • maps:nil
  • channels:nil
  • functions:nil

nil表示空,也就是数组初始化的默认值就是nil

var slice2 [] int
fmt.Println(slice2 == nil)

运行结果

true

切片的遍历

切片的遍历和数组是一样的

var slice = []int{1,2,3}
for i := 0; i < len(slice); i++ {
    fmt.Print(slice[i], " ")
}

基于数组定义切片

由于切片的底层就是一个数组,所以我们可以基于数组来定义切片

// 基于数组定义切片
a := [5]int {55,56,57,58,59}
// 获取数组所有值,返回的是一个切片
b := a[:]
// 从数组获取指定的切片
c := a[1:4]
// 获取 下标3之前的数据(不包括3)
d := a[:3]
// 获取下标3以后的数据(包括3)
e := a[3:]

运行结果

[55 56 57 58 59]
[55 56 57 58 59]
[56 57 58]
[55 56 57]
[58 59]

同理,我们不仅可以对数组进行切片,还可以切片在切片

切片的长度和容量

切片拥有自己的长度和容量,我们可以通过使用内置的len)函数求长度,使用内置的cap() 函数求切片的容量。

切片的长度就是它所包含的元素个数。

切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。切片s的长度和容量可通过表达式len(s)和cap(s)来获取。

举例

// 长度和容量
s := []int {2,3,5,7,11,13}
fmt.Printf("长度%d 容量%d\n", len(s), cap(s))
​
ss := s[2:]
fmt.Printf("长度%d 容量%d\n", len(ss), cap(ss))
​
sss := s[2:4]
fmt.Printf("长度%d 容量%d\n", len(sss), cap(sss))

运行结果

长度6 容量6
长度4 容量4
长度2 容量4

为什么最后一个容量不一样呢,因为我们知道,经过切片后sss = [5, 7] 所以切片的长度为2,但是一因为容量是从2的位置一直到末尾,所以为4

切片的本质

切片的本质就是对底层数组的封装,它包含了三个信息

  • 底层数组的指针
  • 切片的长度(len)
  • 切片的容量(cap)

举个例子,现在有一个数组 a := [8]int {0,1,2,3,4,5,6,7},切片 s1 := a[:5],相应示意图如下

image-20200720094247624.png 切片 s2 := a[3:6],相应示意图如下:

image-20200720094336749.png

使用make函数构造切片

我们上面都是基于数组来创建切片的,如果需要动态的创建一个切片,我们就需要使用内置的make函数,格式如下:

make ([]T, size, cap)

其中:

  • T:切片的元素类型
  • size:切片中元素的数量
  • cap:切片的容量

举例:

// make()函数创建切片
fmt.Println()
var slices = make([]int, 4, 8)
//[0 0 0 0]
fmt.Println(slices)
// 长度:4, 容量8
fmt.Printf("长度:%d, 容量%d", len(slices), cap(slices))

需要注意的是,golang中没办法通过下标来给切片扩容,如果需要扩容,需要用到append

slices2 := []int{1,2,3,4}
slices2 = append(slices2, 5)
fmt.Println(slices2)
// 输出结果 [1 2 3 4 5]

同时切片还可以将两个切片进行合并

// 合并切片
slices3 := []int{6,7,8}
slices2 = append(slices2, slices3...)
fmt.Println(slices2)
// 输出结果  [1 2 3 4 5 6 7 8]

需要注意的是,切片会有一个扩容操作,当元素存放不下的时候,会将原来的容量扩大两倍

使用copy()函数复制切片

前面我们知道,切片就是引用数据类型

  • 值类型:改变变量副本的时候,不会改变变量本身
  • 引用类型:改变变量副本值的时候,会改变变量本身的值

如果我们需要改变切片的值,同时又不想影响到原来的切片,那么就需要用到copy函数

// 需要复制的切片
var slices4 = []int{1,2,3,4}
// 使用make函数创建一个切片
var slices5 = make([]int, len(slices4), len(slices4))
// 拷贝切片的值
copy(slices5, slices4)
// 修改切片
slices5[0] = 4
fmt.Println(slices4)
fmt.Println(slices5)

运行结果为

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

删除切片中的值

Go语言中并没有删除切片元素的专用方法,我们可以利用切片本身的特性来删除元素。代码如下

// 删除切片中的值
var slices6 = []int {0,1,2,3,4,5,6,7,8,9}
// 删除下标为1的值
slices6 = append(slices6[:1], slices6[2:]...)
fmt.Println(slices6)

运行结果

[0 2 3 4 5 6 7 8 9]

切片的排序算法以及sort包

编写一个简单的冒泡排序算法

func main() {
    var numSlice = []int{9,8,7,6,5,4}
    for i := 0; i < len(numSlice); i++ {
        flag := false
        for j := 0; j < len(numSlice) - i - 1; j++ {
            if numSlice[j] > numSlice[j+1] {
                var temp = numSlice[j+1]
                numSlice[j+1] = numSlice[j]
                numSlice[j] = temp
                flag = true
            }
        }
        if !flag {
            break
        }
    }
    fmt.Println(numSlice)
}

在来一个选择排序

// 编写选择排序
var numSlice2 = []int{9,8,7,6,5,4}
for i := 0; i < len(numSlice2); i++ {
    for j := i + 1; j < len(numSlice2); j++ {
        if numSlice2[i] > numSlice2[j] {
            var temp = numSlice2[i]
            numSlice2[i] = numSlice2[j]
            numSlice2[j] = temp
        }
    }
}
fmt.Println(numSlice2)

对于int、float64 和 string数组或是切片的排序,go分别提供了sort.Ints()、sort.Float64s() 和 sort.Strings()函数,默认都是从小到大进行排序

var numSlice2 = []int{9,8,7,6,5,4}
sort.Ints(numSlice2)
fmt.Println(numSlice2)

降序排列

Golang的sort包可以使用 sort.Reverse(slic e) 来调换slice.Interface.Less,也就是比较函数,所以int、float64 和 string的逆序排序函数可以这样写

// 逆序排列
var numSlice4 = []int{9,8,4,5,1,7}
sort.Sort(sort.Reverse(sort.IntSlice(numSlice4)))
fmt.Println(numSlice4)

Golang map详解

map的介绍

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

Go语言中map的定义语法如下:

map[KeyType]ValueType

其中:

  • KeyType:表示键的类型
  • ValueType:表示键对应的值的类型

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

make:用于slice、map和channel的初始化

示例如下所示:

// 方式1初始化
var userInfo = make(map[string]string)
userInfo["userName"] = "zhangsan"
userInfo["age"] = "20"
userInfo["sex"] = "男"
fmt.Println(userInfo)
fmt.Println(userInfo["userName"])
// 创建方式2,map也支持声明的时候填充元素
var userInfo2 = map[string]string {
    "username":"张三",
    "age":"21",
    "sex":"女",
}
fmt.Println(userInfo2)

遍历map

使用for range遍历

// 遍历map
for key, value := range userInfo2 {
    fmt.Println("key:", key, " value:", value)
}

判断map中某个键值是否存在

我们在获取map的时候,会返回两个值,也可以是返回的结果,一个是是否有该元素

// 判断是否存在,如果存在  ok = true,否则 ok = false
value, ok := userInfo2["username2"]
fmt.Println(value, ok)

使用delete()函数删除键值对

使用delete()内建函数从map中删除一组键值对,delete函数的格式如下所示

delete(map 对象, key)

其中:

  • map对象:表示要删除键值对的map对象
  • key:表示要删除的键值对的键

示例代码如下

// 删除map数据里面的key,以及对应的值
delete(userInfo2, "sex")
fmt.Println(userInfo2)

元素为map类型的切片

我们想要在切片里面存放一系列用户的信息,这时候我们就可以定义一个元素为map类型的切片

// 切片在中存放map
var userInfoList = make([]map[string]string, 3, 3)
var user = map[string]string{
    "userName": "张安",
    "age": "15",
}
var user2 = map[string]string{
    "userName": "张2",
    "age": "15",
}
var user3 = map[string]string{
    "userName": "张3",
    "age": "15",
}
userInfoList[0] = user
userInfoList[1] = user2
userInfoList[2] = user3
fmt.Println(userInfoList)
​
for _, item := range userInfoList {
    fmt.Println(item)
}

值为切片类型的map

我们可以在map中存储切片

// 将map类型的值
var userinfo = make(map[string][]string)
userinfo["hobby"] = []string {"吃饭", "睡觉", "敲代码"}
fmt.Println(userinfo)

示例

统计字符串中单词出现的次数

// 写一个程序,统计一个字符串中每个单词出现的次数。比如 "how do you do"
var str = "how do you do"
array := strings.Split(str, " ")
fmt.Println(array)
countMap := make(map[string]int)
for _, item := range array {
    countMap[item]++
}
fmt.Println(countMap)

Go的函数

函数定义

函数是组织好的、可重复使用的、用于执行指定任务的代码块

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

Go语言中定义函数使用func关键字,具体格式如下:

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

其中:

  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也不能重名

示例

// 求两个数的和
func sumFn(x int, y int) int{
    return x + y
}
// 调用方式
sunFn(1, 2)

获取可变的参数,可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后面加... 来标识。

注意:可变参数通常要作为函数的最后一个参数

func sunFn2(x ...int) int {
    sum := 0
    for _, num := range x {
        sum = sum + num
    }
    return sum
}
// 调用方法
sunFn2(1, 2, 3, 4, 5, 7)

方法多返回值,Go语言中函数支持多返回值,同时还支持返回值命名,函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回

// 方法多返回值
func sunFn4(x int, y int)(sum int, sub int) {
    sum = x + y
    sub = x -y
    return
}

函数类型和变量

定义函数类型

我们可以使用type关键字来定义一个函数类型,具体格式如下

type calculation func(int, int) int

上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。

简单来说,凡是满足这两个条件的函数都是calculation类型的函数,例如下面的add 和 sub 是calculation类型

type calc func(int, int) int
// 求两个数的和
func sumFn(x int, y int) int{
    return x + y
}
func main() {
    var c calc
    c = add
}

方法作为参数

/**
    传递两个参数和一个方法
 */
func sunFn (a int, b int, sum func(int, int)int) int {
    return sum(a, b)
}

或者使用switch定义方法,这里用到了匿名函数

// 返回一个方法
type calcType func(int, int)int
func do(o string) calcType {
    switch o {
        case "+":
            return func(i int, i2 int) int {
                return i + i2
            }
        case "-":
            return func(i int, i2 int) int {
                return i - i2
            }
        case "*":
            return func(i int, i2 int) int {
                return i * i2
            }
        case "/":
            return func(i int, i2 int) int {
                return i / i2
            }
        default:
            return nil
​
    }
}
​
func main() {
    add := do("+")
    fmt.Println(add(1,5))
}

匿名函数

函数当然还可以作为返回值,但是在Go语言中,函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下

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

匿名函数因为没有函数名,所以没有办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:

func main() {
    func () {
        fmt.Println("匿名自执行函数")
    }()
}

Golang中的闭包

全局变量和局部变量

全局变量的特点:

  • 常驻内存
  • 污染全局

局部变量的特点

  • 不常驻内存
  • 不污染全局

闭包

  • 可以让一个变量常驻内存
  • 可以让一个变量不污染全局

闭包可以理解成 “定义在一个函数内部的函数”。在本质上,闭包就是将函数内部 和 函数外部连接起来的桥梁。或者说是函数和其引用环境的组合体。

  • 闭包是指有权访问另一个函数作用域中的变量的函数
  • 创建闭包的常见的方式就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量

注意:由于闭包里作用域返回的局部变量资源不会被立刻销毁,所以可能会占用更多的内存,过度使用闭包会导致性能下降,建议在非常有必要的时候才使用闭包。

// 闭包的写法:函数里面嵌套一个函数,最后返回里面的函数就形成了闭包
func adder() func() int {
    var i = 10
    return func() int {
        return i + 1
    }
}
​
func main() {
    var fn = adder()
    fmt.Println(fn())
    fmt.Println(fn())
    fmt.Println(fn())
}

最后输出的结果

11
11
11

另一个闭包的写法,让一个变量常驻内存,不污染全局

func adder2() func(y int) int {
    var i = 10
    return func(y int) int {
        i = i + y
        return i
    }
}
​
func main() {
    var fn2 = adder2()
    fmt.Println(fn2(10))
    fmt.Println(fn2(10))
    fmt.Println(fn2(10))
}

defer语句

Go 语言中的defer 语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

// defer函数
fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
fmt.Println("4")

defer将会延迟执行

1
3
4
2

如果有多个defer修饰的语句,将会逆序进行执行

// defer函数
fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("4")

运行结果

1
4
3
2

如果需要用defer运行一系列的语句,那么就可以使用匿名函数

func main() {
    fmt.Println("开始")
    defer func() {
        fmt.Println("1")
        fmt.Println("2")
    }()
    fmt.Println("结束")
}

运行结果

开始
结束
1
2

defer执行时机

在Go语言的函数中return语句在底层并不是原子操作,它分为返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前,具体如下图所示

image-20200720220700249.png

panic/revocer处理异常

Go语言中是没有异常机制,但是使用panic / recover模式来处理错误

  • panic:可以在任何地方引发
  • recover:只有在defer调用函数内有效
func fn1() {
    fmt.Println("fn1")
}
​
func fn2() {
    panic("抛出一个异常")
}
func main() {
    fn1()
    fn2()
    fmt.Println("结束")
}

上述程序会直接抛出异常,无法正常运行

fn1
panic: 抛出一个异常

解决方法就是使用 recover进行异常的监听

func fn1() {
    fmt.Println("fn1")
}
​
func fn2() {
    // 使用recover监听异常
    defer func() {
        err := recover()
        if err != nil {
            fmt.Println(err)
        }
    }()
    panic("抛出一个异常")
}
func main() {
    fn1()
    fn2()
    fmt.Println("结束")
}

异常运用场景

模拟一个读取文件的方法,这里可以主动发送使用panic 和 recover

func readFile(fileName string) error {
    if fileName == "main.go" {
        return nil
    } else {
        return errors.New("读取文件失败")
    }
}
​
func myFn () {
    defer func() {
        e := recover()
        if e != nil {
            fmt.Println("给管理员发送邮件")
        }
    }()
    err := readFile("XXX.go")
    if err != nil {
        panic(err)
    }
}
​
func main() {
    myFn()
}

内置函数

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