【Golang】Golang快速上手

  • ~20.18K 字
  1. 1. Go开发环境
    1. 1.0.1. Linux
    2. 1.0.2. Windows
  • 2. 基本语法
    1. 2.1. 程序书写
    2. 2.2. 编译运行
    3. 2.3. 变量
      1. 2.3.1. 使用
      2. 2.3.2. 实现
    4. 2.4. 常量
    5. 2.5. 控制语句
      1. 2.5.1. 选择控制
      2. 2.5.2. 循环控制
    6. 2.6. 函数
    7. 2.7. 包
      1. 2.7.1. 创建
      2. 2.7.2. 导入
      3. 2.7.3. 导包逻辑
      4. 2.7.4. import别名与匿名导包
    8. 2.8. 指针
    9. 2.9. defer
    10. 2.10. 数组与切片
      1. 2.10.1. 切片详解
        1. 2.10.1.1. 切片的声明方式
        2. 2.10.1.2. 追加与截取
    11. 2.11. map
    12. 2.12. 结构体
    13. 2.13. 面向对象
      1. 2.13.1. 定义类
      2. 2.13.2. 继承
      3. 2.13.3. 多态
      4. 2.13.4. 空接口
    14. 2.14. 反射
      1. 2.14.1. 机制
      2. 2.14.2. 用法1
      3. 2.14.3. 结构体标签
  • 3. GoRoutine
    1. 3.1. 协程
    2. 3.2. GoLang对协程的处理
      1. 3.2.1. 早期调度器
      2. 3.2.2. GMP模型
    3. 3.3. 调度器的设计策略
      1. 3.3.1. 复用线程
      2. 3.3.2. 利用并行
      3. 3.3.3. 抢占
      4. 3.3.4. 全局G队列
    4. 3.4. Goroutine创建
  • 4. channel
    1. 4.1. 定义
    2. 4.2. 用法
    3. 4.3. 阻塞逻辑
    4. 4.4. channel缓冲
    5. 4.5. 删除
    6. 4.6. 与range
    7. 4.7. 与select
  • 5. GoModules
    1. 5.1. 基础环境
    2. 5.2. 通过Modules初始化项目
    3. 5.3. 改变模块依赖关系

  • 基于视频8小时转职Golang工程师 by 刘丹冰做的笔记

    也有看了一点官方文档的部分补充

    Go开发环境

    /golang文件夹下

    • src:源码仓库
    • bin:编译环境,将该文件夹配置到环境变量中

    Linux

    在家目录下的~/.bashrc中配置环境变量:

    1
    2
    3
    4
    # 设置路径
    export GOROOT=/usr/local/go
    export GOPATH=$HOME/go # 工作路径,可以自定义
    export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

    运行source ~/.bashrc更新环境

    Windows

    在环境变量中Path中添加go根目录bin文件夹作为GOROOT

    在环境变量中添加GOPATH作为工作目录,之后可以使用modules独自管理

    基本语法

    程序书写

    1
    2
    3
    4
    5
    6
    package main // 导包, 一般当前有main函数就要导入main包
    import "fmt"
    // main函数
    func main(){
    fmt.Println("hello world!")
    }
    • golang中有无分号均可以编译,编译器会自动添加分号

    • import是导包语句,需要使用相应内容时需要导入对应的包

    1
    2
    3
    4
    5
    import "包名" // 导入单个
    import (
    "包1"
    "包2"
    ) // 导入多个
    • 函数的左花括号一定要和当前函数名在同一行,习惯需要改

    编译运行

    • go run 源代码:编译+运行
    • go build 源代码:编译,编译后生成的可执行文件可以直接运行
    • go fmt 源代码:格式化

    变量

    使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    package main

    import "fmt"
    /* 四种变量的声明方式 */
    func main(){
    // 声明一个变量并初始化, 默认值为0
    var a int
    var b int = 10
    var c float64
    var d string = "abc"
    fmt.Println("a=", a)

    fmt.Println("b=", b)
    fmt.Println("c=", c)
    fmt.Println("d=", d)

    // 初始化时可以省去数据类型,通过值自动匹配当前变量的数据类型
    var e = 10
    var f = "acd"

    // 格式化输出,%T指输出当前变量的数据类型
    fmt.Printf("type of e = %T\n", e)
    fmt.Printf("type of f = %T\n", f)

    // 省略var关键字,自动匹配,使用:=自动声明+赋值, 这种方法无法声明全局变量
    g := 100
    fmt.Printf("type of g = %T\n", g)
    fmt.Println("g=", g)

    // 声明多个变量
    var a1, a2 int = 10, 20 // 相同类型
    fmt.Println("a1=",a1,"a2=",a2)
    var a3, a4 = "aa", 10 // 不同类型
    fmt.Println("a3=",a3,"a4=",a4)
    var (
    a5 int = 100
    a6 bool = true
    )
    fmt.Println("a5=",a5,"a6=",a6)
    }

    常用的是方法四,直接自动匹配,但是方法四不支持全局变量

    • 变量定义了必须用,不然会报错
    • 用_定义匿名变量,可以不使用

    实现

    GoLang中的变量是由两部分构成的一个pair,分别是变量的类型type和值value

    类型又分为静态类型(static type)和具体类型(concrete type)

    • 静态类型为基本类型,如int, string等
    • 具体类型指interface具体指向的数据类型,或者是系统看得见的类型

    常量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package main

    import "fmt"

    func main() {
    // 定义常量
    const length int = 10

    fmt.Println("length=", length)

    // 定义枚举类型
    const (
    A = 0
    B = 1
    C = 2
    )

    // iota自动累加1,第一行默认为0
    const (
    X = 10*iota
    Y
    Z
    )

    fmt.Println("X = ", X)
    fmt.Println("Y = ", Y)
    fmt.Println("Z = ", Z)

    }
    • 使用const定义常量,定义后不能修改
    • const括号可以定义枚举,枚举内使用iota实现自增
    • 注意:iota自增论行算,若某一行同时定义两个变量,他们对应的iota值是一样的
    • iota可以用在表达式里,表达式计算出的结果就是对应的枚举值

    控制语句

    选择控制

    if-else if-else:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if expression1 {

    }else if expression2 {

    }else if expression3 {

    }else {

    }

    不要单行开

    switch-case:

    1
    2
    3
    4
    5
    6
    7
    8
    switch expr {
    case case1:
    statement1
    case case2:
    statement2
    default:
    default statement
    }
    • expr可以没有

    • 可以通过fallthrough来表示执行相邻的下一个分支

    • 可以在switch后case前写一些简单语句,如声明新变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func main() {
    switch num := f(); {
    case num >= 0 && num <= 1:
    num++
    case num > 1:
    num--
    fallthrough
    case num < 0:
    num += num
    }
    }

    循环控制

    Golang中只有for循环

    1
    2
    3
    for init statement; expression; post statement {
    execute statement
    }

    当只保留循环条件expression时就和while一样了

    1
    2
    3
    for expression {
    execute statement
    }

    下面是一个死循环

    1
    2
    3
    for {
    execute statement
    }

    可以用for range迭代器来遍历一些可以迭代的数据

    1
    2
    3
    for index, value := range iterable {
    // body
    }

    你甚至可以迭代一个整型值,字面值,常量,变量

    函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    package main

    import "fmt"
    // 定义函数
    func fool(a string, b int) int {
    fmt.Println("a=", a)
    fmt.Println("b=", b)
    return 100
    }
    // 返回多个匿名值
    func fool2(a string, b int) (int, int) {
    fmt.Println("a=", a)
    fmt.Println("b=", b)
    return 100, 200
    }
    // 返回多个有名值
    func fool3(a string, b int) (r1 int, r2 int) {
    fmt.Println("a=", a)
    fmt.Println("b=", b)
    r1 = 300
    r2 = 400
    return r1, r2
    }

    func main(){
    c := fool("abc", 111)
    fmt.Println("c=", c)
    ret1, ret2 := fool3("eee", 114514)
    fmt.Println("ret1 = ", ret1, ",ret2 = ", ret2)
    }
    • 定义函数变量名在前,类型在后
    • 有名返回值中定义的变量属于局部变量,生存域为当前大括号

    包

    包可以区分命令空间

    go中创建一个包需要创建一个文件夹,在文件夹的go文件中使用package声明包名称,通常文件夹名和包名相同且同一文件下只有一个包

    创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package pkg1
    import "fmt"

    func Test() {
    fmt.Println("test package")
    }

    func init() {
    fmt.Println("pkg1 init")
    }

    流程:创建pkg1文件夹->创建pkg1.go文件->文件中声明包

    • 包中的init()函数为初始化函数,导包自动调用
    • 包中的所有API都需要开头大写

    导入

    1
    2
    3
    4
    5
    6
    7
    package main

    import "pkg1"

    func main() {
    pkg1.Test()
    }
    • 导入的包路径是从GOPATH开始的相对目录
    • 一个文件夹下只能有一个package,一个package的文件不能在多个文件夹下,若有同名则他们彼此无关,使用时需要给每个目录定一个别名
    • 导入的包必须使用

    导包逻辑

    import别名与匿名导包

    使用别名 包路径给导入的包起别名

    使用_ 包路径表示一个匿名导包,这样导入的包不使用也不会报错,此时无法使用当前包的方法,但是会执行init

    使用. 包路径可以直接将包内方法导入导当前包,不需要用.来执行

    1
    2
    3
    4
    5
    import (
    _ "pkg1"
    p2 "pkg2"
    . "pkg3"
    )

    指针

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package main
    import "fmt"
    // 传入指针并修改值
    func swap(a *int, b *int) {
    t := *a
    *a = *b
    *b = t
    }
    func main() {
    var a int = 1
    var b int = 2
    fmt.Println("original a = ", a, ", b = ", b)
    swap(&a, &b)
    fmt.Println("after a = ", a, ", b = ", b)
    }
    • 定义:*数据类型,二级指针**数据类型
    • &用于取地址,*用于解引用
    • 其实大体和C/C++一样

    defer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package main

    import "fmt"

    func f1() {
    fmt.Println("f1")
    }
    func f2() {
    fmt.Println("f2")
    }
    func f3() {
    fmt.Println("f3")
    }
    func main() {
    defer f1()
    defer f2()
    defer f3()
    fmt.Println("hello")
    }

    结果是:

    1
    2
    3
    4
    hello
    f3
    f2
    f1
    • defer定义了在当前函数结束时执行的操作,类似于cpp的析构函数

    • 当有多个defer时,执行操作顺序按照入栈顺序依次出栈执行,满足LIFO

    • 若同时出现return和defer,执行顺序是return先于defer

    数组与切片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    package main

    import "fmt"

    // 传参需要定义整个数据类型, 不常用
    func printArray(array [10]int){
    for i :=0; i<len(array); i++ {
    fmt.Println(array[i])
    }
    }
    // 传动态数组
    func printSlice(array []int){
    for _, value := range array {
    fmt.Println("value = ", value)
    }
    }
    func main() {
    // 固定长数组, 默认为0
    var array1 [10]int
    array2 := [10]int{1,2,3,4}

    // 数组遍历
    for i :=0; i<len(array1); i++ {
    fmt.Println(array1[i])
    }

    // 这种遍历根据类型返回多个值, 如数组返回下标与值
    for i, v := range array2 {
    fmt.Println("index = ", i,", value = ", v)
    }

    printArray(array2)

    // 动态数组
    array3 := []int{1,2,3,4}
    printSlice(array3)
    }
    • 静态数组需要定义长度,可以用{}在内部初始化值,默认值为0
    • 静态数组传参时需要传完整的类型,且是按值传递
    • 切片不需要定义长度,用{}在内部初始化值
    • 切片传参时只需要传入名字 []类型,且传入的是切片内用于存储该数组地址的变量,类似于传引用

    切片详解

    切片的声明方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 声明并初始化
    slice1 := []int{1,2,3}
    fmt.Println(len(slice1))

    // 声明空切片并开空间
    var slice2 []int
    slice2 = make([]int, 3)
    fmt.Println(len(slice2))

    // 声明的同时开空间
    var slice3 []int = make([]int, 3)
    fmt.Println(len(slice3))

    // :=+make自动推导
    slice4 := make([]int, 3)
    fmt.Println(len(slice4))
    • 声明切片,并初始化
    • 声明一个空切片,并开辟空间
    • 声明的同时开空间
    • 使用:=和make自动推导

    注意:判空使用slice==nil

    追加与截取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 容量追加
    var num = make([]int, 3, 5)
    fmt.Printf("len=%d, cap=%d, slice=%v\n", len(num), cap(num), num)

    // num切片追加元素1, 此时len=4, cap=5
    num = append(num, 1)
    fmt.Printf("len=%d, cap=%d, slice=%v\n", len(num), cap(num), num)
    num = append(num, 1)

    // 向容量满的切片追加元素,动态扩增
    num = append(num, 2)
    fmt.Printf("len=%d, cap=%d, slice=%v\n", len(num), cap(num), num)
    • 切片的容量和长度是两个概念,一个切片有两个指针指向长度的头尾,切片本身的变量名就是指向底层首地址的一个指针
      • 长度指左右指针之间的距离
      • 容量指左指针至切片末尾的距离
      • 其中多出的容量无法访问,但确实有这么一块空间提前开辟了
    • 使用make构建切片时,可以指定容量:make(类型, 长度, 容量)
    • 使用cap(切片)获取数组容量
    • 使用append(数组, 元素)向切片追加元素,若切片容量满的情况下追加元素,切片会动态扩容到之前容量的两倍
    1
    2
    3
    4
    5
    6
    7
    8
    9
    s := []int{1,2,3}
    // 中括号截取,左闭右开, 底层指向同一个切片
    s1 := s[0:2]
    fmt.Printf("len=%d, cap=%d, slice=%v\n", len(s1), cap(s1), s1)

    // copy函数深拷贝,可以将底层slice一同拷贝
    s2 := make([]int, 3)
    copy(s2, s) // s中的值依次拷贝到s2
    fmt.Printf("len=%d, cap=%d, slice=%v\n", len(s2), cap(s2), s2)
    • 使用中括号截取切片,左闭右开,截取出的切片和原切片底层是同一个数组
    • 使用copy函数实现深拷贝,可以将其底层数组一块拷贝

    map

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package main
    import "fmt"
    func main() {
    // 声明类型,key与value均为string, 默认没有空间
    var mymap1 map[string]string
    // 使用make给map分配空间
    mymap1 = make(map[string]string, 10)
    mymap1["lang1"] = "java"
    mymap1["lang2"] = "cpp"
    mymap1["lang3"] = "python"
    mymap1["lang4"] = "go"

    fmt.Println(mymap1)

    // :=自动推导声明
    mymap2 := make(map[int]string)
    mymap2[1] = "java"
    mymap2[2] = "cpp"
    fmt.Println(mymap2)

    // 多行声明
    mymap3 := map[string]string{
    "one": "java",
    "two": "cpp",
    }
    fmt.Println(mymap3)
    }
    • go的map底层是哈希表,无序
    • 四种声明方式如上,注意若不通过make声明,map是没有空间的
    • 给一个容量已满的map添加元素,map会自动扩容到原来的两倍
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package main
    import "fmt"
    // 传参
    func printmap(city map[string]string)
    {
    for key, val := range city {
    fmt.Printf("key=%s, val=%s\n", key, val)
    }
    }
    func main() {
    city := make(map[string]string)

    city["China"] ="Beijing"
    city["US"] = "Washington"
    city["Japan"] = "Tokyo"

    // 遍历
    for key, val := range city {
    fmt.Printf("key=%s, val=%s\n", key, val)
    }

    // 删除
    delete(city, "Japan")

    // 修改
    city["US"] = "DC"
    printmap(city)
    }
    • map常用方法如上

    • 传参时指定对应map类型,值传递,但这里传递的是map的指针值

    结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    package main
    import "fmt"
    // 声明数据结构的别名
    type myint int

    // 定义结构体
    type Book struct {
    title string
    author string
    }

    // 传参 值传递
    func printBook(book1 Book) {
    fmt.Println(book1)
    }

    // 传参 传指针
    func chgBook(book1 *Book) {
    book1.author = "he"
    }

    func main() {
    // 使用结构体
    var book1 Book
    book1.title = "Golang"
    book1.author = "who"
    printBook(book1)
    chgBook(&book1)
    printBook(book1)
    }
    • 使用type 别名 原名定义一个数据类型的别名
    • 使用时和CPP一样
    • 传参时是值传递,若需要修改则传指针

    面向对象

    定义类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package main
    import "fmt"

    // 类,若类名,属性名,方法名首字母大写,表示其他包能够访问
    type person struct {
    Name string
    Ad int
    Level
    }

    // 对应方法, 结构体值传递
    func (this *person) GetName() {
    fmt.Println(this.Name)
    }

    func (this *person) SetName(newName string) {
    this.Name = newName
    }

    func main() {
    // 定义对象
    p := person{Name:"zhangsan", Ad:100, Level:1}
    p.GetName()
    p.SetName("lisi")
    p.GetName()
    }
    • 用结构体当作类,内部填属性
    • 方法定义的时候在func后面指定是哪个类
    • 默认对类是值传递
    • 类名、属性名、方法名大写,说明是公用的,其他包可以访问,否则只能本包用

    继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    package main
    import "fmt"
    type Human struct {
    name string
    sex string
    }

    func (this *Human) Eat() {
    fmt.Println("eating")
    }
    func (this *Human) Walk() {
    fmt.Println("walking")
    }

    // 继承
    type SuperMan struct {
    Human // 继承Human类
    level int
    }

    // override
    func (this *SuperMan) Eat() {
    fmt.Println("superman eat")
    }
    // 新方法
    func (this *SuperMan) Fly() {
    fmt.Println("flying")
    }
    func main() {
    h :=Human{"zhangsan", "female"}
    h.Eat()
    h.Walk()
    s := SuperMan{Human{"wangwu", "male"}, 100}
    s.Walk()
    s.Eat()
    s.Fly()
    }
    • 继承直接在属性里写上父类
    • 可以重写也可以定义新方法
    • 这样创建类的时候要嵌套着写父类,或者直接var创建变量后,统一赋值

    多态

    GoLang的多态基于接口实现

    创建一个接口:

    1
    2
    3
    4
    5
    6
    // 接口
    type AnimalIF interface {
    Sleep()
    GetColor() string
    GetType() string
    }

    只要添加interface标识符即可,本质上相当于一个父类指针

    形成多态的基本要素:

    • 有一个父类(接口)定义方法
    • 有子类,需要实现父类的全部接口方法
    • 父类类型的变量要指向子类的具体数据变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    package main
    import "fmt"
    // 接口
    type AnimalIF interface {
    Sleep()
    GetColor() string
    GetType() string
    }

    // 具体类1, 只要重写接口里面的所有方法就相当于实现了这个接口
    type Cat struct {
    color string
    }
    func (this *Cat) Sleep() {
    fmt.Println("Cat is sleep")
    }

    func (this *Cat) GetColor() string {
    return this.color
    }

    func (this *Cat) GetType() string {
    return "Cat"
    }

    // 具体类2
    type Dog struct {
    color string
    }
    func (this *Dog) Sleep() {
    fmt.Println("Dog is sleep")
    }

    func (this *Dog) GetColor() string {
    return this.color
    }

    func (this *Dog) GetType() string {
    return "Dog"
    }

    func showAnimal(animal AnimalIF) {
    animal.Sleep()
    fmt.Println("color = ", animal.GetColor())
    fmt.Println("type = ", animal.GetType())
    }
    func main() {
    var animal AnimalIF // 本质是父类指针
    animal = &Cat{"Green"}
    animal.Sleep() // 调用的是Cat的方法

    animal = &Dog{"Yellow"}
    animal.Sleep() // 调用的是Dog的方法

    cat := Cat{"Green"}
    dog := Dog{"Yellow"}
    showAnimal(&cat)
    showAnimal(&dog)
    }

    空接口

    类型:interface{}

    GoLang中的基本类型基本都实现了interface{},所以我们可以用这种类型引用任意的数据类型(类似于java中的Object)

    并且,空接口自带类型断言机制,语法类似:变量名.(数据类型),返回两个值value和ok,前者为变量的值,后者为是否是这个数据类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package main
    import "fmt"

    func myFunc(arg interface{}) {
    fmt.Println("called")
    fmt.Println(arg)

    // 类型断言,返回value与ok两个值,若触发断言则ok为true
    value, ok := arg.(string)
    if !ok {
    fmt.Println("arg is not string")
    } else {
    fmt.Println("arg is string, value=",value)
    fmt.Printf("value type is %T\n", value)
    }
    }

    type Book struct {
    auth string
    }
    func main() {
    book:=Book{"GoLang"}
    myFunc(book)
    myFunc(100)
    myFunc("abc")
    myFunc(3.14)
    }

    反射

    GoLang中的语言底层由一个pair实现,见上面

    1
    2
    3
    4
    5
    6
    func main() {
    var s string
    a = "abc"
    var allType interface{}
    allType = a
    }

    此时allType本身pair永远不变,type和value分别变成两个指针

    若appType赋值,相当于把这两个指针指向对应的type和value

    机制

    GoLang中的reflect包提供了反射机制,可以根据形参获得对应的type和value

    主要包含两个重要函数:

    • ValueOf:获取输入参数接口中数据的值,若接口为空返回0
    • TypeOf:动态获取输入参数接口中值的类型,若接口为空返回nil

    其他更多方法可以查阅文档

    用法1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    package main
    import "fmt"
    import "reflect"
    // 反射的基本用法
    func reflectNum(arg interface{}) {
    fmt.Println("type: ", reflect.TypeOf(arg))
    fmt.Println("value: ", reflect.ValueOf(arg))
    }

    type User struct {
    Id int
    Name string
    Age int
    }

    func reflectStru(input interface{}) {
    // 获取整个类型的type和value
    inputType := reflect.TypeOf(input)
    inputValue := reflect.ValueOf(input)
    // 进而获取每个字段的type和value
    for i:=0; i<inputType.NumField(); i++ {
    field:= inputType.Field(i)
    value:= inputValue.Field(i).Interface()
    fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
    }
    // 还可以获取方法
    for i:=0; i<inputType.NumMethod(); i++ {
    m := inputType.Method(i)
    fmt.Printf("%s: %v\n", m.Name, m.Type)
    }
    }

    func (this User) Call() {
    fmt.Println("user is called")
    fmt.Printf("%v\n", this)
    }
    func main() {
    var num float64 = 1.2345
    reflectNum(num)
    user :=User{1,"lihua",18}
    reflectStru(user)
    }
    • 对于基本的类型,我们可以直接传给interface{}arg,此时接口值内部保存了动态类型和动态值两部分信息,可以使用TypeOf取出动态类型,使用ValueOf取出动态值

    • 更复杂一点,对于一个空接口,我们不知道传入的具体情况,可以用反射来获取接口内各个字段的具体类型和值

      • 首先获取整个类型的type和value
      • 通过NumField()方法可以获得结构体字段数量,NumMethod()方法可以获得结构体方法数量
      • 通过inputType.Field(i)获取第i个字段的类型信息,类型为StructField,包含了字段名、字段类型等,如field.Name是字段名,field.Type是字段类型
      • 通过inputValue.Field(i)获取第i个字段的值,类型为Value,再使用.Interface()把反射类型转换为普通的interface{}
      • 最后通过Interface{}把反射的value值转换会普通的接口类型

    结构体标签

    我们可以给结构体打标签,标签是键值对的形式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package main
    import "reflect"

    // 给结构体上标签
    type resume struct {
    Name string `info:"name" doc:"我的名字"`
    Sex string `info:"sex"`
    }

    // 解析标签信息
    func findTag(str interface{}) {
    // 找到结构体全部元素
    t := reflect.TypeOf(str).Elem()
    for i:=0; i<t.NumField(); i++ {
    // 获得对应key名的标签
    tagstring := t.Field(i).Tag.Get("info")
    tagdoc := t.Field(i).Tag.Get("doc")
    fmt.Println("info: ", tagstring)
    fmt.Println("info: ", tagstring)
    }
    }
    func main() {
    var re resume

    findTag(&re)

    }
    • 直接在定义结构体成员时,在后面的反括号中添加标签键值对即可

    • 使用反射相关方法.Tag().Get()可以读取标签

    使用结构体标签可以编码/解码JSON

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    package main

    import (
    "fmt"
    "encoding/json"
    )

    // 带标签,让json知道
    type Movie struct {
    Title string `json:"title"`
    Year int `json:"year"`
    Price int `json:"rmb"`
    Actors []string `json:"actors"`
    }

    func main() {
    movie := Movie{"test", 2000, 10, []string{"zhangsan","lisi"}}

    // 编码
    jsonStr, err := json.Marshal(movie)
    if err!=nil {
    fmt.Println("JSON marshal error ", err)
    return
    }
    fmt.Printf("jsonStr: %s\n", jsonStr)

    // 解码
    myMovie := Movie{}
    err = json.Unmarshal(jsonStr, &myMovie)
    if err!=nil {
    fmt.Println("json unmarshal error ", err)
    return
    }
    fmt.Printf("%v\n", myMovie)
    }
    • JSON相关需要导入库"encoding/json"
    • 编码使用json.Marshal,解码使用json.Unmarshal
    • 自动生成的JSON键根据你的结构体标签json:"xxx"来定

    GoRoutine

    协程

    多进程或多线程解决了单线程的阻塞问题,达到了宏观的并发

    但是多线程的切换是有切换成本的,这部分是cpu浪费时间成本

    总的来说,传统多线程有着高消耗cpu和高内存占用的缺点

    为了解决以上问题,新的多线程机制将一个线程分割为线程(内核部分)与协程(用户部分),且一一绑定

    这种关系下,一个线程对应一个协程,并发时还需要切换线程,有高切换成本

    改进一下,可以通过一个协程调度器用一个线程绑定多个协程,即为n对1的协程关系,这种可以有效解决高消耗cpu的问题

    但这种模型会导致其中一个协程阻塞会影响后面协程的进行

    最后,我们可以让线程与协程产生n比m的关系,通过协程调度器合理分配,因此协程调度器更好,对并发优化的更好,占用就越少

    GoLang对协程的处理

    GoLang首先对协程优化,将co-routine改为Goroutine,并优化内存到几kb,因此可以大量生产,且Goroutine可以灵活调度,可以灵活切换

    早期调度器

    早期的调度器会让想要获取协程的线程获取锁和全局Go协程队列,按照队列的进出获取Goroutine并尝试执行,结束后放回队尾,并尝试还锁

    缺点:

    • 这种调度模式创建、销毁和调度G都需要每个线程获取锁,形成锁竞争

    • 线程转移G会造成延迟和额外的系统负载

    • 系统调用导致频繁的线程阻塞和取消阻塞操作增加开销

    GMP模型

    GMP即Goroutine,Machine thread和processor

    Gorouting优先放入某本地队列,满了后放入全局队列

    一个P同一时间只能执行一个Go,最大并行数量为GOMAXPROCS个

    调度器的设计策略

    复用线程

    work stealing机制:

    当前G1正在被M1的P执行,本地队列中有两个正在等待

    此时M2会从M1偷一个G来执行,此为work stealing

    hand off机制:

    当前G1正在被M1的P执行,G2在M1的队列等待,G3在M2的队列等待,即将操作

    若此时G1阻塞,我们会创建/唤醒一个线程M3,将M1的P和队列移到M3,CPU切换到M2/M3执行

    若G1持续阻塞,则M1处于睡眠,若G1不再执行,M1就会被销毁

    利用并行

    我们可以限定P的个数GOMAXPROCS=CPU核数/2

    抢占

    以往的co-routine和对应的cpu绑定后,若有其他co-routine想要使用该cpu,就只能等待上一个主动释放

    现在的go-routine和对应cpu绑定只允许绑定一段时间(10ms),时间到后其他go-routing可以抢占该cpu

    全局G队列

    依照work stealing机制,若其他本地队列中没有G,而全局G队列中有,它也会从全局G队列中获取,这个过程需要加锁与解锁

    Goroutine创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package main

    import (
    "fmt"
    "time"
    )
    // 从goroutine
    func newTask() {
    i:=0
    for {
    i++
    fmt.Printf("new Goroutine : i = %d\n", i)
    }
    }

    // 主goroutine
    func main() {
    // 创建一个go程,去执行newTask
    go newTask()

    i:=0
    for {
    i++
    fmt.Printf("main goroutine: i = %d\n", i)
    time.Sleep(1*time.Second)

    }
    }
    • 使用go关键字后面接匿名/有名函数就可以创建goroutine
    • 其中main是主goroutine,其他的是从goroutine,从随着主的消亡而消亡
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package main

    import (
    "fmt"
    "runtime"
    "time"
    )
    func main(){
    // 创建一个匿名方法,作为goroutine
    go func() {
    defer fmt.Println("A.defer")
    func () {
    defer fmt.Println("B.defer")
    // 退出当前goroutine
    runtime.Goexit()
    fmt.Println("B")
    }()
    fmt.Println("A")
    }()

    for {
    time.Sleep(1*time.Second)
    }
    }

    • 创建的匿名方法前加go就可以直接创建goroutine
    • 使用runtime.Goexit()退出当前goroutine
    • 注意:作为协程的方法的返回值(若有)无法被变量接收,因为执行是并发的,返回值会被丢弃。协程之间的通信需要用到channel

    channel

    定义

    用于goroutine之间的信息交换

    语法:

    1
    2
    make(chan Type)
    make(chan Type, capacity)

    指定channel传输类型与缓冲量

    用法

    • 向channel中写数据:channel<-value
    • 接受并将其丢弃:<-channel
    • 从channel中接收数据并赋值:x:=<-channel
    • 同时检查管道是否关闭或为空:x, ok := <-channel
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package main
    import "fmt"

    func main() {
    // 定义channel
    c := make(chan int)
    go func() {
    defer fmt.Println("goroutine结束")
    fmt.Println("goroutine正在进行")
    // 将值发给channel
    c <- 666
    }()

    // 从c中接受数据并赋值给num
    num := <-c
    fmt.Println("num =",num)
    fmt.Println("main goroutine结束")
    }


    运行结果:

    1
    2
    3
    4
    goroutine正在进行
    goroutine结束
    num = 666
    main goroutine结束

    阻塞逻辑

    channel具有同步两个不同goroutine的能力,保证了一个阻塞队列

    若main goroutine提前到达获取channel数据的语句,会进入阻塞,直到sub goroutine传入数据并唤醒阻塞

    若sub goroutine提前到达发送channel数据的语句,也会进入阻塞,直到main goroutine开始读取数据并唤醒阻塞,发送数据

    channel缓冲

    若使用无缓冲的channel,两个goroutine先进入通道的需要阻塞等待另一方,直到数据交换完成都解锁

    若使用有缓存的channel,channel允许让我们在管道内存数据

    左右侧的goroutine可以一直往channel内放入值和取出值,这两个操作既不同步,也不互相阻塞,直到通道满了或为空,可能会让对应的一方阻塞

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package main

    import (
    "fmt"
    "time"
    )
    func main() {
    c := make(chan int ,3)
    fmt.Println("len=", len(c), ",cap=", cap(c))
    go func() {
    defer fmt.Println("子goroutine结束")
    for i:=0; i<4; i++ {
    c <-i
    fmt.Println("发送元素=",i,"len=", len(c), ",cap=", cap(c))

    }
    }()
    time.Sleep(2*time.Second)
    for i:=0; i<4; i++ {
    num := <-c
    fmt.Println("num=",num)
    }
    }


    输出:

    1
    2
    3
    4
    5
    6
    7
    8
    len= 0 ,cap= 3
    发送元素= 0 len= 1 ,cap= 3
    发送元素= 1 len= 2 ,cap= 3
    发送元素= 2 len= 3 ,cap= 3
    num= 0
    num= 1
    num= 2
    num= 3

    删除

    • 使用close()方法可以关闭channel
    • 读取channel值时,可以多返回一个布尔值,表示channel是否关闭
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package main 
    import "fmt"
    func main() {
    c := make(chan int)
    go func() {
    for i:=0; i<5; i++ {
    c<-i
    }
    close(c)
    }()
    for {
    // ok为true表示channel没有关闭,可以继续读,否则表示已经关闭
    if data, ok := <-c; ok {
    fmt.Println(data)
    } else {
    break
    }
    }
    fmt.Println("main finished")
    }


    • 当确实没有任何发送数据时,或你想显式的结束range循环时,才去关闭channel
    • 对一个关闭的channel发送数据会爆panic
    • 关闭channel后,可以继续接收数据
    • 对于一个nil channel,无论收发都会阻塞

    与range

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package main 
    import "fmt"

    func main() {
    c := make(chan int)
    go func() {
    for i:=0; i<5; i++ {
    c<-i
    }
    close(c)
    }()

    // range迭代不断操作channel
    for data:= range c{
    fmt.Println(data)
    }
    fmt.Println("main finished")
    }

    可以使用range不断迭代操作channel

    与select

    select可以完成监控多个channel的状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package main 
    import "fmt"
    func fib(c, quit chan int) {
    x, y:=1,1
    for {
    select {
    case c <- x:
    // 若c可写,则case进入
    x, y= y, x+y

    case <- quit:
    fmt.Println("quit")
    return
    }
    }
    }
    func main() {
    c := make(chan int)
    quit := make(chan int)

    go func() {
    for i:=0; i<10; i++ {
    fmt.Println(<-c)
    }
    quit <-0
    }()
    fib(c, quit)
    }

    格式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    select {
    case <- chan1:
    // 若chan1读到数据,则执行该行处理语句
    case chan2 <- 1:
    // 若成功向chan2写入数据,则执行该行处理语句
    default:
    // 若都没有成功,则执行这里的语句
    }

    GoModules

    Go Modules是go语言的依赖解决方案,它淘汰了原有的GOPATH的使用模式

    基础环境

    必须的环境变量:

    • GO111MODULE:Go Modules的开关,允许设置如下参数

      • auto:只要项目包含go.mod文件的话就启用Gomodules
      • on:启用Gomodules
      • off:禁用Gomodules
    • GOPROXY:设置Go模块代理,用于在后续拉取模块版本时直接通过镜像站快速拉取

    • GOSUMDB:值是一个Go checksum database,用于拉取模块版本时保证拉取到的模块版本数据未经过篡改

    • GONOPROXY/GONOSUMDB/GOPRIVATE:用在内部仓库中,设置GOPRIVATE后,导包不会走PROXY,也不会通过SUMDB校验

    通过Modules初始化项目

    首先创建一个项目文件夹,cd进入

    输入go mod init 模块名,模块名决定导包怎么写

    初始化后出现go.mod文件,存储go版本号与模块名

    写main文件,若源代码依赖了某个库,我们可以在终端中手动使用go get 模块名就可以拉下来,或者在go run的时候自动拉下来

    拉下来的库在go.mod中会写入require,包含以来的模块名,版本,若有//indirect表示间接依赖

    在go.sum中罗列出当前项目直接或间接依赖的所有模块版本,包括模块名、版本和哈希校验码(表示整体项目文件的全部文件的校验和哈希)

    若校验码不存在,表示依赖的库可能用不上

    改变模块依赖关系

    如果想回退当前项目用到的包的版本,可以使用go mod edit -replace=之前依赖的版本=要替换的版本

    回退后可以在go.mod看到对应的replace字段

    赞助喵
    非常感谢您的喜欢!
    赞助喵