Go(上):go基础 续

sync包——互斥锁

sync是synchronization同步这个词的缩写,所以也会叫做同步包。这里提供了基本同步的操作,比如互斥锁等等。这里除了Once和WaitGroup类型之外,大多数类型都是供低级库例程使用的。更高级别的同步最好通过channel通道和communication通信来完成

一、Mutex(互斥锁)
通过上一小节,我们知道了在并发程序中,会存在临界资源问题。就是当多个协程来访问共享的数据资源,那么这个共享资源是不安全的。为了解决协程同步的问题我们使用了channel,但是Go语言也提供了传统的同步工具。

什么是锁呢?就是某个协程(线程)在访问某个资源时先锁住,防止其它协程的访问,等访问完毕解锁后其他协程再来加锁进行访问。一般用于处理并发中的临界资源问题。

Go语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex。

Mutex 是最简单的一种锁类型,互斥锁,同时也比较暴力,当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖等到这个 goroutine 释放该 Mutex。

每个资源都对应于一个可称为 “互斥锁” 的标记,这个标记用来保证在任意时刻,只能有一个协程(线程)访问该资源。其它的协程只能等待。

互斥锁是传统并发编程对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,Unlock进行解锁。

在使用互斥锁时,一定要注意:对资源操作完成后,一定要解锁,否则会出现流程执行异常,死锁等问题。通常借助defer。锁定后,立即使用defer语句保证互斥锁及时解锁。

/ A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
type Mutex struct {
    
    
    state int32 //互斥锁上锁状态枚举值如下所示
    sema  uint32 //信号量,向处于Gwaitting的G发送信号
}

// A Locker represents an object that can be locked and unlocked.
type Locker interface {
    
    
    Lock()
    Unlock()
}

const (
    mutexLocked = 1 << iota // mutex is locked  ,1 互斥锁是锁定的
    mutexWoken // 2 唤醒锁
    mutexStarving
    mutexWaiterShift = iota // 统计阻塞在这个互斥锁上的goroutine数目需要移位的数值
    starvationThresholdNs = 1e6
)

二、Lock()方法:
Lock()这个方法,锁定m。如果该锁已在使用中,则调用goroutine将阻塞,直到互斥体可用。
三、Unlock()方法
Unlock()方法,解锁解锁m。如果m未在要解锁的条目上锁定,则为运行时错误。

锁定的互斥体不与特定的goroutine关联。允许一个goroutine锁定互斥体,然后安排另一个goroutine解锁互斥体。
五、示例代码:
我们针对于上次课程汇总,使用goroutine,模拟4个售票口出售火车票的案例。4个售票口同时卖票,会发生临界资源数据安全问题。我们使用互斥锁解决一下。(Go语言推崇的是使用Channel来实现数据共享,但是也还是提供了传统的同步处理方式)

示例代码:

package main

import (
    "fmt"
    "time"
    "math/rand"
    "sync"
)

//全局变量,表示票
var ticket = 10 //100张票

var mutex sync.Mutex //创建锁头

var wg sync.WaitGroup //同步等待组对象
func main() {
    
    
    /*
    4个goroutine,模拟4个售票口,

    在使用互斥锁的时候,对资源操作完,一定要解锁。否则会出现程序异常,死锁等问题。
    defer语句
     */

     wg.Add(4)
    go saleTickets("售票口1")
    go saleTickets("售票口2")
    go saleTickets("售票口3")
    go saleTickets("售票口4")

    wg.Wait() //main要等待
    fmt.Println("程序结束了。。。")

    //time.Sleep(5*time.Second)
}

func saleTickets(name string){
    
    
    rand.Seed(time.Now().UnixNano())
    defer wg.Done()
    for{
    
    
        //上锁
        mutex.Lock() //g2
        if ticket > 0{
    
     //ticket 1 g1
            time.Sleep(time.Duration(rand.Intn(1000))*time.Millisecond)
            fmt.Println(name,"售出:",ticket) // 1
            ticket-- // 0
        }else{
    
    
            mutex.Unlock() //条件不满足,也要解锁
            fmt.Println(name,"售罄,没有票了。。")
            break
        }
        mutex.Unlock() //解锁
    }
}
GOROOT=/usr/local/go #gosetup
GOPATH=/Users/ruby/go #gosetup
/usr/local/go/bin/go build -i -o /private/var/folders/kt/nlhsnpgn6lgd_q16f8j83sbh0000gn/T/___go_build_demo06_mutex_go /Users/ruby/go/src/l_goroutine/demo06_mutex.go #gosetup
/private/var/folders/kt/nlhsnpgn6lgd_q16f8j83sbh0000gn/T/___go_build_demo06_mutex_go #gosetup
售票口4 售出: 10
售票口4 售出: 9
售票口2 售出: 8
售票口1 售出: 7
售票口3 售出: 6
售票口4 售出: 5
售票口2 售出: 4
售票口1 售出: 3
售票口3 售出: 2
售票口4 售出: 1
售票口2 售罄,没有票了。。
售票口1 售罄,没有票了。。
售票口3 售罄,没有票了。。
售票口4 售罄,没有票了。。
程序结束了。。。

Process finished with exit code 0

sync包——读写锁

sync是synchronization同步这个词的缩写,所以也会叫做同步包。这里提供了基本同步的操作,比如互斥锁等等。这里除了Once和WaitGroup类型之外,大多数类型都是供低级库例程使用的。更高级别的同步最好通过channel通道和communication通信来完成

一、RWMutex(读写锁)
通过对互斥锁的学习,我们已经知道了锁的概念以及用途。主要是用于处理并发中的临界资源问题。

Go语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex。其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能。

RWMutex是读/写互斥锁。锁可以由任意数量的读取器或单个编写器持有。RWMutex的零值是未锁定的mutex。

如果一个goroutine持有一个rRWMutex进行读取,而另一个goroutine可能调用lock,那么在释放初始读取锁之前,任何goroutine都不应该期望能够获取读取锁。特别是,这禁止递归读取锁定。这是为了确保锁最终可用;被阻止的锁调用会将新的读卡器排除在获取锁之外。
我们怎么理解读写锁呢?当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,你给我站一边去,等它们读(读解锁)完你再来写(写锁定)。我们可以将其总结为如下三条:

同时只能有一个 goroutine 能够获得写锁定。
同时可以有任意多个 gorouinte 获得读锁定。
同时只能存在写锁定或读锁定(读和写互斥)。
所以,RWMutex这个读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景。

读写锁的写锁只能锁定一次,解锁前不能多次锁定,读锁可以多次,但读解锁次数最多只能比读锁次数多一次,一般情况下我们不建议读解锁次数多余读锁次数。

基本遵循两大原则:

​ 1、可以随便读,多个goroutine同时读。

​ 2、写的时候,啥也不能干。不能读也不能写。

读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。读写锁遵循的访问控制规则与互斥锁有所不同。在读写锁管辖的范围内,它允许任意个读操作的同时进行。但是在同一时刻,它只允许有一个写操作在进行。

并且在某一个写操作被进行的过程中,读操作的进行也是不被允许的。也就是说读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系。

二、常用方法
2.1、RLock()方法

func (rw *RWMutex) RLock()

读锁,当有写锁时,无法加载读锁,当只有读锁或者没有锁时,可以加载读锁,读锁可以加载多个,所以适用于“读多写少”的场景。

2.2、RUnlock()方法

func (rw *RWMutex) RUnlock()

