golang特性深入

综述

本篇为Golang基础知识的第二篇,这一篇主要围绕着Golang的高级部分进行展开。其中会包括Golang的协程、锁、网络等内容。

下面是正文内容,本次分享还是使用理论+实操的方式进行。

1、协程的使用

做过java研发的应该都基本使用过线程,我们知道线程是轻量级进程,而线程再给我们提高程序并发与效率的同时,其实也会增加程序处理的复杂性,那么Golang中是如何来进行“线程”的定义呢?我们先举一个简单的例子。

package main
 
import "fmt"
 
func PrintHello(){
    fmt.Println("Hello golang routine")
}
 
func main() {
    fmt.Println("golan routine starting...")
    PrintHello()
}

如上这段程序很简单,不出所料他会输出如下的结果:

golan routine starting...
Hello golang routine

那么我们如何来定义一个协程呢?其实很简单,只需要在PrintHello()方法前加关键字“go”即可。如下:

package main
 
import "fmt"
 
func PrintHello(){
    fmt.Println("Hello golang routine")
}
 
func main() {
    fmt.Println("golan routine starting...")
    go PrintHello()
}

不过,很奇怪的是,我们发现并没有输出我们想要的结果:

golan routine starting...

这里面有个比较深入的原因,就是其实main函数也是一个协程,成为“主协程”,而主协程在运行的时候其实阻塞了子协程的运行,那么我们如何能够让程序正常输出呢?

package main
 
import (
    "fmt"
    "time"
)
 
func PrintHello(){
    fmt.Println("Hello golang routine")
}
 
func main() {
    fmt.Println("golan routine starting...")
    go PrintHello()
    time.Sleep(time.Millisecond*5)
}      

如上程序,我们通过加入一个睡眠等待时间,让主程序在运行的时候做一个睡眠阻塞,从而可以将控制权交给子协程,并且不会有其他调度去调度主协程,这样,我们就可以看到程序进行了正常的输出。

golan routine starting...
Hello golang routine    

接下来,我们看一个多协程的例子。假如我们再定义一个函数:PrintSay(),如下:

package main
 
import (
    "fmt"
    "time"
)
 
func PrintHello(){
    fmt.Println("Hello golang routine")
}
 
func PrintSay(){
    fmt.Println("say golang routine")
}
 
func main() {
    fmt.Println("golan routine starting...")
    go PrintHello()
    go PrintSay()
    time.Sleep(time.Millisecond*5)
}    

我们可以看到如下的输出结果:

golan routine starting...
Hello golang routine
say golang routine    

我们尝试给每一个函数定义一个时长,假如第一个函数和第二个函数所睡的时间都比主函数的时间长,那么会发生什么情况:

package main
 
import (
    "fmt"
    "time"
)
 
func PrintHello(){
    time.Sleep(time.Millisecond*10)
    fmt.Println("Hello golang routine")
}
 
func PrintSay(){
    time.Sleep(time.Millisecond*8)
    fmt.Println("say golang routine")
}
 
func main() {
    fmt.Println("golan routine starting...")
    go PrintHello()
    go PrintSay()
    time.Sleep(time.Millisecond*5)
}    

我们可以看到如下的输出:

golan routine starting...    

这个结果正如我上面所说的,其实是子协程依赖于主协程,主协程已经运行结束了,所以子协程也无法被调度到。

下面我们调整一下,让主协程的阻塞时间更长点:

package main
 
import (
    "fmt"
    "time"
)
 
func PrintHello(){
    time.Sleep(time.Millisecond*10)
    fmt.Println("Hello golang routine")
}
 
func PrintSay(){
    time.Sleep(time.Millisecond*8)
    fmt.Println("say golang routine")
}
 
func main() {
    fmt.Println("golan routine starting...")
    go PrintHello()
    go PrintSay()
    time.Sleep(time.Millisecond*20)
}    

我们可以看到程序进行了正常的输出。

golan routine starting...
say golang routine
Hello golang routine    

2、匿名协程

既然会有匿名函数,那么也有匿名协程,我们看下在Golang中匿名协程是如何来定义的。

