【Golang 快速入门】高级语法:反射 + 并发

简介: Golang 进阶反射变量内置 Pair 结构reflect结构体标签并发知识基础知识早期调度器的处理GMP 模型调度器的设计策略并发编程goroutinechannel无缓冲的 channel有缓冲的 channel关闭 channelchannel 与 rangechannel 与 select
学习视频: 8 小时转职 Golang 工程师,这门课很适合有一定开发经验的小伙伴,强推!

Golang 进阶

反射

变量内置 Pair 结构

var a string
// pair<statictype:string, value:"aceld">
a = "aceld"

var allType interface{}
// pair<type:string, value:"aceld">
allType = a

str, _ := allType.(string)
类型断言其实就是根据 pair 中的 type 获取到 value
// tty: pair<type: *os.File, value: "/dev/tty" 文件描述符>
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
  fmt.Println("open file error", err)
  return
}

// r: pair<type: , value: >
var r io.Reader
// r: pair<type: *os.File, value: "/dev/tty" 文件描述符>
r = tty

// w: pair<type: , value: >
var w io.Writer
// w: pair<type: *os.File, value: "/dev/tty" 文件描述符>
w = r.(io.Writer) // 强转

w.Write([]byte("HELLO THIS IS A TEST!!\n"))

仔细分析下面的代码:

  • 由于 pair 在传递过程中是不变的,所以不管 r 还是 w,pair 中的 tpye 始终是 Book
  • 又因为 Book 实现了 Reader、Wrtier 接口,所以 type 为 Book 可以调用 ReadBook() 和 WriteBook()
type Reader interface {
    ReadBook()
}

type Writer interface {
    WriteBook()
}

// 具体类型
type Book struct {
}

func (b *Book) ReadBook() {
    fmt.Println("Read a Book")
}

func (b *Book) WriteBook() {
    fmt.Println("Write a Book")
}

func main() {
    // b: pair<type: Book, value: book{} 地址>
    b := &Book{}

    // book ---> reader
    // r: pair<type: , value: >
    var r Reader
    // r: pair<type: Book, value: book{} 地址>
    r = b
    r.ReadBook()

    // reader ---> writer
    // w: pair<type: , value: >
    var w Writer
    // w: pair<type: Book, value: book{} 地址>
    w = r.(Writer) // 此处的断言为什么成功?因为 w, r 的type是一致的
    w.WriteBook()
}

reflect

reflect 包中的两个重要方法:

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value {...}

// ValueOf接口用于获取输入参数接口中的数据的值,如果接口为空则返回0
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}

// TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

反射的应用:

  • 获取简单变量的类型和值:
func reflectNum(arg interface{}) {
    fmt.Println("type : ", reflect.TypeOf(arg))
    fmt.Println("value : ", reflect.ValueOf(arg))
}

func main() {
    var num float64 = 1.2345
    reflectNum(num)
}
type :  float64
value :  1.2345
  • 获取结构体变量的字段方法:
type User struct {
    Id   int
    Name string
    Age  int
}

func (u User) Call() {
    fmt.Println("user ius called..")
    fmt.Printf("%v\n", u)
}

func main() {
    user := User{1, "AceId", 18}
    DoFieldAndMethod(user)
}

func DoFieldAndMethod(input interface{}) {
    // 获取input的type
    inputType := reflect.TypeOf(input)
    fmt.Println("inputType is :", inputType.Name())
    // 获取input的value
    inputValue := reflect.ValueOf(input)
    fmt.Println("inputValue is :", inputValue)

    // 通过type获取里面的字段
    // 1.获取interface的reflect.Type,通过Type得到NumField,进行遍历
    // 2.得到每个field,数据类型
    // 3.通过field有一个Interface()方法,得到对应的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)
    }

    // 通过type获取里面的方法,调用
    for i := 0; i < inputType.NumMethod(); i++ {
        m := inputType.Method(i)
        fmt.Printf("%s: %v\n", m.Name, m.Type)
    }
}
inputType is : User
inputValue is : {1 AceId 18}
Id: int = 1
Name: string = AceId
Age: int = 18
Call: func(main.User)

