上面的两个特征到底是什么意思呢?下面我们通过具体的事例详细的讲述一下,
package main
import "fmt"
func main() {
for i := 0; i < 10; i ++ {
func(i int){
for {
fmt.Println("Goroutine :" , i)
}
}(i)
}
}
上面这段代码有问题么? 这就是一个从 0 到 10 的调用,但是匿名函数内部没有中止条件,所以会进入一个死循环。要是我们在匿名函数前加上 go 关键字,就不是刚才的意思了,就变成并发执行这个函数。主程序继续向下跑,同时并发开了一个函数,就相当于开了一个线程,当然我们后面会继续介绍,这个叫 协程
package main
import "fmt"
func main() {
for i := 0; i < 10; i ++ {
go func(i int){
for {
fmt.Println("Goroutine :" , i)
}
}(i)
}
}
我们再执行这段代码,发现什么都没有输出,这又是为什么呢?因为这个 main
和 fmt.Println
是并发执行的,我们还来不及print结果, main
就执行完成退出了。Go语言一旦main函数退出了,所有的Goroutine就被杀掉了。
当然要是想看到输出结果,main函数可以在最后sleep一下
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 10; i ++ {
go func(i int){
for {
fmt.Println("Goroutine :" , i)
}
}(i)
}
time.Sleep(time.Millisecond)
}
这时候就有相关的结果输出了。那我们将现在的10改成1000,又会怎样呢?当然还是可以正常输出的,熟悉操作系统的都知道正常的线程几十个上百个是没啥问题的,1000个还是有点难度的,其它语言通常使用异步IO的方式。在Go语言中我们不用管10个、100个、1000个代码还是一样的写法。
非抢占式多任务 这又是什么意思呢?下面我们用一个例子来解释一下
package main
import (
"time"
"fmt"
)
func main() {
var a [10]int
for i := 0; i < 10; i ++ {
go func(i int){
for {
a[i] ++
}
}(i)
}
time.Sleep(time.Millisecond)
fmt.Println(a)
}
在运行之前,可以想一下会输出什么呢? 什么也没有输出,进入了死循环。
上图是我的活动监视器的截图,因为是4核的机器,几乎全部占满了。退不出的原因是因为Goroutine ai 交不出控制权,没有yield出去,同时main函数也是一个goroutine,因为没人交出控制权,所以下面的sleep永远也不会执行。
那我该如何交出控制权呢?我们可以做一个IO操作可以交出控制权,当然也可以手动交出控制权
package main
import (
"time"
"fmt"
"runtime"
)
func main() {
var a [10]int
for i := 0; i < 10; i ++ {
go func(i int){
for {
a[i] ++
runtime.Gosched()
}
}(i)
}
time.Sleep(time.Millisecond)
fmt.Println(a)
}
只需要加上 runtime.Gosched()
,这样大家都主动让出控制权,这时候代码可以正常输出了
[321 986 890 880 831 881 919 904 861 904]
Process finished with exit code 0
如果我们把goroutine的参数 i 去掉会怎样呢?
直接的看语法上没有什么问题,就变成了一个闭包,使用外部的变量 i ,
package main
import (
"time"
"fmt"
"runtime"
)
func main() {
var a [10]int
for i := 0; i < 10; i ++ {
go func(){
for {
a[i] ++
runtime.Gosched()
}
}()
}
time.Sleep(time.Millisecond)
fmt.Println(a)
}
运行之后会出现什么问题呢?
panic: runtime error: index out of range
goroutine 6 [running]:
main.main.func1(0xc42001a0f0, 0xc42001c060)
/Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:15 +0x45
created by main.main
/Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:13 +0x95
Process finished with exit code 2
这里我们通过Go语言的 go run -race xxx.go
,执行分析一下
sheng$ go run -race route.go
==================
WARNING: DATA RACE
Read at 0x00c420092008 by goroutine 6:
main.main.func1()
/Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:15 +0x54
Previous write at 0x00c420092008 by main goroutine:
main.main()
/Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:12 +0x11b
Goroutine 6 (running) created at:
main.main()
/Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:13 +0xf1
==================
这个地址 0x00c420092008
是谁呢,很显然就是 i ,原因是因为在最后跳出来的时候 i 会变成10,里面的 a[i] ++
就会是a10 ,所以出错的原因就在这。
sheng$ go run -race route.go
==================
WARNING: DATA RACE
Read at 0x00c420092008 by goroutine 6:
main.main.func1()
/Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:15 +0x54
Previous write at 0x00c420092008 by main goroutine:
main.main()
/Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:12 +0x11b
Goroutine 6 (running) created at:
main.main()
/Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:13 +0xf1
==================
上面还剩一个的两个Goroutine读写的问题需要我们后面介绍的Channel来解决。
首先我们先看一张普通函数和协程的对比图
普通函数main函数和work函数都运行在一个线程里面,main函数在等work函数执行完才能执行其他的操作。可以看到普通函数 main 函数和 work 函数是单向的,但是发现协程的 main 和 work 是双向通道的,控制权可以在work也可以在main,不需要像普通函数那样等work函数执行完才交出控制权。协程中main和work可能执行在一个线程中,有可能执行在多个线程中。
上图就是Go语言的协程, 首先下面会有一个调度器,负责调度协程,有些是一个goroutine放在一个线程里面,有的是两个,有的是多个,这些我们都不需要关注。
上述仅是参考,不能保证切换,不能保证在其他的地方不切换
我们可以开很多个goroutine,goroutine和goroutine之间的双向通道就是channel。
首先我们先来介绍一下channel的用法
ch := make(chan int)
和其他类型类似,都是需要先创建声明
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
for {
num := <- ch
fmt.Println(num)
}
}()
ch <- 1
ch <- 2
time.Sleep(time.Millisecond)
}
这就是一个简单的channel示例,同时channel是一等公民,可以作为参数也可以作为返回值,那我们就用一个例子来简单的演示一下
package main
import (
"fmt"
"time"
)
func work(channels chan int, num int) {
for ch := range channels {
fmt.Println("Work ID :", num)
fmt.Println(ch)
}
}
func createWork(num int) chan<- int {
ch := make(chan int)
go work(ch, num)
return ch
}
func main() {
var channels [10]chan<- int
for i := 0; i < 10; i ++ {
channels[i] = createWork(i)
}
for i := 0; i < 10; i ++ {
channels[i] <- 'M' + i
}
time.Sleep(time.Millisecond)
}
输出结果为
Work ID : 3
80
Work ID : 0
77
Work ID : 1
78
Work ID : 6
Work ID : 9
83
Work ID : 4
Work ID : 5
82
86
81
Work ID : 8
85
Work ID : 2
79
Work ID : 7
84
结果为什么是乱序的呢?因为 fmt.Println
有I/O操作;上述例子,可以看到channel既可以作参数,也可以作为返回值。
ch := make(chan int)
ch <- 1
我们要是光发送,没有接收是不行的,程序会报错,比如上述代码运行之后
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/Users/verton/GoLangProject/src/shengguocun.com/channel/channel.go:42 +0x50
我们可以设置一个缓冲区
ch := make(chan int, 5)
ch <- 1
缓冲区大小设置为5,只要发送不超过5个都不会报错,下面我们来演示一下buffer channel的使用
func main() {
channels := make(chan int, 5)
go func() {
for ch := range channels {
fmt.Println(ch)
}
}()
channels <- 1
channels <- 2
channels <- 3
channels <- 4
channels <- 5
time.Sleep(time.Millisecond)
}
结果输出正常
1
2
3
4
5
Process finished with exit code 0
比如我们确定数据结束了,可以在最后进行close;同时只能是发送方close的
func main() {
channels := make(chan int, 5)
go func() {
for ch := range channels {
fmt.Println(ch)
}
}()
channels <- 1
channels <- 2
channels <- 3
channels <- 4
channels <- 5
close(channels)
time.Sleep(time.Millisecond)
}
直观地从输出结果来看,加不加close这两者是没有区别的。
前面的例子中我们等待任务结束是通过sleep来处理,因为打印的数据较少,1 毫秒足够;但是这种方式等待任务结束显然不是很优雅。
对于任务结束首先我们需要确定的通知外面我们打印结束了,那我们又如何通知呢?在Go语言中我们不要通过共享内存来通信,而是要通过通信来共享内存。直接用Channel就可以,下面我们来改造上面的例子
package main
import (
"fmt"
)
type worker struct {
in chan int
done chan bool
}
func work(in chan int, done chan bool, num int) {
for ch := range in {
fmt.Println("Work ID :", num)
fmt.Println(ch)
done<- true
}
}
func createWork(num int) worker {
ch := worker{
in: make(chan int),
done: make(chan bool),
}
go work(ch.in, ch.done, num)
return ch
}
func main() {
var workers [10]worker
for i := 0; i < 10; i ++ {
workers[i] = createWork(i)
}
for i := 0; i < 10; i ++ {
workers[i].in <- 'M' + i
<-workers[i].done
}
}
打印输出结果
Work ID : 0
77
Work ID : 1
78
Work ID : 2
79
Work ID : 3
80
Work ID : 4
81
Work ID : 5
82
Work ID : 6
83
Work ID : 7
84
Work ID : 8
85
Work ID : 9
86
虽然sleep部分的代码已经删除了,但是发现是顺序打印的,这显然不是我想要的结果。Go语言对等待多任务完成提供了一个库 WaitGroup,下面我们就用它继续重构上述的代码
package main
import (
"fmt"
"sync"
)
type worker struct {
in chan int
done func()
}
func work(worker worker, num int) {
for ch := range worker.in {
fmt.Println("Work ID :", num)
fmt.Println(ch)
worker.done()
}
}
func createWork(num int, wg *sync.WaitGroup) worker {
worker := worker{
in: make(chan int),
done: func() {
wg.Done() // 每个任务做完了就调用Done
},
}
go work(worker, num)
return worker
}
func main() {
var wg sync.WaitGroup
var workers [10]worker
for i := 0; i < 10; i ++ {
workers[i] = createWork(i, &wg)
}
wg.Add(10) // Add 总共有多少个任务
for i := 0; i < 10; i ++ {
workers[i].in <- 'M' + i
}
wg.Wait() // 等待所有的任务做完
}
结果输出
Work ID : 4
81
Work ID : 5
82
Work ID : 1
78
Work ID : 2
79
Work ID : 6
Work ID : 3
80
Work ID : 0
Work ID : 9
86
83
77
Work ID : 7
84
Work ID : 8
85
Process finished with exit code 0
这样相应的结果才是我们想要的。
协程交替执行,使其能顺序输出1-20的自然数
这个问题就不做演示了,留给读者自行发挥。
首先我们先来介绍一下select常规的应用场景,比如
var ch1, ch2 chan int
我们有两个channel,我们想从 ch1、ch2 里面收数据,
var ch1, ch2 chan int
data1 := <- ch1
data2 := <- ch2
谁快我就要谁,这就是我们的select
package main
import (
"fmt"
)
func main() {
var ch1, ch2 chan int
select {
case data := <- ch1:
fmt.Println("CH1 的数据:", data)
case data := <-ch2:
fmt.Println("CH2 的数据:", data)
default:
fmt.Println("没收到 CH1、CH2 的数据")
}
}
这就相当于做了一个非阻塞式的获取。下面我们就结合一个channel生成器来做一个例子演示
package main
import (
"fmt"
"time"
"math/rand"
)
func genChan() chan int {
out := make(chan int)
go func() {
i := 0
for {
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
out <- i
i ++
}
}()
return out
}
func main() {
var ch1, ch2 = genChan(), genChan()
for {
select {
case data := <- ch1:
fmt.Println("CH1 的数据:", data)
case data := <-ch2:
fmt.Println("CH2 的数据:", data)
}
}
}
输出结果(部分)
CH1 的数据: 0
CH2 的数据: 0
CH1 的数据: 1
CH2 的数据: 1
CH1 的数据: 2
CH2 的数据: 2
CH1 的数据: 3
CH2 的数据: 3
CH1 的数据: 4
CH2 的数据: 4
CH1 的数据: 5
CH2 的数据: 5
CH2 的数据: 6
CH1 的数据: 6
CH1 的数据: 7
CH1 的数据: 8
CH2 的数据: 7
CH1 的数据: 9
CH2 的数据: 8
CH1 的数据: 10
CH2 的数据: 9
CH1 的数据: 11
CH1 的数据: 12
CH1 的数据: 13
CH2 的数据: 10
CH2 的数据: 11
CH1 的数据: 14
CH2 的数据: 12
CH2 的数据: 13
CH1 的数据: 15
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
这就是select的一个应用场景,从输出结果可以看到,CH1、CH2的输出结果不一样,谁先出数据就先选择谁;两个同时出就随机的选择一个。
比如上面的这段代码我想要在10秒之后程序就终止,我该如何处理呢?我们这里需要介绍一下Go语言的 time.After
// 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
}
从源码来看,他的返回值类型是一个 <-chan Time
,那就方便很多了
package main
import (
"fmt"
"time"
)
func genChan() chan int {
out := make(chan int)
go func() {
i := 0
for {
time.Sleep(time.Second)
out <- i
i ++
}
}()
return out
}
func main() {
var ch1, ch2 = genChan(), genChan()
tm := time.After(10 * time.Second) // 加上10秒的定时
for {
select {
case data := <- ch1:
fmt.Println("CH1 的数据:", data)
case data := <-ch2:
fmt.Println("CH2 的数据:", data)
case <-tm:
return // 收到指令程序直接return
}
}
}
运行到10秒,代码自动退出。
Go语言除了CSP模型外,还是有传统同步机制的,比如互斥量 Mutex
,现在我们就用它举个例子:
用互斥量实现 atomic
package main
import (
"sync"
"time"
"fmt"
)
type atomicInt struct {
value int
lock sync.Mutex
}
func increment(a *atomicInt) {
a.lock.Lock()
defer a.lock.Unlock()
a.value ++
}
func get(a *atomicInt) int {
a.lock.Lock()
defer a.lock.Unlock()
return a.value
}
func main() {
var a atomicInt
increment(&a)
go func() {
increment(&a)
}()
time.Sleep(time.Second)
fmt.Println(get(&a))
}
结果输出
2
Process finished with exit code 0
代码写完,可以用上面介绍的race来检查一下,是否有冲突,是否安全;当然这里还是不建议自己来造这些轮子的,直接使用系统的就可以了。系统提供了 atomic.AddInt32()
等等这些原子操作。
Go语言有很多的标准库,http库是最重要的之一,它对Http的封装也是非常完善的,之前我们有演示过服务端的一些基础使用,下面我们介绍一些客户端相关的使用
package main
import (
"net/http"
"net/http/httputil"
"fmt"
)
func main() {
response, err := http.Get("https://www.shengguocun.com")
if err != nil{
panic(err)
}
defer response.Body.Close()
ss, err := httputil.DumpResponse(response, true)
if err != nil {
panic(err)
}
fmt.Printf("%s \n", ss)
}
这里就把完整的头信息以及html的部分打印出来了。比如在我们现实的情境中,我们会根据UA做一些反作弊的策略,以及是否需要重定向到 M 端等等。这里的 http.Client
就能实现。
请求头信息直接通过 request.Header.Add
添加就可以了
package main
import (
"net/http"
"net/http/httputil"
"fmt"
)
func main() {
request, err := http.NewRequest(http.MethodGet,"https://www.shengguocun.com", nil)
request.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36")
response, err := http.DefaultClient.Do(request)
if err != nil{
panic(err)
}
defer response.Body.Close()
ss, err := httputil.DumpResponse(response, true)
if err != nil {
panic(err)
}
fmt.Printf("%s \n", ss)
}
上面我们都用的是 DefaultClient ,我们也可以自己创建 client, 首先我们先看一下 Client 内部都有些什么
查看源码我们发现有一个 CheckRedirect
,我们发现这是一个检查重定向的函数。那我们就用它做一下演示
package main
import (
"net/http"
"net/http/httputil"
"fmt"
)
func main() {
request, err := http.NewRequest(http.MethodGet,"https://jim-sheng.github.io", nil)
request.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36")
client := http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
fmt.Println("重定向地址:", req)
return nil
},
}
response, err := client.Do(request)
if err != nil{
panic(err)
}
defer response.Body.Close()
ss, err := httputil.DumpResponse(response, true)
if err != nil {
panic(err)
}
fmt.Printf("%s \n", ss)
}
输出结果(部分)
重定向地址: &{GET https://www.shengguocun.com/ 0 0 map[User-Agent:[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36] Referer:[https://jim-sheng.github.io]] <nil> <nil> 0 [] false map[] map[] <nil> map[] <nil> <nil> 0xc42012c090 <nil>}
HTTP/2.0 200 OK
Accept-Ranges: bytes
Access-Control-Allow-Origin: *
我们可以看到具体的重定向的地址 https://www.shengguocun.com/ ,其它的
// Transport specifies the mechanism by which individual
// HTTP requests are made.
// If nil, DefaultTransport is used.
Transport RoundTripper
主要用于代理
// Jar specifies the cookie jar.
//
// The Jar is used to insert relevant cookies into every
// outbound Request and is updated with the cookie values
// of every inbound Response. The Jar is consulted for every
// redirect that the Client follows.
//
// If Jar is nil, cookies are only sent if they are explicitly
// set on the Request.
Jar CookieJar
主要用于模拟登录用的
// Timeout specifies a time limit for requests made by this
// Client. The timeout includes connection time, any
// redirects, and reading the response body. The timer remains
// running after Get, Head, Post, or Do return and will
// interrupt reading of the Response.Body.
//
// A Timeout of zero means no timeout.
//
// The Client cancels requests to the underlying Transport
// using the Request.Cancel mechanism. Requests passed
// to Client.Do may still set Request.Cancel; both will
// cancel the request.
//
// For compatibility, the Client will also use the deprecated
// CancelRequest method on Transport if found. New
// RoundTripper implementations should use Request.Cancel
// instead of implementing CancelRequest.
Timeout time.Duration
主要设置超时的
还是使用之前的代码
package main
import (
"net/http"
"os"
"io/ioutil"
_ "net/http/pprof"
)
type appHandler func(writer http.ResponseWriter, request *http.Request) error
func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {
err := handler(writer, request)
if err != nil {
switch {
case os.IsNotExist(err):
http.Error(writer, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
}
}
}
func main() {
http.HandleFunc("/list/",
errWrapper(func(writer http.ResponseWriter, request *http.Request) error {
path := request.URL.Path[len("/list/"):]
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
all, err := ioutil.ReadAll(file)
if err != nil {
return err
}
writer.Write(all)
return nil
}))
err := http.ListenAndServe(":2872", nil)
if err != nil {
panic(err)
}
}
还是一样的代码,只不过import多了一个 import _ "net/http/pprof"
, 为什么会多一个下划线呢?因为代码没有使用到,会报错,加一个下划线就可以了。重启代码,我们就可以访问 http://localhost:2872/debug/pprof/ 了
里面的 stacktrace 都可以查看到
我们可以查看 pprof 的源码,继续查看它的其他的使用方式
// Or to look at a 30-second CPU profile:
//
// go tool pprof http://localhost:6060/debug/pprof/profile
比如这一段,我们可以查看30秒的CPU的使用情况。可以终端敲下该命令(替换成自己的监听的端口),获取出结果后敲下 web 命令就可以看下具体的代码哪些地方需要优化。其他的使用使用方式就不一一罗列了,有兴趣可以继续查阅。
其它的标准库就不过多罗列了, https://studygolang.com/pkgdoc 上面的中文版的文档已经非常详细了 。
Go语言给我们展现了不一样的世界观,没有类、继承、多态、重载,没有构造函数,没有断言,没有try/catch等等;上面是在学习Go语言的过程中,记录下来的笔记;也可能有部分地方存在偏颇,还望指点~~~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有