跳至主要內容

新特性

程序员李某某原创Golanggo大约 9 分钟

新特性

1.7

context

提示

思考一个问题:在主线程中如何通知 goroutine 程序结束啦,不用继续执行了 之前的做法

  • 定义一个全局变量,比如var flag = false,主线程结束时改为true,goroutine循环读flag状态
  • 定义一个管道,通过 select-case 读管道
    func main() {
          stop := make(chan bool)
    
          go func() {
              for {
                  select {
                  case <-stop:    // 收到了停滞信号
                      fmt.Println("监控退出,停止了...")
                      return
                  default:
                      fmt.Println("goroutine监控中...")
                      time.Sleep(2 * time.Second)
                  }
              }
          }()
    
          time.Sleep(10 * time.Second)
          fmt.Println("可以了,通知监控停止")
          stop<- true
    
          //为了检测监控过是否停止,如果没有监控输出,就表示停止了
          time.Sleep(5 * time.Second)
      }
    

这样做的问题

  • 没有统一的规范,不好多人协作,
  • 多个goroutine通知很麻烦,尤其嵌套的情况

开发中的问题:平时在 Go 工程中开发中,几乎所有服务端(例如:HTTP Server)的默认实现,都在处理请求时新起 goroutine 进行处理。

但一开始存在一个问题,那就是当一个请求被取消或超时时,所有在该请求上工作的 goroutines 应该迅速退出,以便系统可以回收他们正在使用的任何资源。

context.Context 是一个接口,定义了4个方法

type Context interface {
    // 用于获取设置的截止时间
    //      - 返回值1:deadline 表示被取消的时间,截止时间,到了这个时间点,Context 会自动发起取消请求
    //      - 返回值2:ok 表示是否设置截止时间,如果没有设置时间,当需要取消的时候,需要调用取消函数进行取消
	Deadline() (deadline time.Time, ok bool)
    // 返回一个只读的 chan,类型为 struct{},
    // 我们在 goroutine 中,如果该方法返回的 chan 可以读取,则意味着 parent context 已经发起了取消请求,
    // 我们通过 Done 方法收到这个信号后,就应该做清理操作,然后退出 goroutine,释放资源,
    // 多次调用 Done 方法会返回同一个 Channel
	Done() <-chan struct{}
    // 返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值;
    //      - 如果 context.Context 被取消,会返回 Canceled 错误;
    //      - 如果 context.Context 超时,会返回 DeadlineExceeded 错误;
	Err() error
    // 获取该 Context 上绑定的值,是一个键值对,所以要通过一个 Key 才可以获取对应的值,
    // 这个值是线程安全的,放心传递。多次调用 Value 并传入相同的 Key 会返回相同的结果
	Value(key interface{}) interface{}
}

以上四个方法中常用的就是 Done 了,如果 Context 取消的时候,我们就可以得到一个关闭的 chan,关闭的 chan 是可以读取的,所以只要 ctx.Done() 可以读取的时候,就意味着收到 Context 取消的信号了,以下是这个方法的经典用法。

func Stream(ctx context.Context, out chan<- Value) error {
	for {
		// do something

		select {
		case <-ctx.Done():
			return ctx.Err()
		case out <- v:
		}
	}
}

内置的两个实现,我们代码中最开始都是以这两个内置的作为最顶层的 partent context,衍生出更多的子 Context

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

// 作为大部分场景的根Context
func Background() Context {
	return background
}
// 不知道具体的使用场景,用todo
func TODO() Context {
	return todo
}