结构体标签

结构体标签的定义:

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++ {
        taginfo := t.Field(i).Tag.Get("info")
        tagdoc := t.Field(i).Tag.Get("doc")
        fmt.Println("info: ", taginfo, " doc: ", tagdoc)
    }
}

func main() {
    var re resume
    findTag(&re)
}
info:  name  doc:  我的名字
info:  sex  doc: 

结构体标签的应用:JSON 编码与解码

import (
    "encoding/json"
    "fmt"
)

type Movie struct {
    Title  string   `json:"title"`
    Year   int      `json:"year"`
    Price  int      `json:"price"`
    Actors []string `json:"actors"`
    Test   string   `json:"-"` // 忽略该值,不解析
}

func main() {
    movie := Movie{"喜剧之王", 2000, 10, []string{"xingye", "zhangbozhi"}, "hhh"}
    // 编码:结构体 -> json
    jsonStr, err := json.Marshal(movie)
    if err != nil {
        fmt.Println("json marshal error", err)
        return
    }
    fmt.Printf("jsonStr = %s\n", jsonStr)

    // 解码:jsonstr -> 结构体
    myMovie := Movie{}
    err = json.Unmarshal(jsonStr, &myMovie)
    if err != nil {
        fmt.Println("json unmarshal error", err)
        return
    }
    fmt.Printf("%v\n", myMovie)
}
jsonStr = {"title":"喜剧之王","year":2000,"price":10,"actors":["xingye","zhangbozhi"]}
{喜剧之王 2000 10 [xingye zhangbozhi] }
其他应用:orm 映射关系 ...

并发知识

基础知识

早期的操作系统是单进程的,存在两个问题:

1、单一执行流程、计算机只能一个任务一个任务的处理

2、进程阻塞所带来的 CPU 浪费时间

20220207165420.png

多线程 / 多进程 解决了阻塞问题:

20220207165635.png

但是多线程又面临新的问题:上下文切换所耗费的开销很大

20220207165902.png

进程 / 线程的数量越多,切换成本就越大,也就越浪费。

有可能 CPU 使用率 100%,其中 60% 在执行程序,40% 在执行切换....

多线程 随着 同步竞争(如 锁、竞争资源冲突等),开发设计变的越来越复杂。

多线程存在 高消耗调度 CPU高内存占用 的问题:


如果将内核空间和用户空间的线程拆开,也就出现了协程(其实就是用户空间的线程)

内核空间的线程由 CPU 调度,协程是由开发者来进行调度。

用户线程,就是协程。内核线程,就是真的线程。

然后在内核线程与协程之间,再加入一个协程调度器:实现线程与协程的一对多模型

  • 弊端:如果一个协程阻塞,会影响下一个的调用(轮询的方式)

如果将上面的模型改成一对一的模型,虽然没有阻塞,但是和以前的线程模型没有区别了....

再继续优化成多对多的模型,则将主要精力放在优化协程调度器上:

内核空间是 CPU 地盘,我们无法进行太多优化。

不同的语言想要支持协程的操作,都是在用户空间优化其协程处理器。

Go 对协程的处理:

早期调度器的处理

老调度器有几个缺点:

  1. 创建、销毁、调度 G 都需要每个 M 获取锁,形成了激烈的锁竞争
  2. M 转移 G 会造成延迟和额外的系统负载
  3. 系统调用(CPU 在 M 之前的切换)导致频繁的线程阻塞和取消阻塞操作,增加了系统开销

GMP 模型

调度器的设计策略

调度器的 4 个设计策略:复用线程、利用并行、抢占、全局G队列


复用线程:work stealing、hand off

  • work stealing 机制:某个处理器的本地队列空余,从其他处理器中偷取协程来执行

    注意,这里是从某个处理器的本地队列偷取,还有从全局队列中偷取的做法

  • hand off 机制:如果某个线程阻塞,会将处理器资源让给其他线程。


利用并行:利用 GOMAXPROCS 限定 P 的个数 = CPU 核数 / 2


抢占


