go语言基础学习

go

Go 语言环境安装

Go 语言支持以下系统:

  • Linux

  • FreeBSD

  • Mac OS X(也称为 Darwin)

  • Windows

安装包下载地址为:All releases - The Go Programming Language

如果打不开可以使用这个地址:All releases - The Go Programming Language

各个系统对应的包名:

操作系统包名
Windowsgo1.4.windows-amd64.msi
Linuxgo1.4.linux-amd64.tar.gz
Macgo1.4.darwin-amd64-osx10.8.pkg
FreeBSDgo1.4.freebsd-amd64.tar.gz

UNIX/Linux/Mac OS X, 和 FreeBSD 安装

以下介绍了在UNIX/Linux/Mac OS X, 和 FreeBSD系统下使用源码安装方法:

1、下载二进制包:go1.4.linux-amd64.tar.gz。

2、将下载的二进制包解压至 /usr/local目录。

tar -C /usr/local -xzf go1.4.linux-amd64.tar.gz

3、将 /usr/local/go/bin 目录添加至 PATH 环境变量:

export PATH=$PATH:/usr/local/go/bin

以上只能暂时添加 PATH,关闭终端下次再登录就没有了。

我们可以编辑 ~/.bash_profile 或者 /etc/profile,并将以下命令添加该文件的末尾,这样就永久生效了:

export PATH=$PATH:/usr/local/go/bin

添加后需要执行:

source ~/.bash_profile
或
source /etc/profile

注意:MAC 系统下你可以使用 .pkg 结尾的安装包直接双击来完成安装,安装目录在 /usr/local/go/ 下。


Windows 系统下安装

Windows 下可以使用 .msi 后缀(在下载列表中可以找到该文件,如go1.4.2.windows-amd64.msi)的安装包来安装。

默认情况下 .msi 文件会安装在 c:\Go 目录下。你可以将 c:\Go\bin 目录添加到 Path 环境变量中。添加后你需要重启命令窗口才能生效。

安装测试

创建工作目录 C:>Go_WorkSpace

test.go 文件代码:

package mainimport "fmt"func main() { fmt.Println("Hello, World!")}

使用 go 命令执行以上代码输出结果如下:

C:\Go_WorkSpace>go run test.go
​
Hello, World!

基础语法

1. 注释

单行注释//

多行注释/*

*/

2. 变量

定义变量使用var关键字

var name string = "kuile"
​

var定义变量 name变量名 string类型

2.1 定义多个变量
    var (
        name string
        age  int
        addr string
    )

string默认值为空

int默认值是0

2.2 短变量的声明初始化
    //自动推导
    name := ""
    age := 2
    addr := false
​

使用受限制

  • 定义变量,同时显示初始化

  • 不能提供数据类型

  • 只能在函数内部,不能随便导出定义

3. 打印内存地址

func main() {
​
    var num int
    fmt.Printf("num:%d,num%p", num, &num) //取地址符 &变量名
}
3.1 变量交换
func main() {
    a := 100
    b := 200
    b, a = a, b
    fmt.Println(a, b)
}
​
3.2 匿名变量

匿名变量的特点是一个下划线"_",本身就是一个特殊的空白表示符,可以向其他标识符那样用于变量的声明和赋值,但任何赋给这个标识符的值都将被抛弃

package main
​
import "fmt"
​
func test() (int, int) {
    return 100, 200
}
func main() {
    a, _ := test()
    fmt.Println(a)
}
​
3.4 变量的作用域

在函数体里面定义的是局部变量

没有在函数体里面定义的变量是全局变量

局部变量和全局变量可以同名,但是在函数体内优先局部变量

4. 常量

常量的定义使用const关键字

可以省略类型说明符 ,编译器可以根据变量自动推断类型

  • 显示类型定义const b string ="abc"

  • 隐式类型定义 const b="abc"

  • 同时定义多个常量const a,b,c="afsd",3,"d"

4.1 iota

iota,特殊常量可认为是一个可以编译器修改的常量,iota是go语言的常量计数器

iota可以作为枚举值

func main() {
    const (
        a = iota //0
        b = iota //1
        c = iota //2
    )
}

第一个iota等于0,每当iota在新的一行被使用时,它的值就会自动加1;所以a=0.b=1,c=2

如果第一个是iota,后面的会延续第一个知道下一个iota出现的时候会重置为0

5. 定义数据类型

go语言是静态类型的语言,在go编程语言中,数据类型用于声明函数和变量。数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候大数据的时候需要申请大内存,就可以充分利用内存,编译器的时候,就要知道每个值得类型

5.1 布尔型

布尔型的值可以是常量true或者false。一个简单的例子:var b bool =true

    func main() {
        var a bool = true
        var b bool = false
        fmt.Printf("%T,%t\n", a, a)
        fmt.Printf("%T,%t\n", b, b)
    }
​

默认值为true

5.2 数字型

整形int和浮点形float32、float64,go语言支持整型和浮点型数字,并且支持复数,其中的运算采用补码

go语言也有基于架构的类型,例如uint无符号、int有符号

数字类型

Go 也有基于架构的类型,例如:int、uint 和 uintptr。

序号类型和描述
1uint8无符号 8 位整型 (0 到 255)
2uint16无符号 16 位整型 (0 到 65535)
3uint32无符号 32 位整型 (0 到 4294967295)
4uint64无符号 64 位整型 (0 到 18446744073709551615)
5int8有符号 8 位整型 (-128 到 127)
6int16有符号 16 位整型 (-32768 到 32767)
7int32有符号 32 位整型 (-2147483648 到 2147483647)
8int64有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

浮点型

序号类型和描述
1float32IEEE-754 32位浮点型数
2float64IEEE-754 64位浮点型数
3complex6432 位实数和虚数
4complex12864 位实数和虚数

其他数字类型

以下列出了其他更多的数字类型:

序号类型和描述
1byte类似 uint8
2rune类似 int32
3uint32 或 64 位
4int与 uint 一样大小
5uintptr无符号整型,用于存放一个指针

6. 运算符

Go 语言内置的运算符有:

    算术运算符
    关系运算符
    逻辑运算符
    位运算符
    赋值运算符
1.1.1. 算数运算符
运算符描述
+相加
-相减
*相乘
/相除
%求余

注意: ++(自增)和--(自减)在Go语言中是单独的语句,并不是运算符。

1.1.2. 关系运算符
运算符描述
==检查两个值是否相等,如果相等返回 True 否则返回 False。
!=检查两个值是否不相等,如果不相等返回 True 否则返回 False。
>检查左边值是否大于右边值,如果是返回 True 否则返回 False。
>=检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。
<检查左边值是否小于右边值,如果是返回 True 否则返回 False。
<=检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。
1.1.3. 逻辑运算符
运算符描述
&&逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。
ll逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。
!逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。
1.1.4. 位运算符

位运算符对整数在内存中的二进制位进行操作。

运算符描述
&参与运算的两数各对应的二进位相与。(两位均为1才为1)
l参与运算的两数各对应的二进位相或。(两位有一个为1就为1)
^参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。(两位不一样则为1)
<<左移n位就是乘以2的n次方。“a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。
>>右移n位就是除以2的n次方。“a>>b”是把a的各二进位全部右移b位。
1.1.5. 赋值运算符
运算符描述
=简单的赋值运算符,将一个表达式的值赋给一个左值
+=相加后再赋值
-=相减后再赋值
*=相乘后再赋值
/=相除后再赋值
%=求余后再赋值
<<=左移后赋值
>>=右移后赋值
&=按位与后赋值
l=按位或后赋值
^=按位异或后赋值

键盘输入输出

输出
func main() {
    var x int
    var y float64
    //  定义了两个变量想用键盘来录入这两个变量
    fmt.Println() //打印并换行
    fmt.Printf()  //格式化输出
    fmt.Print()   //打印输出
}
输入
func main() {
    var x int
    var y float64
    var c float32
    //  定义了两个变量想用键盘来录入这两个变量
    //接受用地址接收
    fmt.Scan(&x)   //键盘输入
    fmt.Scanf(&y)  //格式化输入
    fmt.Scanln(&c) //输入并换行
}
​

流程控制

程序的流程控制一共有三种:顺序结构,选择结构,循环结构

顺序结构:从上到下,逐行执行。默认的逻辑

选择结构:条件满足某些代码才会执行

  • if

  • swich

  • select

循环结构:条件满足某些条件某些代码会被反复执行0-n次

  • for

if语句

条件语句需要开发者执行一个或多个条件,并通过测试条件是否为true来决定是否执行指定的语句,并在条件为false的情况在执行另外的语句

下图展示了程序语言条件语句的结构:

package main
​
import "fmt"
​
func main() {
    var a int = 15
    if a > 20 {
        fmt.Println("a>20")
    }
    if a > 10 {
        fmt.Println("a>10")
    }
}
​
swich语句
package main
​
import "fmt"
​
func main() {
    var score int = 90
    switch score {
    case 90:
        fmt.Println("A")
    case 80:
        fmt.Println("B")
    case 50, 60, 70:
        fmt.Println("C")
    default:
        fmt.Println("D")
​
    }
}
​
falthrough穿透

不管下一个条件满不能满足都会执行

case穿透

package main
​
import "fmt"
​
func main() {
    a := false
    switch a {
    case false:
        fmt.Println("case执行条件为false")
        fallthrough
    case true:
        fmt.Println("case执行条件为true")
    }
}
​

终止穿透

package main
​
import "fmt"
​
func main() {
    a := false
    switch a {
    case false:
        fmt.Println("case执行条件为false")
        fallthrough
    case true:
        if a == false {
            break
        }
        fmt.Println("case执行条件为true")
    }
}
​
循环控制
  • for

    语法

    //for给控制变量赋初值 循环条件 给控制变量增量或减量
    for i :=1;i<=5;i++{
    ​
    }

    案列

    1累加到10

    package main
    ​
    import "fmt"
    ​
    func main() {
        var sum int = 1
        for i := 1; i <= 10; i++ {
            sum += i
    ​
        }
        fmt.Println(sum)
    }
    ​

    for赋值和控制变量可以省略

    package main
    ​
    import "fmt"
    ​
    func main() {
        var sum int = 1
        i := 1
        for i <= 10 {
            sum += i
            i++
        }
        fmt.Println(sum)
    }
    ​

    控制条件也可以省略会死循环

    在循环未终止的时候退出循环

    break

    break结束当前整个循环

    package main
    ​
    import "fmt"
    ​
    func main() {
        for i := 1; i < 10; i++ {
            if i == 5 {
                break
            }
            fmt.Println(i)
    ​
        }
    }
    ​

    continue

    continue结束本次循环,继续下一次循环

    package main
    ​
    import "fmt"
    ​
    func main() {
        for i := 0; i < 10; i++ {
            if i == 5 {
                continue
            }
            fmt.Println(i)
        }
    }
    ​