package main
 
import (
    "fmt"
    "time"
)
 
func PrintHello(){
    fmt.Println("Hello golang routine")
}
 
func PrintSay(){
    fmt.Println("say golang routine")
}
 
func main() {
    fmt.Println("golan routine starting...")
    go func() {
        fmt.Println("another golang routine")
    }()
    time.Sleep(time.Millisecond*5)
}

如上程序,我们看下输出的结果。

golan routine starting...
another golang routine    

那多个协程之间如何进行通信呢?在讲通信之前,我们先介绍下锁。

3、锁相关

锁我们应该或多或少都用过,而且锁也会有各种区分,比如乐观锁,悲观所,读锁,写锁等等,那么在Golang中我们如何来使用锁呢?

在Golang中其实也分为读写锁、普通锁等,我们看一个简单的例子。

package main
 
import (
    "fmt"
    "sync"
)
 
func main() {
    var mu sync.Mutex
    mu.Lock()
    fmt.Println("Hello Lock")
    mu.Unlock()
}

如上就是一个简单的普通锁定义的例子。

我们在看一个稍微复杂的例子。

package main
 
import (
    "fmt"
    "sync"
    "time"
)
 
var Mutex sync.Mutex
 
func Printer(s string) {
    for _, data := range s {
        fmt.Printf("%c ", data)
    }
    fmt.Println("")
}
 
func main() {
    //创建协程
    go Printer("hello")
    go Printer("world")
    go Printer("!")
 
    time.Sleep(time.Second * 2)
}    

我们可以看到程序会随机地输出,比如输出如下的结果:

!
w o h e l l o
r l d    

那么如何才能够按照顺序输出我们想要的“Hello World!”呢?

这个时候就需要用到锁了,看如下调整的程序:

package main
 
import (
    "fmt"
    "sync"
    "time"
)
 
var Mutex sync.Mutex
 
func Printer(s string) {
    Mutex.Lock()
    for _, data := range s {
        fmt.Printf("%c ", data)
    }
    fmt.Println("")
    Mutex.Unlock()
}
 
func main() {
    //创建协程
    go Printer("hello")
    go Printer("world")
    go Printer("!")
 
    time.Sleep(time.Second * 2)
}    

经过加锁,我们就可以看到程序进行了正常的输出。

除了普通的互斥锁,Golang中还有读写锁RWMutex。

4、通道处理

如果说 goroutine 是 Go语言程序的并发体的话,那么 channels 就是它们之间的通信机制。一个 channels 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。

如下是Goroutine与Channel进行通信的一幅图。

下面我们以一个示例来学习一下通道的使用。

package main
 
import (
    "fmt"
    "log"
)
 
func main() {
    ch := make(chan int)
    go func() {
        log.Printf("Hello World\n")
        ch <- 0
        log.Printf("!")
    }()
    fmt.Printf("Channel Test Start...\n")
    <- ch
    fmt.Printf("Channel Test End...\n")
}

我们可以看到如下的一段输出:

Channel Test Start...
2019/12/25 16:30:11 Hello World
Channel Test End...
2019/12/25 16:30:11 !

也会有这样的输出:

Channel Test Start...
Channel Test End...
2019/12/25 16:30:47 Hello World
2019/12/25 16:30:47 !      

从以上的结果,我们可以看到,main函数主协程会阻塞等待信道中有值存在。而子协程一旦有值会及时地通知主协程。

下面我们看一个通道信息循环接受的例子。

package main
 
import (
    "fmt"
    "time"
)
 
func main() {
 
    ch := make(chan int)
 
    go func() {
        for i:=3;i>=0;i-- {
            ch <- i
            time.Sleep(time.Second)
        }
    }()
 
    for data := range ch {
        fmt.Println(data)
        if data<=0 {
            break
        }
    }
}      

如上是一个子协程循环对信道产生信息,主函数协程循环去获取信息的过程。从上我们也可以看到chan的数据结构其实是一种first in,first out的队列形式。

5、网络相关-TCP