读锁解锁,RUnlock 撤销单次RLock调用,它对于其它同时存在的读取器则没有效果。若rw并没有为读取而锁定,调用RUnlock就会引发一个运行时错误。

2.2、RUnlock()方法

func (rw *RWMutex) RUnlock()

读锁解锁,RUnlock 撤销单次RLock调用,它对于其它同时存在的读取器则没有效果。若rw并没有为读取而锁定,调用RUnlock就会引发一个运行时错误。

2.2、RUnlock()方法

func (rw *RWMutex) RUnlock()

读锁解锁,RUnlock 撤销单次RLock调用,它对于其它同时存在的读取器则没有效果。若rw并没有为读取而锁定,调用RUnlock就会引发一个运行时错误。

三、示例代码:
示例代码:

package main

import (
    "fmt"
    "sync"
    "time"
)

var rwMutex *sync.RWMutex
var wg *sync.WaitGroup
func main() {
    
    
    rwMutex = new(sync.RWMutex)
    wg = new (sync.WaitGroup)

    //wg.Add(2)
    //
    多个同时读取
    //go readData(1)
    //go readData(2)

    wg.Add(3)
    go writeData(1)
    go readData(2)
    go writeData(3)

    wg.Wait()
    fmt.Println("main..over...")
}

func writeData(i int){
    
    
    defer wg.Done()
    fmt.Println(i,"开始写:write start。。")
    rwMutex.Lock()//写操作上锁
    fmt.Println(i,"正在写:writing。。。。")
    time.Sleep(3*time.Second)
    rwMutex.Unlock()
    fmt.Println(i,"写结束:write over。。")
}

func readData(i int) {
    
    
    defer wg.Done()

    fmt.Println(i, "开始读:read start。。")

    rwMutex.RLock() //读操作上锁
    fmt.Println(i,"正在读取数据:reading。。。")
    time.Sleep(3*time.Second)
    rwMutex.RUnlock() //读操作解锁
    fmt.Println(i,"读结束:read over。。。")
}
GOROOT=/usr/local/go #gosetup
GOPATH=/Users/ruby/go #gosetup
/usr/local/go/bin/go build -i -o /private/var/folders/kt/nlhsnpgn6lgd_q16f8j83sbh0000gn/T/___go_build_demo07_rwmutex_go /Users/ruby/go/src/l_goroutine/demo07_rwmutex.go #gosetup
/private/var/folders/kt/nlhsnpgn6lgd_q16f8j83sbh0000gn/T/___go_build_demo07_rwmutex_go #gosetup
3 开始写:write start
3 正在写:writing
2 开始读:read start
1 开始写:write start
3 写结束:write over
2 正在读:reading
2 读结束:read over
1 正在写:writing
1 写结束:write over
main..over...

Process finished with exit code 0

最后概括:

读锁不能阻塞读锁
读锁需要阻塞写锁,直到所有读锁都释放
写锁需要阻塞读锁,直到所有写锁都释放
写锁需要阻塞写锁

channel通道

通道可以被认为是Goroutines通信的管道。类似于管道中的水从一端到另一端的流动,数据可以从一端发送到另一端,通过通道接收。

在前面讲Go语言的并发时候,我们就说过,当多个Goroutine想实现共享数据的时候,虽然也提供了传统的同步机制,但是Go语言强烈建议的是使用Channel通道来实现Goroutines之间的通信。

“不要通过共享内存来通信,而应该通过通信来共享内存” 这是一句风靡golang社区的经典语

Go语言中,要传递某个数据给另一个goroutine(协程),可以把这个数据封装成一个对象,然后把这个对象的指针传入某个channel中,另外一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。Go从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,为开发者提供了一种优雅简单的工具,所以Go的做法就是使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信。

一、 什么是通道
1.1 通道的概念
通道是什么,通道就是goroutine之间的通道。它可以让goroutine之间相互通信。

每个通道都有与其相关的类型。该类型是通道允许传输的数据类型。(通道的零值为nil。nil通道没有任何用处,因此通道必须使用类似于map和切片的方法来定义。)

1.2 通道的声明
声明一个通道和定义一个变量的语法一样:

//声明通道
var 通道名 chan 数据类型
//创建通道:如果通道为nil(就是不存在),就需要先创建通道
通道名 = make(chan 数据类型)

示例代码:

package main

import "fmt"

func main() {
    
    
    var a chan int
    if a == nil {
    
    
        fmt.Println("channel 是 nil 的, 不能使用,需要先创建通道。。")
        a = make(chan int)
        fmt.Printf("数据类型是: %T", a)
    }
}

运行结果:

channel 是 nil 的, 不能使用,需要先创建通道。。
数据类型是: chan int

也可以简短的声明:

a := make(chan int) 

1.3 channel的数据类型
channel是引用类型的数据,在作为参数传递的时候,传递的是内存地址。

示例代码:

package main

import (
    "fmt"
)

func main() {
    
    
    ch1 := make(chan int)
    fmt.Printf("%T,%p\n",ch1,ch1)

    test1(ch1)

}

func test1(ch chan int){
    
    
    fmt.Printf("%T,%p\n",ch,ch)
}

1.4 通道的注意点
Channel通道在使用的时候,有以下几个注意点:

1.用于goroutine,传递消息的。

2.通道,每个都有相关联的数据类型,
nil chan,不能使用,类似于nil map,不能直接存储键值对

3.使用通道传递数据:<-
chan <- data,发送数据到通道。向通道中写数据
data <- chan,从通道中获取数据。从通道中读数据

4.阻塞:
发送数据:chan <- data,阻塞的,直到另一条goroutine,读取数据来解除阻塞
读取数据:data <- chan,也是阻塞的。直到另一条goroutine,写出数据解除阻塞。

5.本身channel就是同步的,意味着同一时间,只能有一条goroutine来操作。

最后:通道是goroutine之间的连接,所以通道的发送和接收必须处在不同的goroutine中。

二、通道的使用语法
2.1 发送和接收
发送和接收的语法:

data := <- a // read from channel a  
a <- data // write to channel a

在通道上箭头的方向指定数据是发送还是接收。

另外:

v, ok := <- a //从一个channel中读取

2.2 发送和接收默认是阻塞的
一个通道发送和接收数据,默认是阻塞的。当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。相对地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。

这些通道的特性是帮助Goroutines有效地进行通信,而无需像使用其他编程语言中非常常见的显式锁或条件变量。

示例代码:

package main

import "fmt"

func main() {
    
    
    var ch1 chan bool       //声明,没有创建
    fmt.Println(ch1)        //
    fmt.Printf("%T\n", ch1) //chan bool
    ch1 = make(chan bool)   //0xc0000a4000,是引用类型的数据
    fmt.Println(ch1)

    go func() {
    
    
        for i := 0; i < 10; i++ {
    
    
            fmt.Println("子goroutine中,i:", i)
        }
        // 循环结束后,向通道中写数据,表示要结束了。。
        ch1 <- true

        fmt.Println("结束。。")

    }()

    data := <-ch1 // 从ch1通道中读取数据
    fmt.Println("data-->", data)
    fmt.Println("main。。over。。。。")
}

在上面的程序中,我们先创建了一个chan bool通道。然后启动了一条子Goroutine,并循环打印10个数字。然后我们向通道ch1中写入输入true。然后在主goroutine中,我们从ch1中读取数据。这一行代码是阻塞的,这意味着在子Goroutine将数据写入到该通道之前,主goroutine将不会执行到下一行代码。因此,我们可以通过channel实现子goroutine和主goroutine之间的通信。当子goroutine执行完毕前,主goroutine会因为读取ch1中的数据而阻塞。从而保证了子goroutine会先执行完毕。这就消除了对时间的需求。在之前的程序中,我们要么让主goroutine进入睡眠,以防止主要的Goroutine退出。要么通过WaitGroup来保证子goroutine先执行完毕,主goroutine才结束。

