1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】
作者:mmseoamin日期:2023-12-14

Go语言开发环境搭建【Win、Linux、Mac】

1 SDK下载

  • 官网地址:golang.org,因为一些原因国内可能无法访问。可以使用下面第二个链接。
  • 国内地址访问:https://golang.google.cn/dl或者https://www.golangtc.com/download

1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第1张

根据自己操作系统版本,下载安装即可,目录尽量选择全英文且没有空格和其他其他特殊字符。

2 环境变量配置[GOPATH、GOROOT]

2.1 Windows下

GOPATH:即默认的workspace路径,在未指定项目路径时使用;

GOROOT:Golang的安装路径

讲解:

1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第2张

  • 进入环境变量配置:此电脑-属性-关于-高级系统设置-环境变量

    1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第3张

    ①配置GOROOT

    新增系统环境变量,变量名为GOROOT,值为:go的安装目录

    1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第4张

    ②在Path中配置,go的路径,引用上面配置的GOROOT

    系统变量-Path-新增-新建的值填写为%GOROOT%\bin

    1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第5张

    ③配置GOPATH(以后Go项目存放的路径位置)

    1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第6张

    2.2 linux下

    ①确认linux版本

    uname -a
    

    1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第7张

    32位系统下载:gox.x.x.linux-386.tar.gz

    64位系统下载:gox.x.x.linux-amd64.tar.gz

    安装路径不要有中文或特殊符号、空格等

    ②安装目录建议放在/opt下

    通过xftp或其他软件将压缩包上传到/opt目录下

    #进入对应目录并解压
    cd /opt
    tar -zxvf go1.9.2.linux-amd64.tar.gz
    

    ③配置环境变量

    1. 以root权限编辑/etc/profile文件
    # 输入下面命令回车并输入密码后进入root权限【linux输入密码默认不显示,直接输入即可】
    su root
    

    1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第8张

    1. 使命令生效
    # 刷新环境变量
    source /etc/profile
    # 或者注销后重新登录
    

    1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第9张

    Mac系统的上配置Go的开发环境类似于linux

    3 检测

    不论在windows还是linux下,直接打开终端,输入go version即可:

    • windows:

      1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第10张

    • linux:

      1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第9张

      4 开发简单go程序

      4.1 hello.go程序入门

      ①文件目录结构

      1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第12张

      ②开发工具:选择Goland或者VSCode等

      此处以VSCode演示

      //每个go文件都需要归属于一个包
      package main
      //下面用到了fmt的Println函数,因此需要导包
      import "fmt"
      func main(){
      	fmt.Println("hello,go")
      }
      

      运行:

      //方式一:将hello.go编译为hello.exe文件,然后再直接运行hello.exe
      go build hello.go
      .\hello.exe
      //方式二:直接以脚本方式运行(底层还是编译过了)
      go run hello.go
      

      1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第13张

      拓展:指定编译后的文件名称

      //将hello.go编译生成myhello.exe文件
      go build -o myhello.exe hello.go
      

      4.2 go的执行流程分析

      • 对源码编译后执行

        1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第14张

      • 直接对源码执行

        1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第15张

        两种执行方式的区别?

        1. 如果我们先编译生成了可执行文件,那么我们可以将该可执行文件拷贝到没有go开发环境的机器上,仍然可以运行【因为已经编译为了二进制,例如:windows上会直接编译生成.exe文件】
        2. 如果我们是直接go run go的源代码,那么如果要在另一个机器上运行,也需要go开发环境,否则无法执行
        3. 在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件会变大很多

        5 Go语言开发的注意事项

        ①注意事项

        1. 源文件以.go为扩展名
        2. Go应用程序入口是main()函数
        3. Go语言严格区分大小写
        4. 每个语句不需要加分号【编译器会自动帮我们添加】,体现了go的简洁性
        5. 不能把多条语句写在同一行,否则会报错【go的编译器是一行一行编译的】

          1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,,,第16张

        6. 定义的变量或import的包没有使用到,代码编译不通过

          1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第17张

        7. 一个包下面只能有一个main函数入口,就类似于Java的包下面类名不能重复。如果有多个main,可以分开在不同的包下

        1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第18张

        ②go的转义字符及注释

        转义字符:

        • \t:制表符
        • \n:换行符
        • \:一个\,通常用于文件分隔符
        • \r:回车符

          1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第19张

        • \":代表一个"

        注释:

        被注释的内容不会被go编译器所编译

        1. 行注释://
        //fmt.Println("jack")
        
        1. 块注释:/**/
        /*
        fmt.Println("tom")
        fmt.Println("jucy)
        */
        

        注意:块注释不能嵌套

        ③编程风格及API地址

        • 编程风格
          1. 官方推荐使用行注释
          2. 代码中需要有正确的缩进和空白【可以使用gofmt进行格式化】

            1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第20张

          3. 运算符两边加空格
          4. 花括号只能在行尾
          //花括号只有这一种写法,go开发者认为应当统一风格
          func main(){
          }
          
          1. 一行最多不能超过80个字符

            1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第21张

          • API地址

            官网:https://golang.org

            1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第22张

            1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第23张

            • Golang 官方标准库 API 文档, https://golang.org/pkg 可以查看 Golang 所有包下的函数和使用

            Golang中导包原理:

            1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第24张

            • 离线版(大家可以自行百度下载一个API)

            6 go modules管理

            6.1 概念

            Go modules是Go语言的依赖解决方案,发布于Go1.11

            • 主要用于解决go的依赖管理问题
            • 淘汰之前的GOPATH使用模式

            6.2 go mod 环境变量

            • GO111MODULE:是否开启go modules模式
            • GOPROXY:项目第三方库的下载源地址,建议设置为国内的镜像地址
              • 阿里云:https://mirrors.aliyun.com/goproxy/
              • 七牛云:https://goproxy.cn,direct
              • direct参数用于表明Go回溯到模块版本的源地址去抓取(如:github等,比如,我们配置的镜像地址拉取不到了,那么go就会从github上去拉取)
              • GOSUMDB:用来校验拉取的第三方库是否是完整的,默认也是国外的网站,如果我们设置了GOPROXY,这个就不用设置了
              • GOPROXY:通过设置GOPRIVATE即可
              • GOPRIVATE:
                1. go env -w GOPRIVATE=“git.example.com(github.com/aceId/zinx)”

                  表示git.example.com和github.com/aceId/zinx是私有仓库,不会通过GOPROXY下载和校验

                  2.go env -w GOPRIVATE=".example.com"表示所有模块路径为example.com的子域名,比如:git.example.com或者hello.example.com都不进行GOPROXY下载和校验

            通过go env来查看环境变量

            1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第25张

            注意:

            go env -w 和 set GOPROXY="xxx" 命令都可以设置 GOPROXY,但是它们的区别在于:

            1. go env -w 是一个永久生效的命令,它会将设置写入到环境变量中,这样在以后的使用中都会使用这个设置。而 set GOPROXY="xxx" 只是一个临时生效的命令,它只会在当前终端或者会话中生效。
            2. go env -w 可以设置多个环境变量,而 set GOPROXY="xxx" 只能设置一个环境变量。

            6.3 go mod 命令

            ①开启go modules模式(建议go v1.11之后,都设置为on)

            ②设置proxy代理

            //1. 初始化,生成.mod文件
            go mod init
            //2. 下载go.mod文件中指明的所有依赖
            go mod download
            //3. 整理现有依赖
            go mod tidy
            //4. 查看现有依赖结构
            go mod graph
            //5. 编辑go.mod文件
            go mod edit
            //6. 导出项目所有的依赖到vendor目录
            go mod vendor
            //7. 校验一个模块是否被篡改过
            go mod verify
            //8. 查看为什么需要依赖某模块
            go mod why
            

            6.3 其他命令

            /*
            -u :upgrade 
            如果不加这个 -u 标记,执行 go get 一个已有的代码包,会发现命令什么都不执行。
            只有加了 -u 标记,命令会去执行 git pull 命令拉取最新的代码包的最新版本,
            下载并安装。
            */
            go get -u 
            

            6.4 go module 之前的做法[GO_PATH/src下放依赖]

            • go是在1.11版本开始支持module的,因此在之前的做法是下载好依赖之后会将依赖下载到PATH的src下面,同时目录结构与go get的目录结构对应,比如:go get github.com/ying32/govcl,则依赖下载完成后会放在%GO_PATH%/src下创建%GO_PATH%/src/github.com/ying32/govcl。
            • 同时在go之前的版本是没有GOPROXY的,因此通过 go get 来获取github上的资源会很慢,所以我们可以手动到github上下载源码,然后自己手动创建对应目录
            • 在goland中如果是go module之前也需要关闭goland集成的go module(settings-go-go modules)
            • 下载指定版本依赖,可以根据git clone 到对应git仓库,然后通过git checkout切换到指定版本
            # eef842397966为go.mod文件中版本的最后一部分
            git checkout eef842397966
            

            6.5 go实现生产者消费者模型

            package main
            import "fmt"
            /*
               golang实现生产者消费者模型
            */
            func main() {
            	ch := make(chan int, 20)             //存放生产者生产好的数据
            	numProduc := 3                       //生产者数量
            	numConsum := 5                       //以5个消费者数量为例
            	donePr := make(chan bool, numProduc) //donePr的容量:生产者数量
            	doneCo := make(chan bool, numConsum) //doneCo的容量:消费者数量
            	//开启多个生产者
            	for i := 1; i <= numProduc; i++ {
            		go Producer(i, ch, donePr)
            	}
            	//开启多个消费者
            	for j := 1; j <= numConsum; j++ {
            		go Consumer(j, ch, doneCo)
            	}
            	for ii := 0; ii < numProduc; ii++ {
            		<-donePr //done通道内的数据全部写出,代表所有生产结束,ch通道可以关闭
            	}
            	close(ch)
            	for jj := 0; jj < numConsum; jj++ {
            		<-doneCo //把done通道内的数据写出,起到一个wait()的作用,等待子进程全部结束之后,主进程便可退出
            	}
            }
            //生产者模型
            func Producer(id int, ch chan int, done chan bool) {
            	for i := 0; i <= 10; i++ {
            		ch <- i
            		fmt.Printf("%v号生产者,产生数据:%v \n", id, i)
            	}
            	done <- true
            	//在开启多个goroutine进行生产时,不能再函数内部将通道关闭
            	//此处往done通道内写数据,目的是为了表示goroutine生产完毕
            	//当在主函数中能取出相应数量(numProduc)的元素时,即代表所有生产结束,通道可关闭
            }
            //消费者
            func Consumer(id int, ch chan int, done chan bool) {
            	for v := range ch {
            		fmt.Printf("%v号消费者,消费数据:%v \n", id, v)
            	}
            	done <- true
            	//函数主体结束时,往通道里写一个数据
            	//当开启多个goroutine时,每个协程结束都往通道写一个数据,相当于告诉外界,我消费结束了
            	//在主函数末尾,依次取出该通道内数据,当全部取出,就代表所有子goroutine结束,主程序也可以退出
            	//相当于起到同步等待的作用
            }
            

            运行结果:

            1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第26张

            7 bug合集

            7.1 导包问题

            ①missing go.sum entry for module providing package <package_name>

            go在使用第三方的时候导入包报错:missing go.sum entry for module providing package <package_name>

            解决办法一:

            当代码中使用了第三方库,但是go.mod中并没有跟着更新的时候,如果直接run或者build就会报错

            # 执行下面命令,删除不需要的依赖包,下载新的依赖包,更新go.sum
            go mod tidy
            

            如果执行上面命令报错:go mod tidy: go.mod file indicates go 1.19, but maximum supported version is 1.17;

            • 原因:go.mod文件中版本与你实际本地go版本不一致
            • 解决:将go.mod中的go版本改为和你本地一致的就可以了

            解决方法二:

            go build -mod = mod
            
            ②unrecognized import path "golang.org/x/sys/windows"问题

            执行语句:go get github.com/StackExchange/wmi,报go get github.com/StackExchange/wmi

            • 解决办法:

              1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第27张

              ③ module declares its xxx but was required as xxx

              在导包时,执行 go get “github.com/Shopify/sarama”

              • 发现报错:

                go: github.com/Shopify/sarama@v1.40.1: parsing go.mod:

                module declares its path as: github.com/IBM/sarama

                but was required as: github.com/Shopify/sarama

                1. 查看go.mod 是否声明错误
                2. 查看底层的模块真正叫什么(goland的scope范围下)

                  1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第28张

                  综上:我应该执行go get github.com/IBM/sarama命令,最后成功导入

              7.2 文件问题

              ①双击exe文件闪退

              可能是本地系统的go版本与IDE的go版本不兼容,比如:goland上是go1.19,本地windows上是go1.10,修改之后重新build即可

              ②写入文件报错:Access is denied.

              当我使用下面的代码进行文件时,报错:Access is denied.

              file, err := os.Open(path)
              defer file.Close()
              _, err = file.WriteString(data)
              

              解决办法及原因:

              1. 权限不够(使用下面方式打开):

                os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)

                例如:os.OpenFile(path, os.O_WRONLY, os.ModePerm)

              2. 以管理员身份运行,linux则以root方式并赋予root对应文件的读写权限
              3. 查看文件句柄是否被占用

              7.3 代码问题

              ①序列化失败

              自定义结构体序列化失败:

              1. 检查是否添加上`json:“name”`参数
              2. 检查字段名是否大写开头,如:Name
              type LocalIp struct {
              	Name string `json:"name"`
              	Ip   string `json:"ip"`
              }
              
              ②以相对路径打开文件失败

              我在项目目录下创建子目录,然后通过子目录来运行代码报错:open file err= open …/…/test.zip: The system cannot find the file specified.

              我们本地通过goland运行go代码的时候,go默认是会以当前项目的根目录去找文件,因此尽量使用绝对路径或者在相对路径的前面添加上自己的目录名

              ③协程无法获取正确的值

              在for循环中开启协程的时候发现总是打印同一个值

              package main
              import (
              	"fmt"
              	"sync"
              )
              var (
              	wg = new(sync.WaitGroup)
              )
              func main() {
              	keys := []string{"key1", "key2", "key3", "key4", "key5"}
              	for _, k := range keys {
              		wg.Add(1)
              		go func() {
              			printKey(k)
              		}()
              	}
              	wg.Wait()
              }
              func printKey(key string) {
              	defer wg.Done()
              	fmt.Println(key)
              }
              

              原因:没有将k的值,正确传给goroutine

              正确做法:

              通过waitGroup进行处理,同时直接将k通过形参传递给协程【wg如果定义为全局变量,则无法保证顺序】

              func main() {
              	keys := []string{"key1", "key2", "key3", "key4", "key5"}
              	var wg sync.WaitGroup
              	for _, k := range keys {
              		wg.Add(1)
              		go func(key string) {
              			defer wg.Done()
              			printKey(key)
              		}(k)
              	}
              	wg.Wait()
              }
              func printKey(key string) {
              	fmt.Println(key)
              }
              

              8 其他tips

              8.1 将源文件编码为linux可执行文件

              下面命令均在windows上执行

              # 设置环境(仅对当前终端有效)
              set GOARCH=amd64
              set GOOS=linux
              go build main.go
              # 打包后会生成一个main程序,将此程序拷贝至linux服务器,两种方式启动:
              1、在当前会话执行
              ./main
              2、后台启动
              setsid ./main
              # 或则直接组合起来一行命令搞定
              GOOS=linux GOARCH=amd64 go build main.go
              

              拓展 - 编译为windows上可执行文件命令:

              #  编码为windows上可以执行文件,运行之后会直接生成.exe文件
              go build main.go
              

              go env -w 和 set GOPROXY=“xxx” 命令都可以设置 GOPROXY,但是它们的区别在于:

              1. go env -w 是一个永久生效的命令,它会将设置写入到环境变量中,这样在以后的使用中都会使用这个设置。而 set GOPROXY=“xxx” 只是一个临时生效的命令,它只会在当前终端或者会话中生效。
              2. go env -w 可以设置多个环境变量,而 set GOPROXY=“xxx” 只能设置一个环境变量。

              go env xxx:查看xxx的环境变量值(区分大小写)

              9 go中Context用法

              9.1 概念

              context是在go1.17版本出现的

              • context可以用来在goroutine之间传递上下文信息,相同的context可以传递给运行在不同goroutine中的函数,上下文对于多个goroutine同时使用是安全的,context包定义了上下文类型,可以使用background、TODO创建一个上下文,在函数调用链之间传播context,也可以使用WithDeadline、WithTimeout、WithCancel 或 WithValue 创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:context的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期。
              • 目前我们常用的一些库都是支持context的,例如gin、database/sql等库都是支持context的,这样更方便我们做并发控制了,只要在服务器入口创建一个context上下文,不断透传下去即可。

              9.2 使用

              context包主要提供了两种方式创建context:

              • context.Backgroud()
              • context.TODO()

                这两个函数其实只是互为别名,没有差别,官方给的定义是:

                1. context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。
                2. context.TODO 应该只在不确定应该使用哪种上下文时使用;

                  所以在大多数情况下,我们都使用context.Background作为起始的上下文向下传递。

                上面的两种方式是创建根context,不具备任何功能,具体实践还是要依靠context包提供的With系列函数来进行派生:

                func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
                func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
                func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
                func WithValue(parent Context, key, val interface{}) Context
                

                1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】,在这里插入图片描述,第29张

                基于一个父Context可以随意衍生,其实这就是一个Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,每个子节点都依赖于其父节点,例如上图,我们可以基于Context.Background衍生出四个子context:ctx1.0-cancel、ctx2.0-deadline、ctx3.0-timeout、ctx4.0-withvalue,这四个子context还可以作为父context继续向下衍生,即使其中ctx1.0-cancel 节点取消了,也不影响其他三个父节点分支。

                9.2.1 WithValue(携带数据:trace_id)
                ①概念及使用

                我们日常在业务开发中都希望能有一个trace_id能串联所有的日志,这就需要我们打印日志时能够获取到这个trace_id,在python中我们可以用gevent.local来传递,在java中我们可以用ThreadLocal来传递,在Go语言中我们就可以使用Context来传递,通过使用WithValue来创建一个携带trace_id的context,然后不断透传下去,打印日志时输出即可,来看使用例子:

                const (
                    KEY = "trace_id"
                )
                func NewRequestID() string {
                    return strings.Replace(uuid.New().String(), "-", "", -1)
                }
                func NewContextWithTraceID() context.Context {
                    ctx := context.WithValue(context.Background(), KEY,NewRequestID())
                    return ctx
                }
                func PrintLog(ctx context.Context, message string)  {
                    fmt.Printf("%s|info|trace_id=%s|%s",time.Now().Format("2006-01-02 15:04:05") , GetContextValue(ctx, KEY), message)
                }
                func GetContextValue(ctx context.Context,k string)  string{
                    v, ok := ctx.Value(k).(string)
                    if !ok{
                        return ""
                    }
                    return v
                }
                func ProcessEnter(ctx context.Context) {
                    PrintLog(ctx, "Golang梦工厂")
                }
                func main()  {
                    ProcessEnter(NewContextWithTraceID())
                }
                

                结果:

                2021-10-31 15:13:25|info|trace_id=7572e295351e478e91b1ba0fc37886c0|Golang梦工厂
                Process finished with the exit code 0
                

                我们基于context.Background创建一个携带trace_id的ctx,然后通过context树一起传递,从中派生的任何context都会获取此值,我们最后打印日志的时候就可以从ctx中取值输出到日志中。目前一些RPC框架都是支持了Context,所以trace_id的向下传递就更方便了。

                ②注意事项

                在使用withVaule时要注意四个事项:

                1. 不建议使用context值传递关键参数,关键参数应该显示的声明出来,不应该隐式处理,context中最好是携带签名、trace_id这类值。
                2. 因为携带value也是key、value的形式,为了避免context因多个包同时使用context而带来冲突,key建议采用内置类型。
                3. 上面的例子我们获取trace_id是直接从当前ctx获取的,实际我们也可以获取父context中的value,在获取键值对时,我们先从当前context中查找,没有找到会在从父context中查找该键对应的值直到在某个父context中返回 nil 或者查找到对应的值。
                4. context传递的数据中key、value都是interface类型,这种类型编译期无法确定类型,所以不是很安全,所以在类型断言时别忘了保证程序的健壮性。
                9.2.2 WithTimeout\WithDeadline(超时控制)

                通常健壮的程序都是要设置超时时间的,避免因为服务端长时间响应消耗资源,所以一些web框架或rpc框架都会采用withTimeout或者withDeadline来做超时控制,当一次请求到达我们设置的超时时间,就会及时取消,不在往下执行。withTimeout和withDeadline作用是一样的,就是传递的时间参数不同而已,他们都会通过传入的时间来自动取消Context,这里要注意的是他们都会返回一个cancelFunc方法,通过调用这个方法可以达到提前进行取消,不过在使用的过程还是建议在自动取消后也调用cancelFunc去停止定时减少不必要的资源浪费。

                withTimeout、WithDeadline不同在于WithTimeout将持续时间作为参数输入而不是时间对象,这两个方法使用哪个都是一样的,看业务场景和个人习惯了,因为本质withTimout内部也是调用的WithDeadline。

                现在我们就举个例子来试用一下超时控制,现在我们就模拟一个请求写两个例子:

                1. 达到超时时间就终止执行:
                func main()  {
                    HttpHandler()
                }
                func NewContextWithTimeout() (context.Context,context.CancelFunc) {
                    return context.WithTimeout(context.Background(), 3 * time.Second)
                }
                func HttpHandler()  {
                    ctx, cancel := NewContextWithTimeout()
                    defer cancel()
                    deal(ctx)
                }
                func deal(ctx context.Context)  {
                    for i:=0; i< 10; i++ {
                        time.Sleep(1*time.Second)
                        select {
                        case <- ctx.Done():
                            fmt.Println(ctx.Err())
                            return
                        default:
                            fmt.Printf("deal time is %d\n", i)
                        }
                    }
                }
                

                输出结果:

                deal time is 0
                deal time is 1
                deal time is 2
                context deadline exceeded
                /*
                deal time is 0
                deal time is 1
                context deadline exceeded
                */
                
                1. 没有达到超时时间终止接下来的执行
                func main()  {
                    HttpHandler1()
                }
                func NewContextWithTimeout1() (context.Context,context.CancelFunc) {
                    return context.WithTimeout(context.Background(), 3 * time.Second)
                }
                func HttpHandler1()  {
                    ctx, cancel := NewContextWithTimeout1()
                    defer cancel()
                    deal1(ctx, cancel)
                }
                func deal1(ctx context.Context, cancel context.CancelFunc)  {
                    for i:=0; i< 10; i++ {
                        time.Sleep(1*time.Second)
                        select {
                        case <- ctx.Done():
                            fmt.Println(ctx.Err())
                            return
                        default:
                            fmt.Printf("deal time is %d\n", i)
                            cancel()
                        }
                    }
                }
                

                结果:

                deal time is 0
                context canceled
                

                使用起来还是比较容易的,既可以超时自动取消,又可以手动控制取消。这里大家要记的一个坑,就是我们往从请求入口透传的调用链路中的context是携带超时时间的,如果我们想在其中单独开一个goroutine去处理其他的事情并且不会随着请求结束后而被取消的话,那么传递的context要基于context.Background或者context.TODO重新衍生一个传递,否决就会和预期不符合了,可以看一下我之前的一篇踩坑文章:context使用不当引发的一个bug。

                9.2.3 WithCancel取消控制

                日常业务开发中我们往往为了完成一个复杂的需求会开多个gouroutine去做一些事情,这就导致我们会在一次请求中开了多个goroutine确无法控制他们,这时我们就可以使用withCancel来衍生一个context传递到不同的goroutine中,当我想让这些goroutine停止运行,就可以调用cancel来进行取消。

                func main()  {
                    ctx,cancel := context.WithCancel(context.Background())
                    go Speak(ctx)
                    time.Sleep(10*time.Second)
                    cancel()
                    time.Sleep(1*time.Second)
                }
                func Speak(ctx context.Context)  {
                    for range time.Tick(time.Second){
                        select {
                        case <- ctx.Done():
                            fmt.Println("我要闭嘴了")
                            return
                        default:
                            fmt.Println("balabalabalabala")
                        }
                    }
                }
                

                运行结果:

                balabalabalabala
                ....省略
                balabalabalabala
                我要闭嘴了
                

                我们使用withCancel创建一个基于Background的ctx,然后启动一个讲话程序,每隔1s说一话,main函数在10s后执行cancel,那么speak检测到取消信号就会退出。

                参考:

                https://blog.csdn.net/Mr_XiMu/article/details/124671852

                https://segmentfault.com/a/1190000040917752