记得我们在第一篇简单介绍了关于网络部分的内容。我们再重新回顾一下一个简单服务器的编写。

看如下的简单程序:

package main
 
import (
    "io"
    "log"
    "net/http"
)
 
func main() {
    http.HandleFunc("/index", func(writer http.ResponseWriter, request *http.Request) {
        writer.WriteHeader(200)
        io.WriteString(writer,"Hello World")
    })
    error := http.ListenAndServe(":8080",nil)
    if error!=nil {
        log.Fatal(error)
    }
}    

我们打开浏览器后可以看到输出相关的内容。

以上的程序其实还是针对于http层面来说,那么Golang中的更接近于底层的Socket如何进行呢?

我们看如下的一个简单的例子。

首先定义一个服务端:

package main
 
import (
    "fmt"
    "log"
    "net"
    "strings"
)
 
func handleConnection(conn net.Conn){
    if conn==nil {
        log.Fatal("无效的连接")
        return
    }
 
    buf := make([]byte,4096)
    for{
        cnt,err := conn.Read(buf)
        if cnt==0||err!=nil {
            conn.Close()
            break
        }
        inputStr := strings.TrimSpace(string(buf[0:cnt]))
        cInputs := strings.Split(inputStr, " ")
        fCommand := cInputs[0]
 
        fmt.Println("客户端传输->" + fCommand)
 
        switch fCommand {
        case "ping":
            conn.Write([]byte("服务器端回复-> pong\n"))
        case "hello":
            conn.Write([]byte("服务器端回复-> world\n"))
        default:
            conn.Write([]byte("服务器端回复" + fCommand + "\n"))
        }
        fmt.Printf("来自 %v 的连接关闭\n", conn.RemoteAddr())
    }
}
 
func main() {
    listen,error := net.Listen("TCP",":9090")
    if error != nil {
        //log.Panic("The conn is error!")
    }
 
    for {
        conn,error := listen.Accept()
        if error !=nil {
            continue
        }
        log.Println(conn)
        go handleConnection(conn)
    }
}    

同时,我们也定义一个客户端,这样就能完成客户端和服务端之间的通信。

package main
 
import (
    "bufio"
    "fmt"
    "net"
    "os"
    "strings"
)
 
func cConnHandler(c net.Conn) {
    //返回一个拥有 默认size 的reader,接收客户端输入
    reader := bufio.NewReader(os.Stdin)
    //缓存 conn 中的数据
    buf := make([]byte, 1024)
 
    fmt.Println("请输入客户端请求数据...")
 
    for {
        input, _ := reader.ReadString('\n')
        input = strings.TrimSpace(input)
        c.Write([]byte(input))
        cnt, err := c.Read(buf)
 
        if err != nil {
            fmt.Printf("客户端读取数据失败 %s\n", err)
            continue
        }
        fmt.Print("服务器端回复" + string(buf[0:cnt]))
    }
}
 
func ClientSocket() {
    conn, err := net.Dial("tcp", "127.0.0.1:8087")
    if err != nil {
        fmt.Println("客户端建立连接失败")
        return
    }
 
    cConnHandler(conn)
}
 
func main() {
    ClientSocket()
}    

6、网络相关-客户端

做过java的应该都知道,java中有httpclient可以去调用远程的接口或者某一个链接,那么在golang中如何进行呢?我们看如下一个案例,主要就是来爬百度官网的首页信息:

package main
 
import (
    "fmt"
    "io/ioutil"
    "net/http"
)
 
func main() {
    resp,error := http.Get("http://www.baidu.com")
    if error != nil {
        fmt.Println(error.Error())
        return
    }
 
    defer resp.Body.Close()
    body,error := ioutil.ReadAll(resp.Body)
    htmltpl := string(body[:])
    fmt.Println(htmltpl)
     
}

这样,我们就可以爬到百度官网的首页信息了。

以上,就是Golang中我们常用的一些高级部分的特性,后面我们会利用这些特性去研究系列开源的产品,让大家对整个Golang体系有更多更深的理解。

—————–EOF——————



Previous     Next
mjgao /