综述
本篇为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——————