全局G队列:基于 warlk stealing 机制,如果所有处理器的本地队列都没有协程,则从全局获取。

并发编程

goroutine

创建 goroutine:

// 子routine
func newTask() {
    i := 0
    for {
        i++
        fmt.Printf("new Goroutie: i = %d\n", i)
        time.Sleep(1 * time.Second)
    }
}

// 主routine
func main() {
    // 创建一个子进程 去执行newTask()流程
    go newTask()

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

退出当前的 goroutine 的方法 runtime.Goexit(),比较以下两段代码:

func main() {
    go func() {
        defer fmt.Println("A.defer")

        func() {
            defer fmt.Println("B.defer")
            fmt.Println("B")
        }()

        fmt.Println("A")
    }()

    // 防止程序退出
    for {
        time.Sleep(1 * time.Second)
    }
}
B
B.defer
A
A.defer

执行了退出 goroutine 的方法:

func main() {
    go func() {
        defer fmt.Println("A.defer")

        func() {
            defer fmt.Println("B.defer")
            runtime.Goexit() // 退出当前goroutine
            fmt.Println("B")
        }()

        fmt.Println("A")
    }()

    // 防止程序退出
    for {
        time.Sleep(1 * time.Second)
    }
}
B.defer
A.defer

channel

channel 用于在 goroutine 之间进行数据传递:

20220207211533.png

make(chan Type) // 等价于 make(chan Type, 0)
make(chan Type, capacity)
channel <- value         // 发送value到channel
<-channel                        // 接收并将其丢弃
x := <-channel            // 从channel中接收数据,并赋值给x
x, ok := <-channel    // 功能同上,同时检查通道是否已关闭或为空

channel 的使用:

func main() {
    // 定义一个channel
    c := make(chan int)

    go func() {
        defer fmt.Println("goroutine 结束")
        fmt.Println("goroutine 正在运行")
        c <- 666 // 将666发送给c
    }()

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

上面的代码(使用 channel 交换数据),sub goroutine 一定会在 main goroutine 之后运行

  • 如果 main goroutine 运行的快,会进入等待,等待 sub goroutine 传递数据过来

  • 如果 sub goroutine 运行的快,也会进入等待,等待 main routine 运行到当前,然后再发送数据

无缓冲的 channel

20220207215904.png

  • 第 1 步,两个 goroutine 都到达通道,但哪个都没有开始执⾏发送或者接收。
  • 第 2 步,左侧的 goroutine 将它的⼿伸进了通道,这模拟了向通道发送数据的⾏为。

    这时,这个 goroutine 会在通道中被锁住,直到交换完成。

  • 第 3 步,右侧的 goroutine 将它的手放⼊通道,这模拟了从通道⾥接收数据。

    这个 goroutine ⼀样也会在通道中被锁住,直到交换完成。

  • 第 4 步和第 5 步,进⾏交换。
  • 第 6 步,两个 goroutine 都将它们的手从通道里拿出来,这模拟了被锁住的 goroutine 得到释放。

    两个 goroutine 现在都可以去做其他事情了。

有缓冲的 channel

20220207220001.png

  • 第 1 步,右侧的 goroutine 正在从通道接收一个值。
  • 第 2 步,右侧的这个 goroutine 独立完成了接收值的动作,左侧的 goroutine 正在发送一个新值到通道里。
  • 第 3 步,左侧的 goroutine 还在向通道发送新值,⽽右侧的 goroutine 正在从通道接收另外一个值。

    这个步骤⾥的两个操作既不是同步的,也不会互相阻塞。

  • 第 4 步,所有的发送和接收都完成,⽽通道里还有⼏个值,也有一些空间可以存更多的值。

特点:

  • 当 channel 已经满,再向⾥面写数据,就会阻塞。
  • 当 channel 为空,从⾥面取数据也会阻塞。
func main() {
    // 带有缓冲的channel
    c := make(chan int, 3)
    fmt.Println("len(c) = ", len(c), "cap(c) = ", cap(c))

    go func() {
        defer fmt.Println("子go程结束")
        for i := 0; i < 3; i++ {
            c <- i
            fmt.Println("子go程正在运行,发送的元素 =", i, "len(c) = ", len(c), " cap(c) = ", cap((c)))
        }
    }()

    time.Sleep(2 * time.Second)

    for i := 0; i < 3; i++ {
        num := <-c // 从c中接收数据,并赋值给num
        fmt.Println("num = ", num)
    }
    fmt.Println("main 结束")
}
len(c) =  0 cap(c) =  3
子go程正在运行,发送的元素 = 0 len(c) =  1  cap(c) =  3
子go程正在运行,发送的元素 = 1 len(c) =  2  cap(c) =  3
子go程正在运行,发送的元素 = 2 len(c) =  3  cap(c) =  3
子go程结束
num =  0
num =  1
num =  2
main 结束
上例中,可以尝试分别改变 2 个 for 的循环次数进行学习。

关闭 channel

func main() {
    c := make(chan int)

    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        // close可以关闭一个channel
        close(c)
    }()

    for {
        // ok为true表示channel没有关闭,为false表示channel已经关闭
        if data, ok := <-c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }

    fmt.Println("Main Finished..")
}
0
1
2
3
4
Main Finished..

channel 不像文件一样需要经常去关闭,只有当确实没有任何发送数据了,或者想显式的结束 range 循环之类的,才去关闭 channel,注意:

  • 关闭 channel 后,无法向 channel 再发送数据(引发 panic 错误后导致接收立即返回零值)
  • 关闭 channel 后,可以继续从 channel 接收数据
  • 对于 nil channel,⽆论收发都会被阻塞

channel 与 range

func main() {
    c := make(chan int)

    go func() {
        defer close(c)
        for i := 0; i < 5; i++ {
            c <- i
        }
    }()

    // 可以使用range来迭代不断操作channel
    for data := range c {
        fmt.Println(data)
    }

    fmt.Println("Main Finished..")
}

channel 与 select

select 可以用来监控多路 channel 的状态:

func fibonacii(c, quit chan int) {
    x, y := 1, 1
    for {
        select {
        case c <- x:
            // 如果c可写,则进入该case
            x, y = y, x+y
        case <-quit:
            // 如果quit可读,则进入该case
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)

    // sub go
    go func() {
        for i := 0; i < 6; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()

    // main go
    fibonacii(c, quit)
}
1
1
2
3
5
8
quit
相关文章
|
2月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
98 4
Golang语言之管道channel快速入门篇
|
2月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
62 4
Golang语言文件操作快速入门篇
|
2月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
80 4
|
28天前
|
安全 Java Go
【Golang入门】简介与基本语法学习
Golang语言入门教程,介绍了Go语言的简介、基本语法、程序结构、变量和常量、控制结构、函数、并发编程、接口和类型、导入包、作用域以及错误处理等关键概念,为初学者提供了一个全面的学习起点。
18 0
|
2月前
|
Go
Golang语言之映射(map)快速入门篇
这篇文章是关于Go语言中映射(map)的快速入门教程,涵盖了map的定义、创建方式、基本操作如增删改查、遍历、嵌套map的使用以及相关练习题。
36 5
|
2月前
|
Go
Golang语言之切片(slice)快速入门篇
这篇文章是关于Go语言中切片(slice)的快速入门教程,详细介绍了切片的概念、定义方式、遍历、扩容机制、使用注意事项以及相关练习题。
30 5
|
2月前
|
Go
Golang语言之数组(array)快速入门篇
这篇文章是关于Go语言中数组的详细教程,包括数组的定义、遍历、注意事项、多维数组的使用以及相关练习题。
29 5
|
3月前
|
Go 开发者 索引
Golang 中 for 循环的语法详解
【8月更文挑战第31天】
29 0
|
3月前
|
存储 人工智能 Go
golang 反射基本原理及用法
golang 反射基本原理及用法
25 0
|
6月前
|
Go
深度探讨 Golang 中并发发送 HTTP 请求的最佳技术
深度探讨 Golang 中并发发送 HTTP 请求的最佳技术
116 4