他们两个本质上都是 emptyCtx 结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的 Context 怎么衍生出更多的子 Context 呢,就用到了下面几个方法

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
  • WithCancel
    • 入参是父 Context
    • 返回值是 子context用于传递,和一个取消函数
  • WithDeadline
    • 入参是父 Context,和截止时间,到时间会自动取消
    • 返回值是 子context用于传递,和一个取消函数
  • WithTimeout 和 WithDeadline 基本一致,只是时间参数的格式不同
    • WithTimeout 是超时时间context.WithTimeout(context.Background(), 500*time.Millisecond)
    • WithDeadline 是具体时间点context.WithDeadline(context.Background(), time.Now().Add(500*time.Millisecond))
  • WithValue 这个函数不会取消
    • 入参是父 Context,和绑定的k和v,都是任意类型
    • 返回值是 子context用于传递,可以通过value := context.Value(key)获取值

注意事项

不要将上下文存储在结构类型中,尽可能的作为函数第一位形参传入,并使用 ctx 变量名惯用语 函数调用链必须传播上下文,实现完整链路上的控制 不传递 nil context,不确定的 context 应当使用 TODO context 仅传递必要的值,不要让可选参数揉在一起,在业务场景上,context 传值适用于传必要的业务核心属性,例如:租户号、小程序ID 等 context 的继承和派生,保证父、子级 context 的联动

1.13

包管理

go module

提示

go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具。

要启用go module支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:offonauto,默认值是auto

  • GO111MODULE=off禁用模块支持,编译时会从GOPATHvendor文件夹中查找包
  • GO111MODULE=on启用模块支持,编译时会忽略GOPATHvendor文件夹,只根据 go.mod下载依赖
  • GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持

简单来说,设置GO111MODULE=on之后就可以使用go module了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。

使用 go module 管理依赖后会在项目根目录下生成两个文件go.modgo.sum

代理

Go1.13之后GOPROXY默认值为https://proxy.golang.org,在国内是无法访问的,所以建议配置下国内代理

go env -w GOPROXY=https://goproxy.cn,direct

命令

go mod init        ## 初始化当前文件夹, 创建go.mod文件				**
go mod tidy        ## 增加缺少的module,删除无用的module			**
go get			   ## 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)	**
go mod download    ## 同上
go mod edit        ## 编辑go.mod文件
go mod graph       ## 打印模块依赖图
go mod vendor      ## 将依赖复制到vendor下
go mod verify      ## 校验依赖
go mod why         ## 解释为什么需要依赖

go.mod

  • module 定义包名
  • go 指定go的版本
  • require用来定义依赖包及版本
  • indirect表示间接引用
  • replace替换
module github.com/xxx/ooo/...

go 1.12

require (
	github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586
	github.com/gin-gonic/gin v1.4.0
	github.com/go-sql-driver/mysql v1.4.1
	github.com/jmoiron/sqlx v1.2.0
	github.com/satori/go.uuid v1.2.0
	google.golang.org/appengine v1.6.1 // indirect
)

replace (
	golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
	golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
	golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)

go.sum go.sum 是 Go 语言项目中的摘要文件,用于记录项目的依赖项的版本和哈希值

  • 根据其中的哈希值验证依赖项的完整性,防止使用被篡改或非预期的依赖项
  • 将项目的依赖项锁定到特定的版本,以确保在构建和部署过程中使用相同的依赖版本 不要手动编辑 go.sum 文件,它应该由 Go 工具自动生成和维护

1.18

泛型 Generics

先看看没有泛型之前

  • 想搞一个遍历打印切片元素的通用函数
  • 我们期望可以有空接口切片接收这个参数,但是调用的时候发现语法不支持
    func main(){
        strs := []string{"aaa","bbb"}
        is := []int{1,2}
        fs := []float64{1,2}
    
        printSlice(strs)    // 语法错误
        printSlice(is)      // 语法错误
        printSlice(fs)      // 语法错误
    
    }
    
    func printSlice(arr []interface{}){
        for _,a := range arr {
            fmt.Println(a)
        }
    }
    
  • 只能用空接口接收,然后进行断言
      func printSlice(arr interface{}) {
          strings, ok := arr.([]string)
          if ok {
              for _, a := range strings {
                  fmt.Println(a)
              }
          }
          // ... 其他类型的都需要断言
      }
    
  • 每个类型都需要断言或者用switch-case是不是很烦 泛型示例