练习 - 使用数组

已完成100 XP

  • 8 分钟

Go 中的数组是一种特定类型且长度固定的数据结构。 它们可具有零个或多个元素,你必须在声明或初始化它们时定义大小。 此外,它们一旦创建,就无法调整大小。 鉴于这些原因,数组在 Go 程序中并不常用,但它们是切片和映射的基础。

声明数组

要在 Go 中声明数组,必须定义其元素的数据类型以及该数组可容纳的元素数目。 然后,可采用下标表示法访问数组中的每个元素,其中第一个元素是 0,最后一个元素是数组长度减去 1(长度 - 1)。

例如,让我们使用以下代码:

Go复制

package main
​
import "fmt"
​
func main() {
    var a [3]int
    a[1] = 10
    fmt.Println(a[0])
    fmt.Println(a[1])
    fmt.Println(a[len(a)-1])
}

运行上述代码时,你会获得以下输出:

输出复制

0
10
0

即使已声明数组,访问其元素时也不会遇到错误。 默认情况下,Go 会用默认数据类型初始化每个元素。 这样的话,int 的默认值为零。 不过,你可为特定位置分配值,就像我们对 a[1] = 10 所做的那样。 你可采用上述表示法来访问该元素。 另请注意,为了引用第一个元素,我们使用了 a[0]。 为了引用最后一个元素,我们使用了 a[len(a)-1]len 函数是 Go 中的内置函数,用于获取数组、切片或映射中的元素数。

初始化数组

声明数组时,还可使用非默认值来初始化数组。 例如,你可使用以下代码来查看和测试语法:

Go复制

package main
​
import "fmt"
​
func main() {
    cities := [5]string{"New York", "Paris", "Berlin", "Madrid"}
    fmt.Println("Cities:", cities)
}

运行上述代码,你应会看到以下输出:

输出复制

Cities: [New York Paris Berlin Madrid ]

即使数组应具有 5 个元素,也无需为所有元素分配值。 如上所示,最后一个位置包含一个空的字符串,因为它是字符串数据类型的默认值。

数组中的省略号

如果你不知道你将需要多少个位置,但知道数据元素集,那么还有一种声明和初始化数组的方法是使用省略号 (...),如下例所示:

Go复制

q := [...]int{1, 2, 3}


让我们修改在上一部分中使用的程序,以使用省略号。 代码应如下例所示:

Go复制

package main

import "fmt"

func main() {
    cities := [...]string{"New York", "Paris", "Berlin", "Madrid"}
    fmt.Println("Cities:", cities)
}


运行上述代码,你应看到如下例所示的输出:

输出复制

Cities: [New York Paris Berlin Madrid]


你能看出区别吗? 末尾没有空字符串。 数组长度由你初始化它时输入的字符串决定。 如果你不再需要,则不保留你不知道的内存。

另一种有趣的数组初始化方法是使用省略号并仅为最后一个位置指定值。 例如,使用以下代码:

Go复制

package main

import "fmt"

func main() {
    numbers := [...]int{99: -1}
    fmt.Println("First Position:", numbers[0])
    fmt.Println("Last Position:", numbers[99])
    fmt.Println("Length:", len(numbers))
}


运行此代码,你将获得以下输出:

输出复制

First Position: 0
Last Position: -1
Length: 100


请注意数组的长度是 100,因为你为第 99 个位置指定了一个值。 第一个位置打印默认值(零)。

多维数组

如果需要处理复杂数据结构,请记住 Go 支持多维数组。 让我们创建一个程序,在其中声明和初始化一个二维数组。 使用以下代码:

Go复制

package main

import "fmt"

func main() {
    var twoD [3][5]int
    for i := 0; i < 3; i++ {
        for j := 0; j < 5; j++ {
            twoD[i][j] = (i + 1) * (j + 1)
        }
        fmt.Println("Row", i, twoD[i])
    }
    fmt.Println("\nAll at once:", twoD)
}


运行上述程序,你应会看到如下例所示的输出:

输出复制

Row 0 [1 2 3 4 5]
Row 1 [2 4 6 8 10]
Row 2 [3 6 9 12 15]

All at once: [[1 2 3 4 5] [2 4 6 8 10] [3 6 9 12 15]]


你声明了一个二维数组,它指定了数组在第二个维度中的位置数,例如 var twoD [3][5]int。 你可将此数组看做一种包含列和行的数据结构,例如电子表格或矩阵。 此时,所有位置均采用默认值(零)。 在 for 循环中,我们将在每行上使用不同的值模式初始化每个位置。 最后,将其所有值打印到终端。

如果要声明一个三维数组,会怎么样? 你可能会猜测要使用的语法是什么,对吧? 可如下例所示执行此操作:

Go复制

package main

import "fmt"

func main() {
    var threeD [3][5][2]int
    for i := 0; i < 3; i++ {
        for j := 0; j < 5; j++ {
            for k := 0; k < 2; k++ {
                threeD[i][j][k] = (i + 1) * (j + 1) * (k + 1)
            }
        }
    }
    fmt.Println("\nAll at once:", threeD)
}


运行上述代码,你应会看到如下例所示的输出:

输出复制

All at once: [[[1 2] [2 4] [3 6] [4 8] [5 10]] [[2 4] [4 8] [6 12] [8 16] [10 20]] [[3 6] [6 12] [9 18] [12 24] [15 30]]]


如果用可读性更强的格式来设置输出格式,则可能得到如下例所示的内容:

输出复制

All at once: 
[
    [
        [1 2] [2 4] [3 6] [4 8] [5 10]
    ] 
    [
        [2 4] [4 8] [6 12] [8 16] [10 20]
    ] 
    [
        [3 6] [6 12] [9 18] [12 24] [15 30]
    ]
]


请注意与二维数组相比结构的变化。 可根据需要继续尝试更多维度,但由于我们还有其他数据类型需要探索,因此暂时介绍到这里。

练习 - 了解切片

已完成100 XP

  • 12 分钟

我们在上一部分了解了数组,并了解了数组是切片和映射的基础。 你稍后就会明白是为什么。 与数组一样,切片也是 Go 中的一种数据类型,它表示一系列类型相同的元素。 不过,与数组更重要的区别是切片的大小是动态的,不是固定的。

切片是数组或另一个切片之上的数据结构。 我们将源数组或切片称为基础数组。 通过切片,可访问整个基础数组,也可仅访问部分元素。

切片只有 3 个组件:

  • 指向基础数组中第一个可访问元素的指针。 此元素不一定是数组的第一个元素 array[0]

  • 切片的长度。 切片中的元素数目。

  • 切片的容量。 切片开头与基础数组结束之间的元素数目。

下图显示了什么是切片:

请注意,切片只是基础数组的一个子集。 让我们看看如何用代码来表示上述图像。

声明和初始化切片

要声明切片,可采用与声明数组相同的方式操作。 例如,以下代码表示你在切片图像中看到的内容:

Go复制

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    fmt.Println(months)
    fmt.Println("Length:", len(months))
    fmt.Println("Capacity:", cap(months))
}


运行代码时,你会看到以下输出:

输出复制

[January February March April May June July August September October November December]
Length: 12
Capacity: 12


请注意目前,切片与数组的区别不大。 可用相同的方式声明这两者。 若要从切片中获取信息,可使用内置函数 len()cap()。 我们将继续使用这些函数来确认切片可具有来自基础数组的后续元素。

切片项

Go 支持切片运算符 s[i:p],其中:

  • s 表示数组。

  • i 表示指向要添加到新切片的基础数组(或另一个切片)的第一个元素的指针。 变量 i 对应于数组 array[i] 中索引位置 i 处的元素。 请记住,此元素不一定是基础数组的第一个元素 array[0]

  • p 表示创建新切片时要使用的基础数组中的元素数目,也表示元素位置。 变量 p 对应于可用于新切片的基础数组中的最后一个元素。 可在位置 array[i+1] 找到基础数组中位置 p 处的元素。 请注意,此元素不一定是基础数组的最后一个元素 array[len(array)-1]

因此,切片只能引用元素的子集。

假设你需要 4 个变量来表示一年的每个季度,并且你有一个包含 12 个元素的 months 切片。 下图演示了如何将 months 切片为 4 个新的 quarter 切片:

若要用代码表示在上图中看到的内容,可使用以下代码:

Go复制

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter1 := months[0:3]
    quarter2 := months[3:6]
    quarter3 := months[6:9]
    quarter4 := months[9:12]
    fmt.Println(quarter1, len(quarter1), cap(quarter1))
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter3, len(quarter3), cap(quarter3))
    fmt.Println(quarter4, len(quarter4), cap(quarter4))
}


运行代码时,你会获得以下输出:

输出复制

[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3


请注意,切片的长度不变,但容量不同。 我们来了解 quarter2 切片。 声明此切片时,你指出希望切片从位置编号 3 开始,最后一个元素位于位置编号 6。 切片长度为 3 个元素,但容量为 9,原因是基础数组有更多元素或位置可供使用,但对切片而言不可见。 例如,如果你尝试打印类似 fmt.Println(quarter2[3]) 的内容,会出现以下错误:panic: runtime error: index out of range [3] with length 3

切片容量仅指出切片可扩展的程度。 因此,你可从 quarter2 创建扩展切片,如下例所示:

Go复制

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter2 := months[3:6]
    quarter2Extended := quarter2[:4]
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
}


运行上述代码时,你会获得以下输出:

输出复制

[April May June] 3 9
[April May June July] 4 9


请注意在声明 quarter2Extended 变量时,无需指定初始位置 ([:4])。 执行此操作时,Go 会假定你想要切片的第一个位置。 你可对最后一个位置 ([1:]) 执行相同的操作。 Go 将假定你要引用所有元素,直到切片的最后位置 (len()-1)。