示例代码:以下代码加入了睡眠,可以更好的理解channel的阻塞

package main

import (
    "fmt"
    "time"
)

func main() {
    
    
    ch1 := make(chan int)
    done := make(chan bool) // 通道
    go func() {
    
    
        fmt.Println("子goroutine执行。。。")
        time.Sleep(3 * time.Second)
        data := <-ch1 // 从通道中读取数据
        fmt.Println("data:", data)
        done <- true
    }()
    // 向通道中写数据。。
    time.Sleep(5 * time.Second)
    ch1 <- 100

    <-done
    fmt.Println("main。。over")

}

再一个例子,这个程序将打印一个数字的个位数的平方和。

package main

import (  
    "fmt"
)

func calcSquares(number int, squareop chan int) {
    
      
    sum := 0
    for number != 0 {
    
    
        digit := number % 10
        sum += digit * digit
        number /= 10
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {
    
      
    sum := 0 
    for number != 0 {
    
    
        digit := number % 10
        sum += digit * digit * digit
        number /= 10
    }
    cubeop <- sum
} 
func main() {
    
      
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares + cubes)
}

运行结果:

Final output 1536

2.3 死锁
使用通道时要考虑的一个重要因素是死锁。如果Goroutine在一个通道上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。

类似地,如果Goroutine正在等待从通道接收数据,那么另一些Goroutine将会在该通道上写入数据,否则程序将会死锁。

示例代码:

package main

func main() {
    
      
    ch := make(chan int)
    ch <- 5
}

报错:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /Users/ruby/go/src/l_goroutine/demo08_chan.go:5 +0x50

在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。
除了这些基本的同步手段,Go语言还提供了一种新的同步机制: Channel,它在Go语言中是一个像int, float32等的基本类型,一个channel可以认为是一个能够在多个Goroutine之间传递某一类型的数据的管道。Go中的channel无论是实现机制还是使用场景都和Java中的BlockingQueue很接近。

关闭通道和通道上范围循环

一、 关闭通道
发送者可以通过关闭信道,来通知接收方不会有更多的数据被发送到channel上。

close(ch)

接收者可以在接收来自通道的数据时使用额外的变量来检查通道是否已经关闭。

语法结构:

v, ok := <- ch  

类似map操作,存储key,value键值对

v,ok := map[key] //根据key从map中获取value,如果key存在, v就是对应的数据,如果key不存在,v是默认值

在上面的语句中,如果ok的值是true,表示成功的从通道中读取了一个数据value。如果ok是false,这意味着我们正在从一个封闭的通道读取数据。从闭通道读取的值将是通道类型的零值。

例如,如果通道是一个int通道,那么从封闭通道接收的值将为0。

示例代码:

package main

import (
    "fmt"
    "time"
)

func main()  {
    
    
    ch1 := make(chan int)
    go sendData(ch1)
    /*
    子goroutine,写出数据10个
            每写一个,阻塞一次,主程序读取一次,解除阻塞

    主goroutine:循环读
            每次读取一个,堵塞一次,子程序,写出一个,解除阻塞

    发送发,关闭通道的--->接收方,接收到的数据是该类型的零值,以及false
     */
    //主程序中获取通道的数据
    for{
    
    
        time.Sleep(1*time.Second)
        v, ok := <- ch1 //其他goroutine,显示的调用close方法关闭通道。
        if !ok{
    
    
            fmt.Println("已经读取了所有的数据,", ok)
            break
        }
        fmt.Println("取出数据:",v, ok)
    }

    fmt.Println("main...over....")
}
func sendData(ch1 chan int)  {
    
    
    // 发送方:10条数据
    for i:=0;i<10 ;i++  {
    
    
        ch1 <- i//将i写入通道中
    }
    close(ch1) //将ch1通道关闭了。
}

在上面的程序中,send Goroutine将0到9写入chl通道,然后关闭通道。主函数里有一个无限循环。它检查通道是否在发送数据后,使用变量ok关闭。如果ok是假的,则意味着通道关闭,因此循环结束。还可以打印接收到的值和ok的值。

二、通道上的范围循环
我们可以循环从通道上获取数据,直到通道关闭。for循环的for range形式可用于从通道接收值,直到它关闭为止。

使用range循环,示例代码:

package main

import (
    "time"
    "fmt"
)

func main()  {
    
    
    ch1 :=make(chan int)
    go sendData(ch1)
    // for循环的for range形式可用于从通道接收值,直到它关闭为止。
    for v := range ch1{
    
    
        fmt.Println("读取数据:",v)
    }
    fmt.Println("main..over.....")
}
func sendData(ch1 chan int)  {
    
    
    for i:=0;i<10 ; i++ {
    
    
        time.Sleep(1*time.Second)
        ch1 <- i
    }
    close(ch1)//通知对方,通道关闭
}

缓冲通道

一、非缓冲通道
之前学习的所有通道基本上都没有缓冲。发送和接收到一个未缓冲的通道是阻塞的。

一次发送操作对应一次接收操作,对于一个goroutine来讲,它的一次发送,在另一个goroutine接收之前都是阻塞的。同样的,对于接收来讲,在另一个goroutine发送之前,它也是阻塞的。

二、缓冲通道
缓冲通道就是指一个通道,带有一个缓冲区。发送到一个缓冲通道只有在缓冲区满时才被阻塞。类似地,从缓冲通道接收的信息只有在缓冲区为空时才会被阻塞。

可以通过将额外的容量参数传递给make函数来创建缓冲通道,该函数指定缓冲区的大小。

语法:

ch := make(chan type, capacity)

上述语法的容量应该大于0,以便通道具有缓冲区。默认情况下,无缓冲通道的容量为0,因此在之前创建通道时省略了容量参数。

三、示例代码
以下的代码中,chan通道,是带有缓冲区的。

package main

import (
    "fmt"
    "strconv"
    "time"
)

func main() {
    
    
    /*
    非缓存通道:make(chan T)
    缓存通道:make(chan T ,size)
        缓存通道,理解为是队列:

    非缓存,发送还是接受,都是阻塞的
    缓存通道,缓存区的数据满了,才会阻塞状态。。

     */
    ch1 := make(chan int)           //非缓存的通道
    fmt.Println(len(ch1), cap(ch1)) //0 0
    //ch1 <- 100//阻塞的,需要其他的goroutine解除阻塞,否则deadlock

    ch2 := make(chan int, 5)        //缓存的通道,缓存区大小是5
    fmt.Println(len(ch2), cap(ch2)) //0 5
    ch2 <- 100                      //
    fmt.Println(len(ch2), cap(ch2)) //1 5

    //ch2 <- 200
    //ch2 <- 300
    //ch2 <- 400
    //ch2 <- 500
    //ch2 <- 600
    fmt.Println("--------------")
    ch3 := make(chan string, 4)
    go sendData3(ch3)
    for {
    
    
        time.Sleep(1*time.Second)
        v, ok := <-ch3
        if !ok {
    
    
            fmt.Println("读完了,,", ok)
            break
        }
        fmt.Println("\t读取的数据是:", v)
    }

    fmt.Println("main...over...")
}

func sendData3(ch3 chan string) {
    
    
    for i := 0; i < 10; i++ {
    
    
        ch3 <- "数据" + strconv.Itoa(i)
        fmt.Println("子goroutine,写出第", i, "个数据")
    }
    close(ch3)
}

定向通道

一、双向通道
通道,channel,是用于实现goroutine之间的通信的。一个goroutine可以向通道中发送数据,另一条goroutine可以从该通道中获取数据。截止到现在我们所学习的通道,都是既可以发送数据,也可以读取数据,我们又把这种通道叫做双向通道。

data := <- a // read from channel a  
a <- data // write to channel a

二、单向通道
单向通道,也就是定向通道。

之前我们学习的通道都是双向通道,我们可以通过这些通道接收或者发送数据。我们也可以创建单向通道,这些通道只能发送或者接收数据。

双向通道,实例代码:

package main

import "fmt"

func main()  {
    
    
    /*
    双向:
        chan T -->
            chan <- data,写出数据,写
            data <- chan,获取数据,读
    单向:定向
        chan <- T,
            只支持写,
        <- chan T,
            只读
     */
    ch1 := make(chan string) // 双向,可读,可写
    done := make(chan bool)
    go sendData(ch1, done)
    data :=<- ch1 //阻塞
    fmt.Println("子goroutine传来:", data)
    ch1 <- "我是main。。" // 阻塞

    <-done
    fmt.Println("main...over....")
}
//子goroutine-->写数据到ch1通道中
//main goroutine-->从ch1通道中取
func sendData(ch1 chan string, done chan bool)  {
    
    
    ch1 <- "我是小明"// 阻塞
    data := <-ch1 // 阻塞
    fmt.Println("main goroutine传来:",data)

    done <- true
}

创建仅能发送数据的通道,示例代码:

package main

import "fmt"

func main()  {
    
    
    /*
        单向:定向
        chan <- T,
            只支持写,
        <- chan T,
            只读

        用于参数传递:
     */
    ch1 := make(chan int)//双向,读,写
    //ch2 := make(chan <- int) // 单向,只写,不能读
    //ch3 := make(<- chan int) //单向,只读,不能写
    //ch1 <- 100
    //data :=<-ch1
    //ch2 <- 1000
    //data := <- ch2
    //fmt.Println(data)
    //  <-ch2 //invalid operation: <-ch2 (receive from send-only type chan<- int)
    //ch3 <- 100
    //  <-ch3
    //  ch3 <- 100 //invalid operation: ch3 <- 100 (send to receive-only type <-chan int)

    //go fun1(ch2)
    go fun1(ch1)
    data:= <- ch1
    fmt.Println("fun1中写出的数据是:",data)

    //fun2(ch3)
    go fun2(ch1)
    ch1 <- 200
    fmt.Println("main。。over。。")
}
//该函数接收,只写的通道
func fun1(ch chan <- int){
    
    
    // 函数内部,对于ch只能写数据,不能读数据
    ch <- 100
    fmt.Println("fun1函数结束。。")
}

func fun2(ch <-chan int){
    
    
    //函数内部,对于ch只能读数据,不能写数据
    data := <- ch
    fmt.Println("fun2函数,从ch中读取的数据是:",data)
}

time包中的通道的相关函数

主要就是定时器,标准库中的Timer让用户可以定义自己的超时逻辑,尤其是在应对select处理多个channel的超时、单channel读写的超时等情形时尤为方便。

Timer是一次性的时间触发事件,这点与Ticker不同,Ticker是按一定时间间隔持续触发时间事件。

Timer常见的创建方式:

t:= time.NewTimer(d)
t:= time.AfterFunc(d, f)
c:= time.After(d)

虽然说创建方式不同,但是原理是相同的。

Timer有3个要素:

定时时间:就是那个d
触发动作:就是那个f
时间channel: 也就是t.C

一、time.NewTimer()
NewTimer()创建一个新的计时器,该计时器将在其通道上至少持续d之后发送当前时间。

它的返回值是一个Timer。

源代码:

// NewTimer creates a new Timer that will send
// the current time on its channel after at least duration d.
func NewTimer(d Duration) *Timer {
    
    
    c := make(chan Time, 1)
    t := &Timer{
    
    
        C: c,
        r: runtimeTimer{
    
    
            when: when(d),
            f:    sendTime,
            arg:  c,
        },
    }
    startTimer(&t.r)
    return t
}

通过源代码我们可以看出,首先创建一个channel,关联的类型为Time,然后创建了一个Timer并返回。

用于在指定的Duration类型时间后调用函数或计算表达式。
如果只是想指定时间之后执行,使用time.Sleep()
使用NewTimer(),可以返回的Timer类型在计时器到期之前,取消该计时器
直到使用<-timer.C发送一个值,该计时器才会过期
示例代码:

package main

import (
“time”
“fmt”
)

func main() {

/*
    1.func NewTimer(d Duration) *Timer
        创建一个计时器:d时间以后触发,go触发计时器的方法比较特别,就是在计时器的channel中发送值
 */
//新建一个计时器:timer
timer := time.NewTimer(3 * time.Second)
fmt.Printf("%T\n", timer) //*time.Timer
fmt.Println(time.Now())   //2019-08-15 10:41:21.800768 +0800 CST m=+0.000461190

//此处在等待channel中的信号,执行此段代码时会阻塞3秒
ch2 := timer.C     //<-chan time.Time
fmt.Println(<-ch2) //2019-08-15 10:41:24.803471 +0800 CST m=+3.003225965 }

二、timer.Stop

package main

import (
    "time"
    "fmt"
)

func main() {
    
    

    /*
        1.func NewTimer(d Duration) *Timer
            创建一个计时器:d时间以后触发,go触发计时器的方法比较特别,就是在计时器的channel中发送值
     */
    //新建一个计时器:timer
    //timer := time.NewTimer(3 * time.Second)
    //fmt.Printf("%T\n", timer) //*time.Timer
    //fmt.Println(time.Now())   //2019-08-15 10:41:21.800768 +0800 CST m=+0.000461190
    //
    此处在等待channel中的信号,执行此段代码时会阻塞3秒
    //ch2 := timer.C     //<-chan time.Time
    //fmt.Println(<-ch2) //2019-08-15 10:41:24.803471 +0800 CST m=+3.003225965

    fmt.Println("-------------------------------")

    //新建计时器,一秒后触发

    timer2 := time.NewTimer(5 * time.Second)

    //新开启一个线程来处理触发后的事件

    go func() {
    
    

        //等触发时的信号

        <-timer2.C

        fmt.Println("Timer 2 结束。。")

    }()

    //由于上面的等待信号是在新线程中,所以代码会继续往下执行,停掉计时器

    time.Sleep(3*time.Second)
    stop := timer2.Stop()

    if stop {
    
    

        fmt.Println("Timer 2 停止。。")

    }

}

三、time.After()
在等待持续时间之后,然后在返回的通道上发送当前时间。它相当于NewTimer(d).C。在计时器触发之前,垃圾收集器不会恢复底层计时器。如果效率有问题,使用NewTimer代替,并调用Timer。如果不再需要计时器,请停止。

源码:

// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {
    
    
    return NewTimer(d).C
}

示例代码:

package main

import (
    "time"
    "fmt"
)

func main() {
    
    

    /*
        func After(d Duration) <-chan Time
            返回一个通道:chan,存储的是d时间间隔后的当前时间。
     */
    ch1 := time.After(3 * time.Second) //3s后
    fmt.Printf("%T\n", ch1) // <-chan time.Time
    fmt.Println(time.Now()) //2019-08-15 09:56:41.529883 +0800 CST m=+0.000465158
    time2 := <-ch1
    fmt.Println(time2) //2019-08-15 09:56:44.532047 +0800 CST m=+3.002662179

}

select语句

select 是 Go 中的一个控制结构。select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

一、语法结构
select语句的语法结构和switch语句很相似,也有case语句和default语句:


```bash
select {
    
    
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s); 
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}

说明:

每个case都必须是一个通信

所有channel表达式都会被求值

所有被发送的表达式都会被求值

如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行。

否则:

如果有default子句,则执行该语句。

如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

二、示例代码
示例代码:

```bash
package main

import (
    "fmt"
    "time"
)

func main() {
    /*
    分支语句:if,switch,select
    select 语句类似于 switch 语句,
        但是select会随机执行一个可运行的case。
        如果没有case可运行,它将阻塞,直到有case可运行。
     */

    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- 200
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- 100
    }()

    select {
    case num1 := <-ch1:
        fmt.Println("ch1中取数据。。", num1)
    case num2, ok := <-ch2:
        if ok {
            fmt.Println("ch2中取数据。。", num2)
        }else{
            fmt.Println("ch2通道已经关闭。。")
        }

    }
}