func printSlice[T string | int](arr []T) {
    for _, a := range arr {
        fmt.Println(a)
    }
}
  • 是不是清爽多了
  • 每个类型都需要写一遍,还是很麻烦,能不能再简化,这就有了内置的泛型类型
    • any --- 相当于空接口interface{}
    • comparable --- 任意可比较的类型: 布尔值、数字、字符串、指针、通道、可比较类型的数组、字段都是可比较类型的结构体
      func printSlice[T comparable](arr []T) { }
    

提示

泛型可以用反射+接口替代,但是反射是非常重的,慢很多导致性能下降,用起来还麻烦,所以引入泛型是非常有必要的

泛型类型

开发中也会需要结构相同,但是元素类型不同的切片类似情况,需要根据每个元素类型定义很多重复的切片,很麻烦,比如

type s1 []int
type s2 []float32
type s3 []float64
type s4 []string

用泛型类型就简单多了

type s5[T int|string|float32|float64] []T
var s s5[int] = []int{1,2,3}

type myMap[K string,V any] map[K]V
var m myMap[string,any] = map[string]any{
    "name":"aaa",
    "age":18
}
fmt.Println(m)

再看一个结构体

type MyStruct[T int|string] struct {
    ID T
    Name string
}

起别名时用到泛型要注意

// 给int类型起了泛型别名
type MyInt[T int|string] int
var m1 MyInt[int] = 123         // 没问题
var m1 MyInt[string] = 123      // 没问题
var m1 MyInt[string] = "123"    // 编译报错

很奇葩对不对,避免这样使用就行

实例化的简写

type MySlice[T int|float64] []T
var m MySlice[int] = []int{1,2,3}
// 可简写为
m := MySlice[int]{1, 2, 3}

泛型函数

func sum[T int | float64](m []T) T {
	var s T
	for _, v := range m {
		s += v
	}
	return s
}

// 调用
sum[int]([]int{1,2,3})
// 简写 --- 省略函数的泛型
sum([]int{1, 2, 3})

泛型方法

type MySlice[T int|float64] []T

// 通用的切片求和
func (m MySlice[T])sum() T {
    var s T
    for _,v := range m{
        s+=v
    }
    return s
}
// 调用
m := MySlice[int]{1, 2, 3}
m.sum()

泛型约束类型

  • 目前内置的两个泛型约束类型:any、comparable
  • 自定义泛型约束也很简单,像定义接口一样
type myfloat interface{
    float32|float64
}

// 泛型函数 -- 这里为了简单给返回值起名了
func add[T float](arr []T) (s T) {
	for _, v := range arr { s += v }
	return
}

// 调用
add([]float64{1, 2, 3})
  • 基本类型起了别名的情况
    • type myfloat32 float32 这里的 myfloat32 就不不配 myfloat 这个约束
    • 要想适用,需要稍加改变
      type myfloat interface{
          ~float32 | float64
      }
      
    • 这个~就可以适配对应衍生的别名

命令相关

go get

go get命令已弃用,并且被go install替代

#old command
go get example.com/cmd

#new command
go install exmple.com/cmd@latest

go work

新增work工作空间,类似与go.mod,不过这个工作空间可以用于切换本地与远程的调试。特别是引用一些本地包的时候就可以使用。例如我们引用了utils内的方法,但是要修改utils的方法以后再使用utils,原来需要在go.mod内使用replace进行调试,调试完毕以后还要修改回去。现在只需要使用go work即可。

# 初始化工作空间
go work init xxx
# 将单个module引入工作空间
go work use xxx
# 将所有的module引入工作空间
go work use -r
# 同步工作空间的module
go work sync

模糊测试 Fuzzing

...

netip包

...

上次编辑于:
贡献者: ext.liyuanhao3