追加项

我们了解了切片的工作原理,还学习了它们与数组的相似性。 现在,让我们来了解它们与数组之间有何不同。 第一个区别是切片的大小不是固定的,而是动态的。 创建切片后,可向其添加更多元素,这样切片就会扩展。 稍后你将了解基础数组发生的情况。

Go 提供了内置函数 append(slice, element),便于你向切片添加元素。 将要修改的切片和要追加的元素作为值发送给该函数。 然后,append 函数会返回一个新的切片,将其存储在变量中。 对于要更改的切片,变量可能相同。

让我们看一下追加进程在代码中的显示方式:

Go复制

package main

import "fmt"

func main() {
    var numbers []int
    for i := 0; i < 10; i++ {
        numbers = append(numbers, i)
        fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
    }
}


运行上述代码时,你应会看到以下输出:

输出复制

0       cap=1   [0]
1       cap=2   [0 1]
2       cap=4   [0 1 2]
3       cap=4   [0 1 2 3]
4       cap=8   [0 1 2 3 4]
5       cap=8   [0 1 2 3 4 5]
6       cap=8   [0 1 2 3 4 5 6]
7       cap=8   [0 1 2 3 4 5 6 7]
8       cap=16  [0 1 2 3 4 5 6 7 8]
9       cap=16  [0 1 2 3 4 5 6 7 8 9]


此输出很有意思。 特别是对于调用 cap() 函数所返回的内容。 一切看起来都很正常,直到第 3 次迭代,此时容量变为 4,切片中只有 3 个元素。 在第 5 次迭代中,容量又变为 8,第 9 次迭代时变为 16。

你注意到容量输出中的模式了吗? 当切片容量不足以容纳更多元素时,Go 的容量将翻倍。 它将新建一个具有新容量的基础数组。 无需执行任何操作即可使容量增加。 Go 会自动扩充容量。 需要谨慎操作。 有时,一个切片具有的容量可能比它需要的多得多,这样你将会浪费内存。

删除项

你可能想知道,删除元素会怎么样呢? Go 没有内置函数用于从切片中删除元素。 可使用上述切片运算符 s[i:p] 来新建一个仅包含所需元素的切片。

例如,以下代码会从切片中删除元素:

Go复制

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    remove := 2

	if remove < len(letters) {

		fmt.Println("Before", letters, "Remove ", letters[remove])

		letters = append(letters[:remove], letters[remove+1:]...)

		fmt.Println("After", letters)
	}

}


运行上述代码时,你会获得以下输出:

输出复制

Before [A B C D E] Remove  C
After [A B D E]


此代码会从切片中删除元素。 它用切片中的下一个元素替换要删除的元素,如果删除的是最后一个元素,则不替换。

另一种方法是创建切片的新副本。 在下一部分中,我们将了解如何创建切片的副本。

创建切片的副本

Go 具有内置函数 copy(dst, src []Type) 用于创建切片的副本。 你需要发送目标切片和源切片。 例如,你可如下例所示创建一个切片副本:

Go复制

slice2 := make([]string, 3)
copy(slice2, letters[1:4])


为何要创建副本? 更改切片中的元素时,基础数组将随之更改。 引用该基础数组的任何其他切片都会受到影响。 让我们在代码中看看此过程,然后创建一个切片副本来解决此问题。

使用下述代码确认切片指向数组,而你在切片中所做的每个更改都会影响基础数组。

Go复制

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]
    slice2 := letters[1:4]

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}


运行上述代码时,你会看到以下输出:

输出复制

Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]


请注意对 slice1 所做的更改如何影响 letters 数组和 slice2。 可在输出中看到字母 B 已替换为 Z,它会影响指向 letters 数组的每个切片。

若要解决此问题,你需要创建一个切片副本,它会在后台生成新的基础数组。 可以使用以下代码:

Go复制

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]

    slice2 := make([]string, 3)
    copy(slice2, letters[1:4])

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}


运行上述代码时,你会看到以下输出:

输出复制

Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]


请注意 slice1 中的更改如何影响基础数组,但它并未影响新的 slice2

String

String在Go语言中是一个字节的切片,可以通过将其内容封装在""中类创建字符串,Go中的字符串式Unicode兼容的,并且式UTF-8编码

​
func main() {
    str := "helloxuexiangban"
    for i := 0; i < len(str); i++ {
        fmt.Printf("%c\n", str[i])
    }
}
​

函数

什么是函数
  • 函数式基本的代码块,用于执行一个任务

  • Go语言最少有一个main()函数

  • 你可以通过函数来划分不同功能,逻辑上每个函数执行的式指定的任务

  • 函数声明告诉了编译器函数的名称,返回类型,和参数

函数的声明

Go语言函数定义格式如下:

func function_name([parameter list]) [return_type]{
//      函数体
}
​
​
  • 无参无返回值函数

  • 有一个参数的函数

  • 有两个参数的函数

  • 有一个返回值的函数

  • 有多个返回值的函数

完整的函数
package main
​
import "fmt"
​
func main() {
    fmt.Println(add(1, 2))
}
​
//func 函数名(参数,参数,……)函数调用后的返回值{
//  函数体:执行一段代码
//  return 返回结果
//}
func add(a, b int) int {
    return a + b
}
​
package main
​
import (
    "fmt"
)
​
func main() {
    myprintinfo()
    myprintint("fga")
    add(1, 2)
    x, y := swap("xue", "kuang")
    fmt.Println(x, y)
}
​
//- 无参无返回值函数
func myprintinfo() {
    fmt.Println("myprintinfo")
}
​
//- 有一个参数的函数
func myprintint(msg string) {
    fmt.Println(msg)
}
​
//- 有两个参数的函数
func add2(a, b int) {
    fmt.Println("add2")
​
}
​
//- 有一个返回值的函数
func add3(a, b int) int {
    c := a + b
    return c
}
​
//- 有多个返回值的函数
func swap(x, y string) (string, string) {
    return y, x
}
​
​
​
​

一个函数上有返回值,那么函数中必须使用return语句

调用出需要使用变量接收该结果

形参

形参:定义函数式,用来接收外部接入数据的参数,就是形式参数

实参

实参:调用参数时,传给形参的实际数据叫做实际参数

可变参数

概念

一个函数的参数类型确定,但个数不确定,就可以使用可变参数

func myfunc(arg ...int){
    
}
//arg ..int 告诉我们go这个函数接收补丁数量的参数,类型全部时int
package main
​
import "fmt"
​
func getSum(nums ...int) {
    sum := 0
    for i := 0; i < len(nums); i++ {
        sum += nums[i]
    }
    fmt.Println(sum)
}
func main() {
    getSum(1, 2, 3, 4, 56, 67, 34, 12)
}
​

注意事项:

  • 如果一个函数的参数是可变参数,同时还有其他的参数,可变参数要放在列表的最后

  • 一个函数的参数列表中最多智能有一个可变参数

参数传递

按照数据的存储特点来分:

  • 值类型的数据:操作的是数据本身、int、string、bool、float64、array……

  • 引用类型的数据:操作的是数据地址 slice、map、chan……

值传递
package main
​
import "fmt"
​
func main() {
    //值传递
    //定义一个数组[个数]类型
    arr := [4]int{1, 2, 3, 4}
    fmt.Println(arr)
    //传递:拷贝arr
    //值传递:传递的是数据的副本,修改数据,对于原始的数据没有影响
    //值传递的数据,默认都是值传递:基础类型、array、struct
    update(arr)
}
func update(arr2 [4]int) {
    fmt.Println(arr2)
    arr2[0] = 100
    fmt.Println(arr2)
}
​
引用类型
package main
​
import "fmt"
​
//引用传递
func main() {
​
    //  切片,可以扩容的数组,不需要写大小
    s1 := []int{1, 2, 3, 4, 5}
    fmt.Println(s1)
    //传入的是引用类型的数据,地址
    update2(s1)
    fmt.Println(s1)
​
}
func update2(s2 []int) {
    fmt.Println(s2)
    s2[0] = 100
    fmt.Println(s2)
}
​

函数变量的作用域

作用域:变量可以使用的范围

局部变量:函数内部定义的变量叫做局部变量

全局变量:函数外部定义的变量,叫做全局变量

在函数内部定义的变量都只能在函数体内使用,不能使用其他函数定义的变量

局部变量遵循就近原则

递归函数

定义:一个函数自己调用自己,就叫做递归函数

注意:递归函数需要有一个出口,逐渐像出口靠近,没有出口就会形成死循环

package main
​
import "fmt"
​
func main() {
    var sum int = getSum1(50)
    fmt.Println(sum)
}
func getSum1(n int) int {
    if n == 1 {
        return 1
    }
    return getSum1(n-1) + n
}
​

defer

语义

推迟、延迟

在go语言中,使用defer关键字来延迟一个函数或者方法的执行、

package main
​
import "fmt"
​
func main() {
    f("1")
    defer f("3")
    fmt.Println(4)
}
func f(s string) {
    fmt.Println(s)
}
​

defer函数或者方法:一个函数或方法的执行被延迟了

  • 可以在函数中写多个defer语句,当函数执行到最后时,这些defer语句会按照逆序执行,最后函数返回,特别时当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源

  • 如果有很多调用的defer,那么defer是采用后进先出(栈)模式

多个defer的执行顺序

多个defer出现的时候,它会把defer之后的函数压入一个栈中延迟执行,也就是先进后出(LIFO),写在前面的defer会比写在后面的defer调用的晚。下面通过一个示例看一下:

func func1(){
    fmt.Println("我是 func1")
}
func func2(){
    fmt.Println("我是 func2")
}
func func3(){
    fmt.Println("我是 func3")
}
func main(){
    defer func1()
    defer func2()
    defer func3()
    fmt.Println("main1")
    fmt.Println("main2")
}

执行输出如下:

main1

main2

我是 func3

我是 func2

我是 func1

通过图示一看就很明白了

defer下的函数参数包含子函数
javascript
复制代码package main
 
import "fmt"
 
func function(index int, value int) int {
 
    fmt.Println(index)
 
    return index
}
 