运行结果:可能执行第一个case,打印100,也可能执行第二个case,打印200。(多运行几次,结果就不同了)

go语言的CSP模型

go语言的最大两个亮点,一个是goroutine,一个就是chan了。二者合体的典型应用CSP,基本就是大家认可的并行开发神器,简化了并行程序的开发难度,我们来看一下CSP。

一、CSP是什么
CSP 是 Communicating Sequential Process 的简称,中文可以叫做通信顺序进程,是一种并发编程模型,是一个很强大的并发数据模型,是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。相对于Actor模型,CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。

严格来说,CSP 是一门形式语言(类似于 ℷ calculus),用于描述并发系统中的互动模式,也因此成为一众面向并发的编程语言的理论源头,并衍生出了 Occam/Limbo/Golang…

而具体到编程语言,如 Golang,其实只用到了 CSP 的很小一部分,即理论中的 Process/Channel(对应到语言中的 goroutine/channel):这两个并发原语之间没有从属关系, Process 可以订阅任意个 Channel,Channel 也并不关心是哪个 Process 在利用它进行通信;Process 围绕 Channel 进行读写,形成一套有序阻塞和可预测的并发模型。

二、Golang CSP
与主流语言通过共享内存来进行并发控制方式不同,Go 语言采用了 CSP 模式。这是一种用于描述两个独立的并发实体通过共享的通讯 Channel(管道)进行通信的并发模型。

Golang 就是借用CSP模型的一些概念为之实现并发进行理论支持,其实从实际上出发,go语言并没有,完全实现了CSP模型的所有理论,仅仅是借用了 process和channel这两个概念。process是在go语言上的表现就是 goroutine 是实际并发执行的实体,每个实体之间是通过channel通讯来实现数据共享。

Go语言的CSP模型是由协程Goroutine与通道Channel实现:

Go协程goroutine: 是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。是一种绿色线程,微线程,它与Coroutine协程也有区别,能够在发现堵塞后启动新的微线程。

通道channel: 类似Unix的Pipe,用于协程之间通讯和同步。协程之间虽然解耦,但是它们和Channel有着耦合。
三、Channel
Goroutine 和 channel 是 Go 语言并发编程的 两大基石。Goroutine 用于执行并发任务,channel 用于 goroutine 之间的同步、通信。

Channel 在 gouroutine 间架起了一条管道,在管道里传输数据,实现 gouroutine 间的通信;由于它是线程安全的,所以用起来非常方便;channel 还提供 “先进先出” 的特性;它还能影响 goroutine 的阻塞和唤醒。

相信大家一定见过一句话:

Do not communicate by sharing memory; instead, share memory by communicating.

不要通过共享内存来通信,而要通过通信来实现内存共享。

这就是 Go 的并发哲学,它依赖 CSP 模型,基于 channel 实现。

channel 实现 CSP

Channel 是 Go 语言中一个非常重要的类型,是 Go 里的第一对象。通过 channel,Go 实现了通过通信来实现内存共享。Channel 是在多个 goroutine 之间传递数据和同步的重要手段。

使用原子函数、读写锁可以保证资源的共享访问安全,但使用 channel 更优雅。

channel 字面意义是 “通道”,类似于 Linux 中的管道。声明 channel 的语法如下:

chan T // 声明一个双向通道
chan<- T // 声明一个只能用于发送的通道
<-chan T // 声明一个只能用于接收的通道

单向通道的声明,用 <- 来表示,它指明通道的方向。你只要明白,代码的书写顺序是从左到右就马上能掌握通道的方向是怎样的。

因为 channel 是一个引用类型,所以在它被初始化之前,它的值是 nil,channel 使用 make 函数进行初始化。可以向它传递一个 int 值,代表 channel 缓冲区的大小(容量),构造出来的是一个缓冲型的 channel;不传或传 0 的,构造的就是一个非缓冲型的 channel。

两者有一些差别:非缓冲型 channel 无法缓冲元素,对它的操作一定顺序是 “发送 -> 接收 -> 发送 -> 接收 -> ……”,如果想连续向一个非缓冲 chan 发送 2 个元素,并且没有接收的话,第一次一定会被阻塞;对于缓冲型 channel 的操作,则要 “宽松” 一些,毕竟是带了 “缓冲” 光环。

在这里插入图片描述

对 chan 的发送和接收操作都会在编译期间转换成为底层的发送接收函数。

Channel 分为两种:带缓冲、不带缓冲。对不带缓冲的 channel 进行的操作实际上可以看作 “同步模式”,带缓冲的则称为 “异步模式”。

同步模式下,发送方和接收方要同步就绪,只有在两者都 ready 的情况下,数据才能在两者间传输(后面会看到,实际上就是内存拷贝)。否则,任意一方先行进行发送或接收操作,都会被挂起,等待另一方的出现才能被唤醒。

异步模式下,在缓冲槽可用的情况下(有剩余容量),发送和接收操作都可以顺利进行。否则,操作的一方(如写入)同样会被挂起,直到出现相反操作(如接收)才会被唤醒。

小结一下:同步模式下,必须要使发送方和接收方配对,操作才会成功,否则会被阻塞;异步模式下,缓冲槽要有剩余容量,操作才会成功,否则也会被阻塞。

简单来说,CSP 模型由并发执行的实体(线程或者进程或者协程)所组成,实体之间通过发送消息进行通信,
这里发送消息时使用的就是通道,或者叫 channel。

CSP 模型的关键是关注 channel,而不关注发送消息的实体。Go 语言实现了 CSP 部分理论,goroutine 对应 CSP 中并发执行的实体,channel 也就对应着 CSP 中的 channel。

四、Goroutine
Goroutine 是实际并发执行的实体,它底层是使用协程(coroutine)实现并发,coroutine是一种运行在用户态的用户线程,类似于 greenthread,go底层选择使用coroutine的出发点是因为,它具有以下特点:

用户空间 避免了内核态和用户态的切换导致的成本
可以由语言和框架层进行调度
更小的栈空间允许创建大量的实例
可以看到第二条 用户空间线程的调度不是由操作系统来完成的,像在java 1.3中使用的greenthread的是由JVM统一调度的(后java已经改为内核线程),还有在ruby中的fiber(半协程) 是需要在重新中自己进行调度的,而goroutine是在golang层面提供了调度器,并且对网络IO库进行了封装,屏蔽了复杂的细节,对外提供统一的语法关键字支持,简化了并发程序编写的成本。