func main() {
    defer function(1, function(3, 0))
    defer function(2, function(4, 0))

这个程序的执行结果是怎么样的的?

首先两个defer会压栈两次,先进栈1,后进栈2,在压栈function1的时候,需要连同函数地址、函数形参一同进栈,那么为了得到function1的第二个参数的结果,需要先执行function3将第二个参数算出,所以function3就被第一个执行。同理压入栈function2,就需要先执行function4算出function2的第二个参数的值,然后函数结束,先出栈function2、再出栈function1。输出结果如下:

3

4

2

1

总结
  1. defer是go中一种延迟调用机制,defer后面的函数只有在当前函数执行完毕后才能执行。

  2. 多个defer出现的时候,它会把defer之后的函数压入一个栈中延迟执行,也就是先进后出。

  3. defer后面的函数值在入栈的时候就决定了。

  4. defer 最大的功能是 panic 后依然有效,我们可以在defer中进行recover,如果defer中包含recover,则程序将不会再进行panic,实现try catch机制。

函数的数据类型

函数本身也是一个数据类型

package main
​
import "fmt"
​
func main() {
    //如果f1加了括号就成了函数调用了
    fmt.Printf("%T", f1)
}
func f1() {
​
}
​

运行结果为

func()

func()本身就是数据类型

如果函数不加括号,函数本身就是一个变量

package main
​
import "fmt"
​
func main() {
    //如果f1加了括号就成了函数调用了
    fmt.Printf("%T", f1)
    //定义函数类型的变量
    var f5 func(int, int) int
    f5 = f1
    //打印的是一段内存地址
    fmt.Println(f5)
    f5(1, 2)
​
}
func f1(a, b int) int {
    fmt.Println(a, b)
    return 0
​
}
​
匿名函数

匿名函数就是没有名字的函数

package main
​
import "fmt"
​
func main() {
    f1()
    f2 := f1 //函数本身就是一个变量
    f2()
    //匿名函数自己调用自己
    r1 := func(a, b int) int {
        fmt.Println("匿名函数")
        return 0
    }(1, 2)
    fmt.Println(r1)
}
func f1() {
    fmt.Println("我是f1函数")
​
}
​

go语言时支持函数式编程:

1、将匿名函数作为另外一个函数的参数,回调函数

2、将匿名函数作为另外一个函数的返回值,可以形成闭包结构

回调函数

高阶函数:根据go语言的数据类型的特点,可以将一个函数作为另外一个函数的参数

fun1(),fun2()

将fun1()函数作为fun2这个函数的参数

fun2函数:就叫做高阶函数,接受了一个函数作为参数的函数

fun1函数:就叫做回调函数,作为另外一个函数的参数

package main
​
import "fmt"
​
func main() {
    r1 := add(1, 2)
    fmt.Println(r1)
    r2 := oper(3, 4, add)
    fmt.Println(r2)
​
}
​
//  高阶函数,可以接收一个函数作为参数
func oper(a, b int, fun func(int, int) int) int {
    r := fun(a, b)
    return r
}
func add(a, b int) int {
    return a + b
}
​
闭包结构

一个外层函数中,有内层函数,该内层函数中,会操作外层函数的局部变量

并且改外层函数的返回值就是这个内层函数

这个内层函数和外层函数的局部变量,统称为闭包结构

局部变量的生命周期就会发生改变,正常的局部变量就会随着函数的调用二创建,随着函数的结束而销毁,但是闭包结构中的外层函数的局部变量并不会随着外层函数的结束而销毁,因为内层函数还在继续使用

package main
​
import "fmt"
​
func main() {
    r1 := add(1, 2)
    fmt.Println(r1)
    r2 := oper(3, 4, add)
    fmt.Println(r2)
​
}
​
//  高阶函数,可以接收一个函数作为参数
func oper(a, b int, fun func(int, int) int) int {
    r := fun(a, b)
    return r
}
func add(a, b int) int {
    return a + b
}
​

指针

go语言中的函数传参都是只拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。

类型指针不能进行偏移和运算

go语言中的指针操作非常简单,只需要记住两个符号:&(取地址符)和*(根据地址取值)

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置,go语言中使用&字符放在变量前面对变量进行取地址操作

指针声明实例
func main() {
    var ip *int
    fmt.Println(ip)
}
​
指针使用实例

package main
​
import "fmt"
​
func main() {
    var a int = 20 //声明实际变量
    var ip *int    //声明指针变量
    ip = &a        //指针变量存储的地址
    fmt.Println(&a)
    fmt.Println(ip)
    fmt.Println(*ip)
}
​

指向数组的指针
定义语法

var ptr []*int; 表示数组里面的元素是指针类型

实际演示
package main
​
import "fmt"
​
func main() {
    a := [3]int{1, 2, 3}
    var par [3]*int
    for i := 0; i < len(a); i++ {
        par[i] = &a[i]
    }
​
    for i := 0; i < len(par); i++ {
        //*par[i]就是打印出相关指针的值
        fmt.Printf("%v\n", *par[i])
    }
    fmt.Println(par)
}

类型定义和类型别名

类型定义
类型定义的语法

type NewType Type

实例
package main
​
import "fmt"
​
func main() {
   //定义一个新类型 与int一致 
    type Myint int
    var i Myint
    i = 100
    fmt.Printf("%v,%T\n", i, i)
}
​

运行结果

100,main.Myint

类型别名

类型别名的语法

type NewType=Type

实例

package main
​
import "fmt"
​
func main() {
    type Myint2 = int
    //i其实还是int类型
    var i Myint2
    i = 100
    fmt.Printf("%v,%T\n", i, i)
}
​

运行结果

100,int

类型定义和类型别名的区别
  1. 类型定义相当于定义了一个全新的类型,与之前的类型不同;但是类型别名没有定义一个全新的类型,而是使用一个别名来替换之前的类型

  2. 类型别名只会在代码中存在在编译完成之后就不会存在改名

  3. 因为类型别名和原来的类型是一致的,所以原来类型所拥有的方法,类型别名中也可以调用,但是如果是重新定义的一个类型,那么不可以调用之前的任何方法

go结构体

go语言没有面向对象的概念了,打造你是可以使用结构体来实现,面向对象编程的一些特性,例如:继承、组合等特性

go语言结构体的定义

结构体定义与go语言定义类似,只是多了一个struct关键字,语法结构如下

type stuct_variable_type struct {
    member definition
    member definition
    member definition
    member definition
}

type:结构体定义关键字

struct_varible_type:结构体类型定义名称

struct:结构体定义关键字

member definition:成员定义

实例

定义一个人的结构体Person

type Person struct {
    id    int
    name  string
    age   int
    email string
}
​

以上定义了一个Person的结构体吗,有四个成员,来描述一个Person的信息

相同类型的可以合并到一行,例如

type Person struct{
    id,age int
    name,email string
}
声明一个结构体变量

声明一个结构提变量和生命一个普通变量相同,例如

package main
​
import "fmt"
​
type Person struct {
    id    int
    name  string
    age   int
    email string
}
​
func main() {
    var tom Person
    fmt.Println(tom)
    kite := Person{}
    fmt.Printf("%v\n", kite)
}
​

运行结果

{0 0 }{0 0 }

结构体成员,在没有赋值之前都是零值

访问结构体成员

可以通过(.),来访问结构体成员,例如:

package main
​
import "fmt"
​
func main() {
    type Person struct {
        id, age     int
        name, email string
    }
    var tom Person
    tom.id = 1
    tom.age = 18
    tom.name = "tom"
    tom.email = "tom@gmail.com"
    fmt.Printf("%v", tom)
}
​

运行结果如下:

{1 18 tom tom@gmail.com}

匿名结构体

如果结构体是临时使用,可以不用起名字,直接使用,例如:

package main
​
import "fmt"
​
func main() {
    var dog struct {
        id   int
        name string
    }
    dog.id = 1
    dog.name = "huahua"
    fmt.Println(dog)
}
​

运行结果

{1 huahua}

结构体初始化

未初始化的结构体,成员都是零值int 0 float 0.0 bool false string nil nil

实例

package main
​
import "fmt"
​
func main() {
    type Person struct {
        id, age     int
        name, email string
    }
    var tom Person
    fmt.Printf("%v", tom)
}
​

运行结果

{0 0 }

使用键值对结构体进行初始化

实例

package main
​
import "fmt"
​
func main() {
    type Person struct {
        id, age     int
        name, email string
    }
    kite := Person{
        id:    1,
        name:  "tom",
        age:   19,
        email: "tom@gmain.com",
    }
    fmt.Println(kite)
}
​

运行结果

{1 19 tom tom@gmain.com}

通过列表的方式赋值

package main
​
import "fmt"
​
func main() {
    type Person struct {
        id    int
        age   int
        name  string
        email string
    }
    tom := Person{
        1,
        18,
        "tom",
        "tom@gaiml.com",
    }
    fmt.Println(tom)
}
​

这种方法赋值顺序要与结构体定义的类型一致,两种方式不能混合使用

结构体指针

结构体指针和普通指针相同

使用new关键字创建结构体指针

package main
​
import "fmt"
​
func main() {
    type Person struct {
        id   int
        name string
    }
    var p_person = new(Person)
    fmt.Printf("%T,", p_person)
}
​

运行结果

*main.Person,

访问结构体指针成员

访问结构体指针成员,也使用(.)运算符,例如:

package main
​
import "fmt"
​
func main() {
    type Person struct {
        id   int
        name string
    }
    var p_person = new(Person)
    fmt.Printf("%T\n", p_person)
    p_person.id = 1
    p_person.name = "tom"
    fmt.Printf("%v\n", *p_person)
}
​
​

运行结果

*main.Person{1 tom}

结构体作为函数参数

go结构体可以像普通变量一样,作为函数参数,传递给函数,这里分为两种情况

  1. 直接传递结构体,这是一个副本(拷贝),在函数内部不会改变外面结构体的内容

  2. 传递结构体指针,这时在函数内部能够改变外部结构体内容

直接传递结构体

实例

package main
​
import "fmt"
​
type Person1 struct {
    id   int
    name string
}
​
func main() {
    person := Person1{1, "kite"}
    fmt.Printf("%v\n", person)
    showPerson(person)
}
func showPerson(person Person1) {
    person.id = 1
    person.name = "tom"
    fmt.Printf("%v\n", person)
}
​

运行结果

{1 kite}{1 tom}

嵌套结构体

go语言没有面向对象编程思想,也没有继承关系,但是可以通过结构体嵌套来实现这种效果

Dog结构体

type Dog struct {
    name  string
    color string
    age   int
}

Person结构体

type Person struct {
    dog  Dog
    name string
    age  int
}

访问它们

package main
​
import "fmt"
​
type Dog struct {
    name  string
    color string
    age   int
}
type Person struct {
    dog  Dog
    name string
    age  int
}
​
func main() {
    var tom Person
    tom.dog.name = "花花"
    tom.dog.color = "red"
    tom.dog.age = 10
    tom.name = "tom"
    tom.age = 18
    fmt.Printf("%v\n", tom)
}
​

运行结果

{{花花 red 10} tom 18}

方法

go没有面向对象的特性,也没有类对象的概念。但是可以使用结构体来模拟这些特性,我们都知道面向对象里面有类方法等概念。我们可以声明一些方法。属于某个结构体

go语言方法的语法

go语言的方法是一种特殊的函数,定义于struct之上(与struct关联、绑定),被称为struct的接受者(receiver)

通俗得奖,方法就是有接收者的函数

语法格式如下:

type mytype struct{}
func(recv mytype) my_method(para) return_type {}
func(recv *mytype) my_method(para) return_type {}

mytype:定义一个结构体

recv:接受该方法的结构体

my_method:方法名称

para:参数列表

return_type:返回值类型

从语法格式可以看出,一个方法和一个函数非常相似,多了一个接受类型

实例
package main
​
import "fmt"
​
type Person struct {
    name string
}
​
func (per Person) eat() {
    fmt.Printf("%v\n", per.name)
}
func (per Person) sleep() {
    fmt.Printf("%v\n", per.name)
}
​
type Customer struct {
    name string
}
​
func (customer Customer) login() {
    fmt.Printf("%v\n", customer.name)
}
func main() {
    per := Person{
        name: "tom",
    }
    per.eat()
    per.sleep()
    customer := Customer{
        name: "cus",
    }
    customer.login()
}
​

运行结果

tomtomcus

值类型结构体和指针类型结构体
实例
package main
​
import "fmt"
​
type Person struct {
    name string
}
​
func main() {
    p1 := Person{name: "tom"}
    fmt.Printf("p1:%T\n", p1)
    p2 := &Person{name: "tom"}
    fmt.Printf("p2:%T\n", p2)
}
​

运行结果

p1:main.Personp2:*main.Person

从运行结果,可以看出p1是值类型,p2是指针类型。

下面看一个传参结构体的例子

package main
​
import "fmt"
​
type Person struct {
    name string
}
​
func showPerson(per Person) {
    fmt.Printf("per: %p\n", &per)
    per.name = "kite"
    fmt.Printf("per: %v\n", per)
}
func showPerson2(per *Person) {
    fmt.Printf("per: %p\n", per)
    per.name = "kite"
    fmt.Printf("per: %v\n", per)
}
func main() {
    p1 := Person{name: "tom"}
    fmt.Printf("p1: %p\n", &p1)
    showPerson(p1)
    fmt.Printf("p1: %v\n", p1)
    fmt.Println("----------------")
    p2 := &Person{name: "tom"}
    fmt.Printf("p2: %p\n", p2)
​
}
​
运行结果

p1: 0xc000088230per: 0xc000088240per: {kite}p1: {tom}

p2: 0xc000088270

接口

结构像是公司里面的领导,他会定义一些通用规范,只设计规范,而不实现规范

go语言的结构,是一种新的类型定义,它把所有的具有共性的方法定义在一起,任何其他类型只要实现这些方法就是实现了这个接口

语法格式
//定义接口
type interface_name interface{
    method_name1 [return_type]
    method_name2 [return_type]
    method_name3 [return_type]
    method_name4 [return_type]
        
}
//定义结构体
type struct_name struct{
    //variables
}
//实现接口方法
func (struct_name_variable struct_name) method_name1() [return_type]{
    //方法实现
}

在接口定义中,若干个空方法。这些方法都具有通用性

接口实例

定义一个usb接口,有读read和写write两个方法,在定义一个电脑computer和一个手机moblie来实现这个接口

usb接口
type USB interface {
    read()
    write()
}
computer结构体
 package main
​
import "fmt"
​
type USB interface {
    read()
    write()
}
type Conputer struct {
    name string
}
​
func (c Conputer) read() {
    fmt.Printf("c.name: %v\n", c.name)
}
​
type Write struct {
    model string
}
​
func (w Write) write() {
    fmt.Printf("w.model: %v\n", w.model)
}
func main() {
    c := Conputer{
        name: "联想",
    }
    w := Write{
        model: "5G",
    }
    c.read()
    w.write()
​
}
​

运行结果

c.name: 联想w.model: 5G

实现接口必须要实现接口中的所有方法

下面定义一个OpenClose接口,里面有两个方法open和close,定义Door结构体,实现其中一个方法

package main
​
import "fmt"
​
type OpenClose interface {
    open()
    close()
}
type Door struct {
}
​
func (d Door) open() {
    fmt.Printf("open door")
}
func main() {
    var oc OpenClose
    oc = Door{} //这里编译错误提示只实现一个接口
}
​
go接口值类型和接收者和指针类型接收者

这个话题,本质上和方法的值类型和指针类型接收者,的思考方法是一样的,值接收者是一个拷贝,是一个副本,而指针接收者,传递的是指针

实例演示

定义一个Pet接口
type Pet interface {
    eat()
}
​
实现Pet接口(接收者是值类型)
func (dog Dog) eat() {
    dog.name = "花花"
    fmt.Printf("dog: %p\n", &dog)
}
测试
func main() {
    dog := Dog{name: "花花"}
    fmt.Printf("%p\n", &dog)
    dog.eat()
    fmt.Printf("%v\n", dog)
}
​

运行结果

0xc00004c240dog: 0xc00004c250{花花}

从运行结果上看,只能看出dog的地址变了说明是复制了一份,dog的name没有变说明,外面的dog变量没有被改变

将Pet接口改为指针接收者
func (dog *Dog) eat() {
    fmt.Printf("dog: %p\n", dog)
    dog.name = "黑黑"
}
测试
func main() {
    dog := &Dog{name: "花花"}
    fmt.Printf("dog: %p\n", dog)
    dog.eat()
    fmt.Printf("dog: %v\n", dog)
}

运行结果

dog: 0xc00004c240dog: 0xc00004c240dog: &{黑黑}

接口和类型的关系

  1. 一个类型可以实现多个接口

  2. 多个类型可以实现同一个接口(多态)

一个类型实现多个接口

一个类型实现多个接口,例如:有一个Player接口可以播放音乐,有一个Video接口可以播放放视频,一个mobile实现这两个接口,既可以播放音乐,又可以播放视频

定义一个Player接口
type Player interface{
    playMusic()
}

定义Video接口

type Video interface {
    playVideo()
}

定义Modile结构体

type Modile struct {
}

实现两个接口

func (m Modile) playMusic() {
    fmt.Println("播放音乐")
}
func (m Modile) playVideo() {
    fmt.Println("播放视频")
}

测试

func main() {
    m := Modile{}
    m.playMusic()
    m.playVideo()
}
​

运行结果

播放音乐播放视频

接口嵌套

接口可以通过嵌套,创建新的接口,例如:飞鱼,既可以飞,又可以游泳。我们创建一个fly接口,创建一个游泳接口,飞鱼接口有这两个接口组成

飞Fler接口
type Flyer interface {
    fley()
}
游泳Swimmer接口
type Swimmer interface {
    swim()
}
实现组合接口
type FlyFish interface {
    Flyer
    Swimmer
}
创建Fish结构体
type Fish struct {
}
实现组合接口
func (fish Fish) fley() {
    fmt.Println("fly……")
}
func (fish Fish) swim() {
    fmt.Println("swim……")
}
测试
func main() {
    var ff FlyFish
    ff = Fish{}
    ff.fley()
    ff.swim()
}
​

运行结果

fly……swim……

通过接口实现ocp设计原则

而面向对象可复用设计的第一块基石,便是所谓的"开闭"原则(open-closed princple,常缩写ocp)。虽然fo语言不是面向对象语言,但是可以模拟实现这个原则

ocp设计原则实例

定义一个宠物接口Pet

type Pet interface {
    eat()
    sleep()
}
定义Dog结构体
type Dog struct {
    name string
    age  int
}
实现Dog结构体
func (dog Dog) eat() {
    fmt.Println("dog eat……")
}
func (dog Dog) sleep() {
    fmt.Println("dog sleep……")
}
定义Cat结构体
type Cat struct {
    name string
    age  int
}
实现Dog结构体
func (cat Cat) eat() {
    fmt.Println("cat eat")
}
func (cat Cat) sleep() {
    fmt.Println("cat sleep")
}
定义Person结构体
type Person struct {
    name string
}
实现Person结构
func (per Person) care(pet Pet) {
    pet.eat()
    pet.sleep()
}
测试
func main() {
    dog := Dog{}
    cat := Cat{}
    person := Person{}
    person.care(dog)
    person.care(cat)
}

运行结果

dog eat……dog sleep……cat eatcat sleep

模拟oop的属性和方法

go语言没有面向对象的概念,也没有封装的概念,但是可以通过结构体struct和函数绑定来实现oop的属性和方法等特性。接收者receiver方法

例如,想要定义一个Person类,有name和age属性,有eat/sleep/work方法

package main
​
import "fmt"
​
type Person struct {
    name string
    age  int
}
​
func (per Person) eat() {
    fmt.Println("eat")
}
func (per Person) sleep() {
    fmt.Println("sleep")
}
func (per Person) work() {
    fmt.Println("work")
}
func main() {
    per := Person{
        name: "tom",
        age:  18,
    }
    fmt.Println(per)
    per.eat()
    per.sleep()
    per.work()
}
​

运行结果

{tom 18}eatsleepwork

继承

golang本质上没有oop的概念,也没有继承的概念,但可以通过结构体嵌套实现这个特性

例如

package main
​
import "fmt"
​
type Animal struct {
    name string
    age  int
}
​
func (animal Animal) eat() {
    fmt.Println("eat")
}
​
func (animal Animal) sleep() {
    fmt.Println("sleep")
}
​
type Dog struct {
    Animal
}
type Cat struct {
    Animal
}
​
func main() {
    a := Animal{
        name: "tom",
        age:  19,
    }
    dog := Dog{a}
    fmt.Println(a)
    a.eat()
    a.sleep()
    fmt.Println(dog)
    cat := Cat{a}
    fmt.Println(cat)
}
​

运行结果

{tom 19}eatsleep{{tom 19}}{{tom 19}}

构造函数

go没有构造函数的概念,可以使用函数来模拟构造函数的功能

例如

package main
​
import "fmt"
​
type Person struct {
    name string
    age  int
}
​
func NewPerson(name string, age int) (*Person, error) {
    if name == "" {
        return nil, fmt.Errorf("name不能为空")
    }
    if age < 0 {
        return nil, fmt.Errorf("age不能小于0")
    }
    return &Person{name: name, age: age}, nil
}
func main() {
    person, err := NewPerson("tom", 20)
    if err == nil {
        fmt.Printf("person: %v\n", *person)
    }
}
​

运行结果

person: {tom 20}

包可以区分命令空间(一个文件夹中不能有两个同名文件),也可以更好的管理项目。go中创建一个包,一般是创建一个文件夹,在该文件夹里面的go文件中,是由package关键字声明包名称,通常package关键字声明包名称相同。并且同一个文件下只有一个包

创建包

  1. 创建一个名为dao的文件夹

  2. 创建一个dao.go文件

  3. 在该文件中声明包

package dao
​
import "fmt"
​
func Test1() {
    fmt.Println("test package")
}
​
导入包

要使用某个包下面的变量或者方法,需要导入该包,导入包时,要导入从GOPATH开始的包路径,例如,在service.go中导入dao

//src

//pkg

//bin

package main
​
import (
    "awesomeProject3/src/dao"
)
​
func main() {
    dao.Test1()
}
​

包的注意事项

  • 一个文件夹下只能有一个package

    • import后面的其实时GOPATH开始的相对目录路径,包括最后一段。但由于一个目录只能有一个package所以import一个路径就等于时import了这个路径下的包

    • 注意这里指的时"直接包含"的go文件。如果有子目录,那么目录的父目录就是两个包

  • 比如你事先了一个计算器package,名叫calc,位于ealc目录下;但又想给别人一个使用范例,于是在calc下可以建个example.go可以时main包,里面还可以有个main函数

包管理工具gomod
gomod简介

go module是golang 1.11新加的特性,用来管理模块中包的依赖工具

go mod使用方法
  • 初始化模块

    go mod init <项目模块名>

  • 将依赖包复制到项目下的vendor目录

    go list -m all

  • 显示详细依赖关系

    go mod download [path@version]

    [path#version]是非必写的

实例演示

初始化模块

go mod init duo360.com/pro02


service包

package service
​
import "fmt"
​
func TestService() {
    fmt.Println("test service")
}
​
​
package service
​
import "fmt"
​
func TestCustomerService() {
    fmt.Println("test customer service")
}
​

测试

package main
​
import "duo360.com/pro02/service"
​
func main() {
    service.TestCustomerService()
    service.TestService()
}
​

运行结果

test customer servicetest service

golang 并发编程

协程

golang中的并发是函数相互独立运行的能力。Gorouines是并发运行的函数。golang提供了Goroutines作为并发处理操作的一种方式

创建一个携程非常简单,就是在一个任务函数前面添加一个go关键字:

go task()
实例1
package main
​
import (
    "fmt"
    "time"
)
​
func show(msg string) {
    for i := 0; i < 5; i++ {
        fmt.Printf("msg: %v\n", msg)
        time.Sleep(time.Millisecond * 2000)
    }
}
func main() {
    go show("java") //启动一个协程来执行 1
    show("golang")  //2
    fmt.Println("main end")
}
​

运行结果

msg: golangmsg: javamsg: javamsg: golangmsg: golangmsg: javamsg: javamsg: golangmsg: golangmsg: javamain end

案例2
package main
​
import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)
​
func responseSize(url string) {
    fmt.Println("Step: ", url)
    response, err := http.Get(url)
    if err == nil {
        log.Fatal(err)
    }
    fmt.Println("Step2: ", url)
    defer response.Body.Close()
    fmt.Println("Step3: ", url)
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Step4: ", len(body))
}
func main() {
    go responseSize("https://www.duoke360.com")
    go responseSize("https://baidu.com")
    go responseSize("https://jd.com")
    time.Sleep(10 * time.Second)
}
​

运行结果

Step:  https://www.duoke360.com
Step:  https://baidu.com
Step:  https://jd.com
​
通道
1. 什么是 Golang 通道?

Golang 中的通道是一种高效、安全、灵活的并发机制,用于在并发环境下实现数据的同步和传递。通道提供了一个线程安全的队列,只允许一个 goroutine 进行读操作,另一个 goroutine 进行写操作。通过这种方式,通道可以有效地解决并发编程中的竞态条件、锁问题等常见问题。

通道有两种类型:有缓冲通道和无缓冲通道。在通道创建时,可以指定通道的容量,即通道缓冲区的大小,如果不指定则默认为无缓冲通道。

2. Golang 通道的基本语法

Golang 通道的基本语法非常简单,使用 make 函数来创建一个通道:

go
复制代码 ch := make(chan int)


这行代码创建了一个名为 ch 的通道,通道的数据类型为 int。通道的读写操作可以使用箭头符号 <-,<- 表示从通道中读取数据,-> 表示向通道中写入数据。例如:

go
复制代码 ch := make(chan int)
 ch <- 1 // 向通道中写入数据1
 x := <- ch // 从通道中读取数据,并赋值给变量x


3. Golang 通道的缓冲机制

在 Golang 中,通道还支持缓冲机制。通道的缓冲区可以存储一定量的数据,当缓冲区满时,向通道写入数据将阻塞。当通道缓冲区为空时,从通道读取数据将阻塞。使用缓冲机制可以增加程序的灵活性和并发性能。

缓冲区大小为 0 的通道称为无缓冲通道。无缓冲通道的发送和接收操作都是阻塞的,因此必须有接收者准备好接收才能进行发送操作,反之亦然。这种机制确保了通道的同步性,即在通道操作前后,发送者和接收者都会被阻塞,直到对方做好准备。

3.1 有缓冲通道

有缓冲通道的创建方式为:

go
复制代码 ch := make(chan int, 3)


这行代码创建了一个名为 ch 的通道,通道的数据类型为 int,通道缓冲区的大小为 3。向有缓冲通道写入数据时,如果缓冲区未满,则写操作将成功,程序将继续执行。如果缓冲区已满,则写操作将阻塞,直到有空闲缓冲区可用。

从有缓冲通道读取数据时,如果缓冲区不为空,则读操作将成功,程序将继续执行。如果缓冲区为空,则读操作将阻塞,直到有数据可读取。

3.2 无缓冲通道

无缓冲通道的创建方式为:

go
复制代码 ch := make(chan int)


这行代码创建了一个名为ch的通道,通道的数据类型为 int,通道缓冲区的大小为 0。无缓冲通道的发送和接收操作都是阻塞的,因此必须有接收者准备好接收才能进行发送操作,反之亦然。

4. Golang 通道的超时和计时器

在并发编程中,常常需要对通道进行超时和计时操作。Golang 中提供了 time 包来实现超时和计时器。

4.1 超时机制

在 Golang 中,可以使用 select 语句和 time.After 函数来实现通道的超时操作。例如:

css
复制代码 select {
     case data := <-ch:
         fmt.Println(data)
     case <-time.After(time.Second):
         fmt.Println("timeout")
 }


这段代码中,select 语句监听了通道 ch 和 time.After(time.Second) 两个信道,如果 ch 中有数据可读,则读取并输出数据;如果等待 1 秒钟后仍然没有数据,则超时并输出 timeout。

4.2 计时器机制

Golang 中提供了 time 包来实现计时器机制。可以使用 time.NewTimer(duration) 函数创建一个计时器,计时器会在 duration 时间后触发一个定时事件。例如:

css
复制代码 timer := time.NewTimer(time.Second * 2)
 <-timer.C
 fmt.Println("Timer expired")


这段代码创建了一个计时器,设定时间为 2 秒钟,当计时器到达 2 秒钟时,会向 timer.C 信道中发送一个定时事件,程序通过 <-timer.C 语句等待定时事件的到来,并在接收到定时事件后输出 “Timer expired”。

5. Golang 通道的传递

在 Golang 中,通道是一种引用类型,可以像普通变量一样进行传递。例如:

go
复制代码 func worker(ch chan int) {
     data := <-ch
     fmt.Println(data)
 }
 
 func main() {
     ch := make(chan int)
     go worker(ch)
     ch <- 1
     time.Sleep(time.Second)
 }


这段代码中,main 函数中创建了一个名为ch的通道,并启动了一个 worker goroutine,向 ch 通道中写入了一个数据 1。worker goroutine 中通过 <-ch 语句从 ch 通道中读取数据,并输出到控制台中。

6. 单向通道

在 Golang 中,可以通过使用单向通道来限制通道的读写操作。单向通道只允许读或写操作,不允许同时进行读写操作。例如:

go
复制代码 func producer(ch chan<- int) {
     ch <- 1
 }
 
 func consumer(ch <-chan int) {
     data := <-ch
     fmt.Println(data)
 }
 
 func main() {
     ch := make(chan int)
     go producer(ch)
     go consumer(ch)
     time.Sleep(time.Second)
 }


这段代码中,produce r函数和 consumer 函数分别用于向通道中写入数据和从通道中读取数据。在函数的参数中,使用了单向通道限制参数的读写操作。在 main 函数中,创建了一个名为 ch 的通道,并启动了一个 producer goroutine 和一个 consumer goroutine,producer 向 ch 通道中写入数据1,consumer 从 ch 通道中读取数据并输出到控制台中。

7. 关闭通道

在 Golang 中,可以使用 close 函数来关闭通道。关闭通道后,通道的读写操作将会失败,读取通道将会得到零值,写入通道将会导致 panic 异常。例如:

go
复制代码 ch := make(chan int)
 go func() {
     for i := 0; i < 5; i++ {
         ch <- i
     }
     close(ch)
 }()
 for data := range ch {
     fmt.Println(data)
 }


这段代码中,创建了一个名为 ch 的通道,并在一个 goroutine 中向通道中写入数据 0 到 4,并通过 close 函数关闭通道。在主 goroutine 中,通过 for...range 语句循环读取通道中的数据,并输出到控制台中,当通道被关闭时,for...range 语句会自动退出循环。

在关闭通道后,仍然可以从通道中读取已经存在的数据,例如:

go
复制代码 ch := make(chan int)
 go func() {
     for i := 0; i < 5; i++ {
         ch <- i
     }
     close(ch)
 }()
 for {
     data, ok := <-ch
     if !ok {
         break
     }
     fmt.Println(data)
 }


这段代码中,通过循环读取通道中的数据,并判断通道是否已经被关闭。当通道被关闭时,读取操作将会失败,ok 的值将会变为 false,从而退出循环。

8. 常见的应用场景

通道是 Golang 并发编程中的重要组成部分,其常见的应用场景包括:

8.1 同步数据传输

通道可以被用来在不同的 goroutine 之间同步数据。当一个 goroutine 需要等待另一个goroutine 的结果时,可以使用通道进行数据的传递。例如:

go
复制代码 package main
 
 import "fmt"
 
 func calculate(a, b int, result chan int) {
     result <- a + b
 }
 
 func main() {
     result := make(chan int)
     go calculate(10, 20, result)
     fmt.Println(<-result)
 }

在这个例子中,我们使用通道来进行 a+b 的计算,并将结果发送给主函数。在主函数中,我们等待通道中的结果并输出。

8.2 协调多个 goroutine

通道也可以用于协调多个 goroutine 之间的操作。例如,在一个生产者-消费者模式中,通道可以作为生产者和消费者之间的缓冲区,协调数据的生产和消费。例如:

go
复制代码 package main
 
 import (
     "fmt"
     "sync"
 )
 
 func worker(id int, jobs <-chan int, results chan<- int) {
     for j := range jobs {
         fmt.Println("worker", id, "processing job", j)
         results <- j * 2
     }
 }
 
 func main() {
     jobs := make(chan int, 100)
     results := make(chan int, 100)
 
     // 开启三个worker goroutine
     for w := 1; w <= 3; w++ {
         go worker(w, jobs, results)
     }
 
     // 发送9个任务到jobs通道中
     for j := 1; j <= 9; j++ {
         jobs <- j
     }
     close(jobs)
 
     // 输出每个任务的结果
     for a := 1; a <= 9; a++ {
         <-results
     }
 }

在这个例子中,我们使用通道来协调三个 worker goroutine 之间的任务处理。每个 worker goroutine 从 jobs 通道中获取任务,并将处理结果发送到 results 通道中。主函数负责将所有任务发送到 jobs 通道中,并等待所有任务的结果返回。

8.3 控制并发访问

当多个 goroutine 需要并发访问某些共享资源时,通道可以用来控制并发访问。通过使用通道,可以避免出现多个 goroutine 同时访问共享资源的情况,从而提高程序的可靠性和性能。例如:

go
复制代码 package main
 
 import (
     "fmt"
     "sync"
 )
 
 var (
     balance int
     wg      sync.WaitGroup
     mutex   sync.Mutex
 )
 
 func deposit(amount int) {
     mutex.Lock()
     balance += amount
     mutex.Unlock()
     wg.Done()
 }
 
 func main() {
     for i := 0; i < 1000; i++ {
         wg.Add(1)
         go deposit(100)
     }
     wg.Wait()
     fmt.Println("balance:", balance)
 }

在这个例子中,我们使用互斥锁来控制对 balance 变量的并发访问。每个 goroutine 负责将 100 元存入 balance 变量中。使用互斥锁可以确保在任意时刻只有一个 goroutine 能够访问 balance 变量。

8.4 模拟事件驱动

通道也可以用来模拟事件驱动的机制。例如,可以使用通道来模拟一个事件队列,当某个事件发生时,可以将事件数据放入通道中,然后通过另一个 goroutine 来处理该事件。例如:

go
复制代码 package main
 
 import (
     "fmt"
     "time"
 )
 
 func eventLoop(eventChan <-chan string) {
     for {
         select {
         case event := <-eventChan:
             fmt.Println("Event received:", event)
         case <-time.After(5 * time.Second):
             fmt.Println("Timeout reached")
             return
         }
     }
 }
 
 func main() {
     eventChan := make(chan string)
 
     // 模拟事件发生
     go func() {
         time.Sleep(2 * time.Second)
         eventChan <- "Event 1"
         time.Sleep(1 * time.Second)
         eventChan <- "Event 2"
         time.Sleep
         1 * time.Second
         eventChan <- "Event 3"
         time.Sleep(4 * time.Second)
         eventChan <- "Event 4"
     }()
     eventLoop(eventChan)
 }

在这个例子中,我们使用通道来模拟事件的发生。eventLoop 函数使用 select 语句监听 eventChan 通道和 5 秒超时事件。当 eventChan 收到事件时,eventLoop 函数将事件打印出来。如果 5 秒内没有收到事件,则 eventLoop 函数结束。主函数负责创建 eventChan 通道,并模拟事件的发生。

8.5 批量处理任务
go
复制代码 package main
 
 import (
     "fmt"
     "sync"
 )
 
 func processTask(task int) {
     fmt.Println("Processing task", task)
 }
 
 func main() {
     tasks := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
     // 定义并发数为3的批量处理函数
     batchSize := 3
     var wg sync.WaitGroup
     taskChan := make(chan int)
     for i := 0; i < batchSize; i++ {
         wg.Add(1)
         go func() {
             defer wg.Done()
             for task := range taskChan {
                 processTask(task)
             }
         }()
     }
 
     // 将任务分发到taskChan通道中
     for _, task := range tasks {
         taskChan <- task
     }
     close(taskChan)
 
     wg.Wait()
 }

在这个例子中,我们使用通道来批量处理任务。首先定义了一个包含 10 个任务的数组。然后,我们定义了一个并发数为 3 的批量处理函数,它从 taskChan 通道中获取任务,并将任务处理结果输出。主函数负责将所有任务发送到 taskChan 通道中,并等待所有任务处理结束。注意,我们使用了 sync.WaitGroup 来等待所有批量处理函数的 goroutine 结束。

8.6 实现发布/订阅模式
go
复制代码 package main
 
 import "fmt"
 
 type eventBus struct {
     subscriptions map[string][]chan string
 }
 
 func newEventBus() *eventBus {
     return &eventBus{
         subscriptions: make(map[string][]chan string),
     }
 }
 
 func (eb *eventBus) subscribe(eventType string, ch chan string) {
     eb.subscriptions[eventType] = append(eb.subscriptions[eventType], ch)
 }
 
 func (eb *eventBus) unsubscribe(eventType string, ch chan string) {
     subs := eb.subscriptions[eventType]
     for i, sub := range subs {
         if sub == ch {
             subs[i] = nil
             eb.subscriptions[eventType] = subs[:i+copy(subs[i:], subs[i+1:])]
             break
         }
     }
 }
 
 func (eb *eventBus) publish(eventType string, data string) {
     for _, ch := range eb.subscriptions[eventType] {
         if ch != nil {
             ch <- data
         }
     }
 }
 
 func main() {
     eb := newEventBus()
 
     ch1 := make(chan string)
     ch2 := make(chan string)
 
     eb.subscribe("event1", ch1)
     eb.subscribe("event2", ch2)
 
     go func() {
         for {
             select {
             case data := <-ch1:
                 fmt.Println("Received event1:", data)
             case data := <-ch2:
                 fmt.Println("Received event2:", data)
             }
         }
     }()
 
     eb.publish("event1", "Event 1 data")
     eb.publish("event2", "Event 2 data")
 
     eb.unsubscribe("event1", ch1)
 
     eb.publish("event1", "Event 1 data after unsubscribe")
 
     // 等待事件处理完成
     fmt.Scanln()
 }

在这个例子中,我们使用通道来实现发布/订阅模式。定义了一个 eventBus 结构体,它包含了一个 subscriptions map,用来存储事件类型和订阅该事件类型的所有通道。我们可以通过 subscribe 函数向某个事件类型添加订阅通道,通过 unsubscribe 函数取消订阅通道,通过 publish 函数向某个事件类型发布事件。

在主函数中,我们创建了两个通道 ch1 和 ch2,并通过 subscribe 函数订阅了 "event1" 和 "event2" 两个事件类型。然后,我们启动了一个 goroutine,使用 select 语句监听 ch1 和 ch2 通道,将接收到的事件打印出来。接着,我们使用 publish 函数分别向 "event1" 和 "event2" 发布了事件。最后,我们使用 unsubscribe 函数取消了对 "event1" 事件类型的 ch1 通道的订阅,再次使用 publish 函数向 "event1" 发布了事件。注意,我们使用了 fmt.Scanln() 来等待事件处理完成,以避免程序在事件处理完毕前退出。

WaitGroup实现同步

go 里面的 WaitGroup 是非常常见的一种并发控制方式,它可以让我们的代码等待一组 goroutine 的结束。比如在主协程中等待几个子协程去做一些耗时的操作,如发起几个 HTTP 请求,然后等待它们的结果。

WaitGroup 示例

下面的代码展示了一个 goroutine 等待另外 2 个 goroutine 结束的例子:

go
复制代码func TestWaitgroup(t *testing.T) {
   var wg sync.WaitGroup
   // 计数器 +2
   wg.Add(2)
​
   go func() {
      sendHttpRequest("https://baidu.com")
      // 计数器 -1
      wg.Done()
   }()
​
   go func() {
      sendHttpRequest("https://baidu.com")
      // 计数器 -1
      wg.Done()
   }()
​
   // 阻塞。计数器为 0 的时候,Wait 返回
   wg.Wait()
}
​
// 发起 HTTP GET 请求
func sendHttpRequest(url string) (string, error) {
   method := "GET"
​
   client := &http.Client{}
   req, err := http.NewRequest(method, url, nil)
​
   if err != nil {
      return "", err
   }
​
   res, err := client.Do(req)
   if err != nil {
      return "", err
   }
   defer res.Body.Close()
​
   body, err := io.ReadAll(res.Body)
   if err != nil {
      return "", err
   }
​
   return string(body), err
}

在这个例子中,我们做了如下事情:

  • 定义了一个 WaitGroup 对象 wg,调用 wg.Add(2) 将其计数器 +2

  • 启动两个新的 goroutine,在这两个 goroutine 中,使用 sendHttpRequest 函数发起了一个 HTTP 请求。

  • 在 HTTP 请求返回之后,调用 wg.Done 将计数器 -1

  • 在函数的最后,我们调用了 wg.Wait,这个方法会阻塞,直到 WaitGroup 的计数器的值为 0 才会解除阻塞状态。

WaitGroup 基本原理

WaitGroup 内部通过一个计数器来统计有多少协程被等待。这个计数器的值在我们启动 goroutine 之前先写入(使用 Add 方法),然后在 goroutine 结束的时候,将这个计数器减 1(使用 Done 方法)。除此之外,在启动这些 goroutine 的协程中,会调用 Wait 来进行等待,在 Wait 调用的地方会阻塞,直到 WaitGroup 内部的计数器减到 0。也就实现了等待一组 goroutine 的目的

背景知识

在操作系统中,有多种实现进程/线程间同步的方式,如:test_and_setcompare_and_swap、互斥锁等。除此之外,还有一种是信号量,它的功能类似于互斥锁,但是它能提供更为高级的方法,以便进程能够同步活动。

信号量

一个信号量(semaphore)S是一个整型变量,它除了初始化外只能通过两个标准的原子操作:wait()signal() 来访问。操作 wait() 最初称为 P(荷兰语 proberen,测试);操作 signal() 最初称为 V(荷兰语 verhogen,增加),可按如下来定义 wait()

PV 原语。

javascript
复制代码wait(S) {
    while (S <= 0)
        ; // 忙等待
    S--;
}

可按如下来定义 signal()

javascript
复制代码signal(S) {
    S++;
}

wait()signal() 操作中,信号量整数值的修改应不可分割地执行。也就是说,当一个进程修改信号量值时,没有其他进程能够同时修改同一信号量的值。

简单来说,信号量实现的功能是:

  • 当信号量>0 时,表示资源可用,则 wait 会对信号量执行减 1 操作。

  • 当信号量<=0 时,表示资源暂时不可用,获取信号量时,当前的进程/线程会阻塞,直到信号量为正时被唤醒。

WaitGroup 中的信号量

WaitGroup 中,使用了信号量来实现 goroutine 的阻塞以及唤醒:

  • 在调用 Wait 的地方,goroutine 会陷入阻塞,直到信号量大于等于 0 的时候解除阻塞状态,得以继续执行。

  • 在调用 Done 的时候,如果 WaitGroup 内的等待协程的计数器减到 0 的时候,信号量会进行递增,这样那些阻塞的协程会进行执行下去。

WaitGroup 数据结构
go
复制代码type WaitGroup struct {
   noCopy noCopy
​
   // 高 32 位为计数器,低 32 位为等待者数量
   state atomic.Uint64
   sema  uint32
}
noCopy

我们发现,WaitGroup 中有一个字段 noCopy,顾名思义,它的目的是防止复制。这个字段在运行时是没有什么影响的,但是我们通过 go vet 可以发现我们对 WaitGroup 的复制。为什么不能复制呢?因为一旦复制,WaitGroup 内的计数器就不再准确了,比如下面这个例子:

go
复制代码func test(wg sync.WaitGroup) {
   wg.Done()
}
​
func TestWaitGroup(t *testing.T) {
   var wg sync.WaitGroup
   wg.Add(1)
   test(wg)
   wg.Wait()
}

go 里面的函数参数传递是值传递。调用 test(wg) 的时候将 WaitGroup 复制了一份。

在这个例子中,程序会永远阻塞下去,因为 test 中调用 wg.Done() 的时候,只是将 WaitGroup 副本的计数器减去了 1,而 TestWaitGroup 里面的 WaitGroup 的计数器并没有发生改变,因此 Wait 会永远阻塞。

我们如果需要将 WaitGroup 作为参数,请传递指针:

go
复制代码func test(wg *sync.WaitGroup) {
   wg.Done()
}


传递指针之后,我们在 test 中调用 wg.Done() 修改的就是 TestWaitGroup 里面同一个 WaitGroup。从而,Wait 方法可以正常返回。

state

WaitGroup 里面的 state 是一个 64 位的 atomic.Uint64 类型,它的高 32 位用来保存 counter(也就是上面说的计数器),低 32 位用来保存 waiter(也就是阻塞在 Wait 上的 goroutine 数量。)

sema

WaitGroup 通过 sema 来记录信号量:

  • runtime_Semrelease 表示将信号量递增(对应信号量中的 signal 操作)

  • runtime_Semacquire 表示将信号量递减(对应信号量中的 wait 操作)

简单来说,在调用 runtime_Semacquire 的时候 goroutine 会阻塞,而调用 runtime_Semrelease 会唤醒阻塞在同一个信号量上的 goroutine。

WaitGroup 的三个基本操作
  • Add: 这会将 WaitGroup 里面的 counter 加上一个整数(也就是传递给 Add 的函数参数)。

  • Done: 这会将 WaitGroup 里面的 counter 减去 1。

  • Wait: 这会将 WaitGroup 里面的 waiter 加上 1,并且调用 Wait 的地方会阻塞。(有可能会有多个 goroutine 等待一个 WaitGroup

WaitGroup 的实现
Add 的实现

Add 做了下面两件事:

  1. delta 加到 state 的高 32 位上

  2. 如果 counter0 了,并且 waiter 大于 0,表示所有被等待的 goroutine 都完成了,而还有在等待的 goroutine,这会唤醒那些阻塞在 Wait 上的 goroutine。

源码实现:

go
复制代码func (wg *WaitGroup) Add(delta int) {
   // wg.state 的计数器加上 delta
   //(加到 state 的高 32 上)
   state := wg.state.Add(uint64(delta) << 32) // 高 32 位加上 delta
   v := int32(state >> 32)                    // 高 32 位(counter)
   w := uint32(state)                         // 低 32 位(waiter)
   // 计数器不能为负数(加上 delta 之后不能为负数,最小只能到 0)
   if v < 0 {
      panic("sync: negative WaitGroup counter")
   }
   // 正常使用情况下,是先调用 Add 再调用 Wait 的,这种情况下,w 是 0,v > 0
   if w != 0 && delta > 0 && v == int32(delta) {
      panic("sync: WaitGroup misuse: Add called concurrently with Wait")
   }
   // v > 0,计数器大于 0
   // w == 0,没有在 Wait 的协程
   // 说明还没有到唤醒 waiter 的时候
   if v > 0 || w == 0 {
      return
   }
​
   // Add 负数的时候,v 会减去对应的数值,减到最后 v 是 0。
   // 计数器是 0,并且有等待的协程,现在要唤醒这些协程。
​
   // 存在等待的协程时,goroutine 已将计数器设置为0。
   // 现在不可能同时出现状态突变:
   // - Add 不能与 Wait 同时发生,
   // - 如果看到计数器==0,则 Wait 不会增加等待的协程。
   // 仍然要做一个廉价的健康检查,以检测 WaitGroup 的误用。
   if wg.state.Load() != state { // 不能在 Add 的同时调用 Wait
      panic("sync: WaitGroup misuse: Add called concurrently with Wait")
   }
​
   // 将等待的协程数量设置为 0。
   wg.state.Store(0)
   for ; w != 0; w-- {
      // signal,调用 Wait 的地方会解除阻塞
      runtime_Semrelease(&wg.sema, false, 0) // goyield
   }
}
Done 的实现

WaitGroup 里的 Done 其实只是对 Add 的调用,但是它的效果是,将计数器的值减去 1。背后的含义是:一个被等待的协程执行完毕了

Wait 的实现

Wait 主要功能是阻塞当前的协程:

  1. Wait 会先判断计数器是否为 0,为 0 说明没有任何需要等待的协程,那么就可以直接返回了。

  2. 如果计数器还不是 0,说明有协程还没执行完,那么调用 Wait 的地方就需要被阻塞起来,等待所有的协程完成。

源码实现:

go
复制代码func (wg *WaitGroup) Wait() {
   for {
      // 获取当前计数器
      state := wg.state.Load()
      // 计数器
      v := int32(state >> 32)
      // waiter 数量
      w := uint32(state)
      // v 为 0,不需要等待,直接返回
      if v == 0 {
         // 计数器是 0,不需要等待
         return
      }

      // 增加 waiter 数量。
      // 调用一次 Wait,waiter 数量会加 1。
      if wg.state.CompareAndSwap(state, state+1) {
         // 这会阻塞,直到 sema (信号量)大于 0
         runtime_Semacquire(&wg.sema) // goparkunlock
         // state 不等 0
         // wait 还没有返回又继续使用了 WaitGroup
         if wg.state.Load() != 0 {
            panic("sync: WaitGroup is reused before previous Wait has returned")
         }
         // 解除阻塞状态了,可以返回了
         return
      }
      // 状态没有修改成功(state 没有成功 +1),开始下一次尝试。
   }
}


runtime包

runtime包里面的定义了协程管理的api

tuntime.Gosched()


让出cpu时间片,等待时间执行

package main
​
import (
    "fmt"
    "runtime"
)
​
func show(msg string) {
    for i := 0; i < 2; i++ {
        fmt.Printf("msg: %v\n", i)
    }
}
func main() {
    go show("java") //字协程运行
    for i := 0; i < 2; i++ {
        runtime.Gosched()
        fmt.Printf("golang\":%v\n", "golang")
    }
    fmt.Println("end")
}
​

运行结果

golang":golangmsg: 0msg: 1golang":golangend

runtime.Goexit()

退出当前协程

package main
​
import (
    "fmt"
    "runtime"
    "time"
)
​
func show() {
    for i := 0; i < 10; i++ {
        fmt.Printf("i: %v\n", i)
        if i >= 5 {
            runtime.Goexit()
        }
    }
}
func main() {
    go show()
    time.Sleep(time.Second)
​
}
​

运行结果

i: 0i: 1i: 2i: 3i: 4i: 5

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

只会_摆烂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值