五、Goroutine 调度器
Go并发调度: G-P-M模型

在操作系统提供的内核线程之上,Go搭建了一个特有的两级线程模型。goroutine机制实现了M : N的线程模型,goroutine机制是协程(coroutine)的一种实现,golang内置的调度器,可以让多核CPU中每个CPU执行一个协程。
在这里插入图片描述
六、最后
Golang 的 channel 将 goroutine 隔离开,并发编程的时候可以将注意力放在 channel 上。在一定程度上,这个和消息队列的解耦功能还是挺像的。如果大家感兴趣,还是来看看 channel 的源码吧,对于更深入地理解 channel 还是挺有用的。

Go 通过 channel 实现 CSP 通信模型,主要用于 goroutine 之间的消息传递和事件通知。

有了 channel 和 goroutine 之后,Go 的并发编程变得异常容易和安全,得以让程序员把注意力留到业务上去,实现开发效率的提升。

要知道,技术并不是最重要的,它只是实现业务的工具

反射机制

反射reflect

一、引入
先看官方Doc中Rob Pike给出的关于反射的定义:

Reflection in computing is the ability of a program to examine its own structure, particularly through types; it’s a form of metaprogramming. It’s also a great source of confusion.
(在计算机领域,反射是一种让程序——主要是通过类型——理解其自身结构的一种能力。它是元编程的组成之一,同时它也是一大引人困惑的难题。)

维基百科中的定义:

在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。
不同语言的反射模型不尽相同,有些语言还不支持反射。

《Go 语言圣经》中是这样定义反射的:

Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。

为什么要用反射

需要反射的 2 个常见场景:

有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。
有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。
但是对于反射,还是有几点不太建议使用反射的理由:

与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。
二、相关基础
反射是如何实现的?我们以前学习过 interface,它是 Go 语言实现抽象的一个非常强大的工具。当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的,反射建立在类型的基础上。

Go 语言在 reflect 包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。在进行更加详细的了解之前,我们需要重新温习一下Go语言相关的一些特性,所谓温故知新,从这些特性中了解其反射机制是如何使用的。

特点 说明
go语言是静态类型语言。 编译时类型已经确定,比如对已基本数据类型的再定义后的类型,反射时候需要确认返回的是何种类型。
空接口interface{} go的反射机制是要通过接口来进行的,而类似于Java的Object的空接口可以和任何类型进行交互,因此对基本数据类型等的反射也直接利用了这一特点
Go语言的类型:

变量包括(type, value)两部分

​ 理解这一点就知道为什么nil != nil了

type 包括 static type和concrete type. 简单来说 static type是你在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型

类型断言能否成功,取决于变量的concrete type,而不是static type。因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer。

Go是静态类型语言。每个变量都拥有一个静态类型,这意味着每个变量的类型在编译时都是确定的:int,float32, *AutoType, []byte, chan []int 诸如此类。

在反射的概念中, 编译时就知道变量类型的是静态类型;运行时才知道一个变量类型的叫做动态类型。

静态类型
静态类型就是变量声明时的赋予的类型。比如:


type MyInt int // int 就是静态类型

type A struct{
Name string // string就是静态
}
var i int // int就是静态类型

- 动态类型
  动态类型:运行时给这个变量赋值时,这个值的类型(如果值为nil的时候没有动态类型)。一个变量的动态类型在运行时可能改变,这主要依赖于它的赋值(前提是这个变量是接口类型)。

```go
var A interface{
    
    } // 静态类型interface{
    
    }
A = 10            // 静态类型为interface{
    
    }  动态为int
A = "String"      // 静态类型为interface{
    
    }  动态为string
var M *int
A = M             // A的值可以改变

Go语言的反射就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。

在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:

(value, type)

value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。

例如,创建类型为*os.File的变量,然后将其赋给一个接口变量r:

tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)

var r io.Reader
r = tty

接口变量r的pair中将记录如下信息:(tty, *os.File),这个pair在接口变量的连续赋值过程中是不变的,将接口变量r赋给另一个接口变量w:

var w io.Writer
w = r.(io.Writer)

接口变量w的pair与r的pair相同,都是:(tty, *os.File),即使w是空接口类型,pair也是不变的。

interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。

所以我们要理解两个基本概念 Type 和 Value,它们也是 Go语言包中 reflect 空间里最重要的两个类型。

三、Type和Value
我们一般用到的包是reflect包。

既然反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢? 它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf(),看看官方的解释

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i.  ValueOf(nil) returns the zero 
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

reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value。

首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数。

t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值
package main

import (
    "fmt"
    "reflect"
)

func main() {
    
    
    //反射操作:通过反射,可以获取一个接口类型变量的 类型和数值
    var x float64 =3.4

    fmt.Println("type:",reflect.TypeOf(x)) //type: float64
    fmt.Println("value:",reflect.ValueOf(x)) //value: 3.4

    fmt.Println("-------------------")
    //根据反射的值,来获取对应的类型和数值
    v := reflect.ValueOf(x)
    fmt.Println("kind is float64: ",v.Kind() == reflect.Float64)
    fmt.Println("type : ",v.Type())
    fmt.Println("value : ",v.Float())
}

运行结果:

type: float64
value: 3.4
-------------------
kind is float64:  true
type :  float64
value :  3.4

说明

reflect.TypeOf: 直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型
reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1 “Allen.Wu” 25} 这样的结构体struct的值
也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种
Type 和 Value 都包含了大量的方法,其中第一个有用的方法应该是 Kind,这个方法返回该类型的具体信息:Uint、Float64 等。Value 类型还包含了一系列类型方法,比如 Int(),用于返回对应的值。以下是Kind的种类:

// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint

const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Func
    Interface
    Map
    Ptr
    Slice
    String
    Struct
    UnsafePointer
)

reflect对象获取接口变量信息

一、反射的规则
其实反射的操作步骤非常的简单,就是通过实体对象获取反射对象(Value、Type),然后操作相应的方法即可。

下图描述了实例、Value、Type 三者之间的转换关系:

在这里插入图片描述
反射 API 的分类总结如下:

  1. 从实例到 Value

通过实例获取 Value 对象,直接使用 reflect.ValueOf() 函数。例如:

func ValueOf(i interface {
    
    }) Value
  1. 从实例到 Type

通过实例获取反射对象的 Type,直接使用 reflect.TypeOf() 函数。例如:

func TypeOf(i interface{
    
    }) Type
  1. 从 Type 到 Value

Type 里面只有类型信息,所以直接从一个 Type 接口变量里面是无法获得实例的 Value 的,但可以通过该 Type 构建一个新实例的 Value。reflect 包提供了两种方法,示例如下:

//New 返回的是一个 Value,该 Value 的 type 为 PtrTo(typ),即 Value 的 Type 是指定 typ 的指针类型
func New(typ Type) Value
//Zero 返回的是一个 typ 类型的零佳,注意返回的 Value 不能寻址,位不可改变
func Zero(typ Type) Value

如果知道一个类型值的底层存放地址,则还有一个函数是可以依据 type 和该地址值恢复出 Value 的。例如:

func NewAt(typ Type, p unsafe.Pointer) Value
  1. 从 Value 到 Type

从反射对象 Value 到 Type 可以直接调用 Value 的方法,因为 Value 内部存放着到 Type 类型的指针。例如:

func (v Value) Type() TypeCOPY
  1. 从 Value 到实例

Value 本身就包含类型和值信息,reflect 提供了丰富的方法来实现从 Value 到实例的转换。例如:

//该方法最通用,用来将 Value 转换为空接口,该空接口内部存放具体类型实例
//可以使用接口类型查询去还原为具体的类型
func (v Value) Interface() (i interface{
    
    })

//Value 自身也提供丰富的方法,直接将 Value 转换为简单类型实例,如果类型不匹配,则直接引起 panic
func (v Value) Bool () bool
func (v Value) Float() float64
func (v Value) Int() int64
func (v Value) Uint() uint64
  1. 从 Value 的指针到值

从一个指针类型的 Value 获得值类型 Value 有两种方法,示例如下。

//如果 v 类型是接口,则 Elem() 返回接口绑定的实例的 Value,如采 v 类型是指针,则返回指针值的 Value,否则引起 panic
func (v Value) Elem() Value
//如果 v 是指针,则返回指针值的 Value,否则返回 v 自身,该函数不会引起 panic
func Indirect(v Value) Value
  1. Type 指针和值的相互转换

指针类型 Type 到值类型 Type。例如:

//t 必须是 Array、Chan、Map、Ptr、Slice,否则会引起 panic
//Elem 返回的是其内部元素的 Type
t.Elem() TypeCOPY
值类型 Type 到指针类型 Type。例如:

//PtrTo 返回的是指向 t 的指针型 Type
func PtrTo(t Type) Type
  1. Value 值的可修改性

Value 值的修改涉及如下两个方法:

//通过 CanSet 判断是否能修改
func (v Value ) CanSet() bool
//通过 Set 进行修改
func (v Value ) Set(x Value)

Value 值在什么情况下可以修改?我们知道实例对象传递给接口的是一个完全的值拷贝,如果调用反射的方法 reflect.ValueOf() 传进去的是一个值类型变量, 则获得的 Value 实际上是原对象的一个副本,这个 Value 是无论如何也不能被修改的。

根据 Go 官方关于反射的博客,反射有三大定律:

Reflection goes from interface value to reflection object. Reflection
goes from reflection object to interface value. To modify a reflection
object, the value must be settable.

第一条是最基本的:反射可以从接口值得到反射对象。

​ 反射是一种检测存储在 interface中的类型和值机制。这可以通过 TypeOf函数和 ValueOf函数得到。

第二条实际上和第一条是相反的机制,反射可以从反射对象获得接口值。

​ 它将 ValueOf的返回值通过 Interface()函数反向转变成 interface变量。

前两条就是说 接口型变量和 反射类型对象可以相互转化,反射类型对象实际上就是指的前面说的 reflect.Type和 reflect.Value。

第三条不太好懂:如果需要操作一个反射变量,则其值必须可以修改。

​ 反射变量可设置的本质是它存储了原变量本身,这样对反射变量的操作,就会反映到原变量本身;反之,如果反射变量不能代表原变量,那么操作了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。

二、反射的使用
3.1 从relfect.Value中获取接口interface的信息
当执行reflect.ValueOf(interface)之后,就得到了一个类型为”relfect.Value”变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。不过,我们可能是已知原有类型,也有可能是未知原有类型,因此,下面分两种情况进行说明。

已知原有类型
已知类型后转换为其对应的类型的做法如下,直接通过Interface方法然后强制转换,如下:

realValue := value.Interface().(已知的类型)

示例代码:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    
    
    var num float64 = 1.2345

    pointer := reflect.ValueOf(&num)
    value := reflect.ValueOf(num)

    // 可以理解为“强制转换”,但是需要注意的时候,转换的时候,如果转换的类型不完全符合,则直接panic
    // Golang 对类型要求非常严格,类型一定要完全符合
    // 如下两个,一个是*float64,一个是float64,如果弄混,则会panic
    convertPointer := pointer.Interface().(*float64)
    convertValue := value.Interface().(float64)

    fmt.Println(convertPointer)
    fmt.Println(convertValue)
}

运行结果:

0xc000098000
1.2345

说明

转换的时候,如果转换的类型不完全符合,则直接panic,类型要求非常严格!
转换的时候,要区分是指针还是指
也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
未知原有类型
很多情况下,我们可能并不知道其具体类型,那么这个时候,该如何做呢?需要我们进行遍历探测其Filed来得知,示例如下:

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    
    
    Name string
    Age int
    Sex string
}

func (p Person)Say(msg string)  {
    
    
    fmt.Println("hello,",msg)
}
func (p Person)PrintInfo()  {
    
    
    fmt.Printf("姓名:%s,年龄:%d,性别:%s\n",p.Name,p.Age,p.Sex)
}

func main() {
    
    
    p1 := Person{
    
    "王二狗",30,"男"}

    DoFiledAndMethod(p1)

}

// 通过接口来获取任意参数
func DoFiledAndMethod(input interface{
    
    }) {
    
    

    getType := reflect.TypeOf(input) //先获取input的类型
    fmt.Println("get Type is :", getType.Name()) // Person
    fmt.Println("get Kind is : ", getType.Kind()) // struct

    getValue := reflect.ValueOf(input)
    fmt.Println("get all Fields is:", getValue) //{
    
    王二狗 30}

    // 获取方法字段
    // 1. 先获取interface的reflect.Type,然后通过NumField进行遍历
    // 2. 再通过reflect.Type的Field获取其Field
    // 3. 最后通过Field的Interface()得到对应的value
    for i := 0; i < getType.NumField(); i++ {
    
    
        field := getType.Field(i)
        value := getValue.Field(i).Interface() //获取第i个值
        fmt.Printf("字段名称:%s, 字段类型:%s, 字段数值:%v \n", field.Name, field.Type, value)
    }

    // 通过反射,操作方法
    // 1. 先获取interface的reflect.Type,然后通过.NumMethod进行遍历
    // 2. 再公国reflect.Type的Method获取其Method
    for i := 0; i < getType.NumMethod(); i++ {
    
    
        method := getType.Method(i)
        fmt.Printf("方法名称:%s, 方法类型:%v \n", method.Name, method.Type)
    }
}

运行结果:

get Type is : Person
get Kind is :  struct
get all Fields is: {
    
    王二狗 30}
字段名称:Name, 字段类型:string, 字段数值:王二狗 
字段名称:Age, 字段类型:int, 字段数值:30 
字段名称:Sex, 字段类型:string, 字段数值:男 
方法名称:PrintInfo, 方法类型:func(main.Person) 
方法名称:Say, 方法类型:func(main.Person, string) 

说明

通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为:

先获取interface的reflect.Type,然后通过NumField进行遍历
再通过reflect.Type的Field获取其Field
最后通过Field的Interface()得到对应的value
通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:

先获取interface的reflect.Type,然后通过NumMethod进行遍历
再分别通过reflect.Type的Method获取对应的真实的方法(函数)
最后对结果取其Name和Type得知具体的方法名
也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
struct 或者 struct 的嵌套都是一样的判断处理方式

通过reflect.Value设置实际变量的值

reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

也就是说:要想修改一个变量的值,那么必须通过该变量的指针地址 , 取消指针的引用 。通过refPtrVal := reflect.Valueof( &var )的方式获取指针类型,你使用refPtrVal.elem( ).set(一个新的reflect.Value)来进行更改,传递给set()的值也必须是一个reflect.value。

这里需要一个方法:

解释起来就是:Elem返回接口v包含的值或指针v指向的值。如果v的类型不是interface或ptr,它会恐慌。如果v为零,则返回零值。

如果你的变量是一个指针、map、slice、channel、Array。那么你可以使用reflect.Typeof(v).Elem()来确定包含的类型。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    
    

    var num float64 = 1.2345
    fmt.Println("num的数值:", num)

    //需要操作指针
    //通过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值
    pointer := reflect.ValueOf(&num)
    newValue := pointer.Elem()

    fmt.Println("类型 :", newValue.Type()) //float64
    fmt.Println("是否可以修改:", newValue.CanSet())

    // 重新赋值
    newValue.SetFloat(77)
    fmt.Println("新的数值:", num)

    

    // 如果reflect.ValueOf的参数不是指针,会如何?

    //尝试直接修改
    //value := reflect.ValueOf(num)
    //value.SetFloat(6.28) //panic: reflect: reflect.Value.SetFloat using unaddressable value
    //fmt.Println(value.CanSet()) //false

    //pointer = reflect.ValueOf(num)
    //newValue = value.Elem() // 如果非指针,这里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value”
}

运行结果:

num的数值: 1.2345
类型 : float64
是否可以修改: true
新的数值: 77

说明

需要传入的参数是* float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,注意一定要是指针。
如果传入的参数不是指针,而是变量,那么
通过Elem获取原始值对应的对象则直接panic
通过CanSet方法查询是否可以设置返回false
newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。
reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的
也就是说如果要修改反射类型对象,其值必须是“addressable”【对应的要传入的是指针,同时要通过Elem方法获取原始值对应的反射对象】
struct 或者 struct 的嵌套都是一样的判断处理方式
尝试修改结构体中的字段数值:

package main

import (
    "reflect"
    "fmt"
)

type Student struct {
    
    
    Name string
    Age int
    School string
}
func main()  {
    
    
    /*
    通过反射,来更改对象的数值:前提是数据可以被更改
     */
    s1:=Student{
    
    "王二狗",19,"千锋教育"}
    fmt.Printf("%T\n",s1) //main.Student
    p1:=&s1
    fmt.Printf("%T\n",p1) //*main.Student
    fmt.Println(s1.Name)
    fmt.Println((*p1).Name,p1.Name)

    v1:= reflect.ValueOf(&s1) // value

    if v1.Kind()==reflect.Ptr{
        fmt.Println(v1.Elem().CanSet())
        v1 = v1.Elem()
    }

    f1:=v1.FieldByName("Name")
    f1.SetString("韩茹")
    f3:=v1.FieldByName("School")
    f3.SetString("幼儿园")
    fmt.Println(s1)

}

运行结果:

main.Student
*main.Student
王二狗
王二狗 王二狗
true
{
    
    韩茹 19 幼儿园}

通过reflect.Value来进行方法的调用

这算是一个高级用法了,前面我们只说到对类型、变量的几种反射的用法,包括如何获取其值、其类型、以及如何重新设置新值。但是在项目应用中,另外一个常用并且属于高级的用法,就是通过reflect来进行方法【函数】的调用。比如我们要做框架工程的时候,需要可以随意扩展方法,或者说用户可以自定义方法,那么我们通过什么手段来扩展让用户能够自定义呢?关键点在于用户的自定义方法是未可知的,因此我们可以通过reflect来搞定。

Call()方法:

通过反射,调用方法。

先获取结构体对象,然后

示例代码:

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    
    
    Name string
    Age int
    Sex string
}

func (p Person)Say(msg string)  {
    
    
    fmt.Println("hello,",msg)
}
func (p Person)PrintInfo()  {
    
    
    fmt.Printf("姓名:%s,年龄:%d,性别:%s\n",p.Name,p.Age,p.Sex)
}

func (p Person) Test(i,j int,s string){
    
    
    fmt.Println(i,j,s)
}

// 如何通过反射来进行方法的调用?
// 本来可以用结构体对象.方法名称()直接调用的,
// 但是如果要通过反射,
// 那么首先要将方法注册,也就是MethodByName,然后通过反射调动mv.Call

func main() {
    
    
    p2 := Person{
    
    "Ruby",30,"男"}
    // 1. 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,
    // 得到“反射类型对象”后才能做下一步处理
    getValue := reflect.ValueOf(p2)

    // 2.一定要指定参数为正确的方法名
    // 先看看没有参数的调用方法

    methodValue1 := getValue.MethodByName("PrintInfo")
    fmt.Printf("Kind : %s, Type : %s\n",methodValue1.Kind(),methodValue1.Type())
    methodValue1.Call(nil) //没有参数,直接写nil

    args1 := make([]reflect.Value, 0) //或者创建一个空的切片也可以
    methodValue1.Call(args1)

    // 有参数的方法调用
    methodValue2 := getValue.MethodByName("Say")
    fmt.Printf("Kind : %s, Type : %s\n",methodValue2.Kind(),methodValue2.Type())
    args2 := []reflect.Value{
    
    reflect.ValueOf("反射机制")}
    methodValue2.Call(args2)

    methodValue3 := getValue.MethodByName("Test")
    fmt.Printf("Kind : %s, Type : %s\n",methodValue3.Kind(),methodValue3.Type())
    args3 := []reflect.Value{
    
    reflect.ValueOf(100), reflect.ValueOf(200),reflect.ValueOf("Hello")}

    methodValue3.Call(args3)
}

运行结果:

Kind : func, Type : func()
姓名:Ruby,年龄:30,性别:男
姓名:Ruby,年龄:30,性别:男
Kind : func, Type : func(string)
hello, 反射机制
Kind : func, Type : func(int, int, string)
100 200 Hello

通过反射,调用函数。

首先我们要先确认一点,函数像普通的变量一样,之前的章节中我们在讲到函数的本质的时候,是可以把函数作为一种变量类型的,而且是引用类型。如果说Fun()是一个函数,那么f1 := Fun也是可以的,那么f1也是一个函数,如果直接调用f1(),那么运行的就是Fun()函数。

那么我们就先通过ValueOf()来获取函数的反射对象,可以判断它的Kind,是一个func,那么就可以执行Call()进行函数的调用。

示例代码:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    
    
    //函数的反射
    f1 := fun1
    value := reflect.ValueOf(f1)
    fmt.Printf("Kind : %s , Type : %s\n",value.Kind(),value.Type()) //Kind : func , Type : func()

    value2 := reflect.ValueOf(fun2)
    fmt.Printf("Kind : %s , Type : %s\n",value2.Kind(),value2.Type()) //Kind : func , Type : func(int, string)

    //通过反射调用函数
    value.Call(nil)

    value2.Call([]reflect.Value{
    
    reflect.ValueOf(100),reflect.ValueOf("hello")})

}

func fun1(){
    
    
    fmt.Println("我是函数fun1(),无参的。。")
}

func fun2(i int, s string){
    
    
    fmt.Println("我是函数fun2(),有参数。。",i,s)
}

说明

要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理
reflect.Value.MethodByName这个MethodByName,需要指定准确真实的方法名字,如果错误将直接panic,MethodByName返回一个函数值对应的reflect.Value方法的名字。
[]reflect.Value,这个是最终需要调用的方法的参数,可以没有或者一个或者多个,根据实际参数来定。
reflect.Value的 Call 这个方法,这个方法将最终调用真实的方法,参数务必保持一致,如果reflect.Value.Kind不是一个方法,那么将直接panic。
本来可以用对象访问方法直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后通过反射调用methodValue.Call

猜你喜欢

转载自blog.csdn.net/weixin_45843419/article/details/124485821