文章

go1.18新特性

golang1.18带来了泛型,一起看看都有哪些新变化

语法层新特性

泛型

泛型是go1.18中最重大的语法层特性,且是完全前向兼容的。这应该是go近些年来最重磅的一次语法更新了,一起看看细节。

  • 函数(function)和类型声明(type declarations)支持接受类型参数(type parameters)
1
2
3
4
5
6
7
8
9
10
11
12
13
func min[T constraints.Ordered](x, y T) T {
        if x < y {
                return x
        }
        return y
}

type Pair[A, B any] struct {
        a A
        b B
}
func (p Pair[A, B]) Swap() Pair[B, A]  { return Pair[B, A]{p.b, p.a} }
func (p Pair[First, _]) First() First  { return p.a }
  • 参数化的函数和类型可以通过在其后添加参数类型的列表、形如:[参数类型1,参数类型2,…]来实例化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    // 实测调用函数不需要显示定义形参的类型,如果定义了则必须和实参类型保持一致
    var minVal = min[int](1,2) 
    // 实例化一个参数化的结构体,必须要显示定义形参的类型
    // 否则会报错:./pkg.go:61:11: cannot use generic type Pair[A, B any] without instantiation
    var pr = Pair[int, string]{
      a: 1,
      b: "1",
    }
    // 实践得出:
    // 函数中定义的多个形参顺序没有限制,定义成MinFunc[ E any, S ~[]E]] 一样可以正常编译运行
    // 实例化调用的时候,形参类型必须从第一个开始顺序定义,且可以省略后面的形参
    func MinFunc[S ~[]E, E any](s S, less func(a,b E)bool)(e E, b bool) {
      if len(s) == 0 {
          return e, false
      }
      var min = s[0]
      for i := 1; i< len(s); i++  {
          if less(min, s[i]) {
              min = s[i]
          }
      }
      return min, true
    }
    // 上面的范型函数可以这样调用
    min2, has2 := MinFunc([]MySt{
      {
          Age: 1,
      },
      ...
    }, func(min, cur MySt) bool {
      return min.Age >cur.Age
    })
    // 也可以这样
    min2, has2 := MinFunc[[]MySt](xxx)
    // 同样可以这样
    min2, has2 := MinFunc[[]MySt,MySt](xxx)
    // 但不能这样
    min2, has2 := MinFunc[MySt](xxx) // illegal
    
  • 新的运算符:~
  • 接口类型

    接口类型的定义发生改变 go1.17及之前: 接口类型是定义了接口可以调用的方法的集合 go1.18: 接口类型定义了类型的集合:(既定义了types集合也定义了methods集合)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
1. 基本接口类型(basic interfaces)
interface {
        Read([]byte) (int, error)
        Write([]byte) (int, error)
        Close() error
}
2. 复合接口类型(Embedded interfaces)
type Reader interface {
        Read(p []byte) (n int, err error)
        Close() error
}

type Writer interface {
        Write(p []byte) (n int, err error)
        Close() error
}

// ReadWriter's methods are Read, Write, and Close.
type ReadWriter interface { 
        Reader  // includes methods of Reader in ReadWriter's method set
        Writer  // includes methods of Writer in ReadWriter's method set
}
3. 范型接口类型(General interfaces)
更一般的组成一个接口类型的类型可能是任意类型T或者底层类型是T的T或者类型的联合体t1|t2|..tn
以及方法的定义这些类型组成了对接口类型的精确定义

- 空接口的类型集合等价于所有非接口类型的集合
- 非空接口的类型集等价于其接口类型组成元素的交集
- 方法定义的集合等价于所有包含这些方法的类型组成的集合
- 非接口类型组成的集合仅代表了这些类型的集合
- T代表了底层类型是T的类型的集合
- 类型联合体t1|t2|..tn是这些类型的并集

T的使用
T不能是接口类型T的底层类型必须是T
type MyInt int

interface {
        ~[]byte  // the underlying type of []byte is itself
        ~MyInt   // illegal: the underlying type of MyInt is not MyInt
        ~error   // illegal: error is an interface
}

联合类型
限制
- 不能是参数类型
- 非接口类型必须不相交
- 不能是comparable预定义标识符
- 不能是定义方法的接口类型
- 不能是包含定义方法/comarable的接口类型

type Floats interface {
        ~float32 | ~float64
}

interface {
        P                 // illegal: the term P is a type parameter
        int | P           // illegal: the term P is a type parameter
        ~int | MyInt      // illegal: the type sets for ~int and MyInt are not disjoint (~int includes MyInt)
        float32 | Floats  // overlapping type sets but Floats is an interface
}

非基本接口类型只能用作类型限定type constraints)、或者作为组成类型限定的元素不能作为
变量的类型(定义变量)或者组成非接口类型

var x Floats                     // illegal: Floats is not a basic interface

var x interface{} = Floats(nil)  // illegal

type Floatish struct {
        f Floats                 // illegal
}

接口类型不能嵌套自身
// illegal: Bad cannot embed itself
type Bad interface {
        Bad
}

// illegal: Bad1 cannot embed itself using Bad2
type Bad1 interface {
        Bad2
}
type Bad2 interface {
        Bad1
}

// illegal: Bad3 cannot embed a union containing Bad3
type Bad3 interface {
        ~int | ~string | Bad3
}

实现接口
 1. 非接口类型是接口定义类型集合的一种
 2. 接口类型是接口定义类型集合的一部分
  • 预定义标识符 any,是interface{}的别名,更推荐使用any。
  • 预定义标识符comparable 是一种接口类型,可以使用 == 或者 != ,只能用作类型限定。

扩展

golang.org/x/exp/constraints 一些有用的类型限定 golang.org/x/exp/slices 列表类型 golang.org/x/exp/maps map类型

当前范型实现的限制

  • 不能处理在范型方法和范型函数内部定义的类型 go1.19TODO
  • real, imag, 和 complex.内建函数不能作为参数类型 go1.19TODO
  • 调用类型参数P的方法m,m必须在P的接口中显示声明。即使P事实上都实现了m. go1.19TODO
  • 结构体不能有匿名的参数类型或其指针。接口中不能包含参数类型。未来不确定是否支持

Bug修复

  1. 变量未使用编译器却没有报错。当一个变量在函数里设置了值但没有使用的时候不会显示编译错误。 ```go func main(){ p := true// 会报错 p = true }

func main(){ p := true // 不会报错 func() { p = true } }

1
2
3
4
5
2. 常量溢出错误编译器没有报错
```go
func main() {
    fmt.Println('1' << 32) // 不报错
}

修复这些bug可以提前让开发者提前发现错误

平台

  • 新环境变量GOAMD64 go1.17之前,支持所有的64位x86处理器 go1.18分成四个架构级别
级别含义
v1(default)默认级别,所有的64位x86处理器处理器的概念实际指的是整个系统
v2v1之上,并支持这些指令的处理器:CMPXCHG16B, LAHF, SAHF, POPCNT, SSE3, SSE4.1, SSE4.2, SSSE3.
v3v2之上,并支持指令AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE, OSXSAVE
v4v3之上,并支持AVX512F, AVX512BW, AVX512CD, AVX512DQ, AVX512VL.AVX512F指令现在并不会生成

go编译器会依赖这个环境变量(也就是cpu架构)生成性能更高的代码。(当然并非所有情况都会更好)

  • RISC-V linux上的64位risc-v平台支持两种新的构建模式:c-archive 和 c-shared 文档
  • linux版本支持 最低2.6.32版本
  • windows所有版本都支持了非合作抢占
  • IOS平台 最低支持ios12
  • FreeBSD go1.18是FreeBSD 11.x的最后一个支持版本

工具

Fuzzing模糊测试 https://go.dev/doc/fuzz/

Fuzzing,又叫fuzz testing,中文叫做模糊测试或随机测试。其本质上是一种自动化测试技术,更具体一点,它是一种基于随机输入的自动化测试技术,常被用于发现处理用户输入的代码中存在的bug和问题。

在具体实现上,Fuzzing不需要像单元测试那样使用预先定义好的数据集作为程序输入,而是会通过数据构造引擎自行构造或基于开发人员提供的初始数据构造一些随机数据,并作为输入提供给我们的程序,然后监测程序是否出现panic、断言失败、无限循环等。这些构造出来的随机数据被称为语料(corpus)。另外Fuzz testing不是一次性执行的测试,如果不限制执行次数和执行时间,Fuzz testing会一直执行下去,因此它也是一种持续测试的技术。

  • FuzzXxx(f *testing.F)
  • _test文件中
  • t.Fuzz(t *testing.T, …),第一个参数是t,其余为参数,没有返回值
  • 一个模糊测试只能调用一次 (*testing.F).Fuzz ,
  • f.Add(xxxx…)和f.Fuzz(t, xxx)里面的参数必须是下面的类型,顺序必须一致
    • string, []byte
    • int, int8, int16, int32/rune, int64
    • uint, uint8/byte, uint16, uint32, uint64
    • float32, float64
    • bool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
    "testing"
    "unicode/utf8"
)

func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc) // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev, err1 := Reverse(orig)
        if err1 != nil {
            return
        }
        doubleRev, err2 := Reverse(rev)
        if err2 != nil {
            return
        }
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}
// 执行模糊测试:3步走
1. 编写测试函数
2. go test 测试非t.Fuzz函数和上一次模糊测试失败的case
3. go test -fuzz=Fuzz 执行模糊测试检查是否有新的bad casectrl-c停止/-fuzztime参数提供
测试时间限制/获取遇到错误停止
// 模糊测试非常的耗费性能和内存,且$GOCACHE/fuzz 缓存可能会占用很多

go command

go get

不再以 module-aware(区别于path-mode) 模式编译和安装包。现在只用来调整go.mod中的依赖。

  • GO111MODULE=off 的话,依然执行编译和安装
  • 安装命令:go install && go get

自动更新go.mod/go.sum

go mod graph, go mod vendor, go mod verify, and go mod why不再自动更新依赖文件,(go get, go mod tidy, or go mod download可以)

go version

新版本中,编译器选项/参数、相关环境变量等,将会包含在编译后的二进制文件中,能够更便于后人排查和查看信息。

go mod download

go.mod 里 go版本>1.17,go mod download只会下载明确声明required的依赖。 go mod download all下载全部

go mod vendor

新的-o参数,指定输出的目录(用于收集源码)

go work (工作区模式):

  • 在当前目录/父目录中存在go.work文件或者设置GOWORK环境变量会进入工作区模式 工作区模式可以保证跨多个模块开发时的依赖保持一致 当前go module在下面两个场景下会非常的不方便
    1. 依赖本地 replace module。 (定制依赖/本地依赖)
    2. 依赖本地未发布的 module。

      需要 replace golang.org/x/net => /Users/eddycjy/go/awesomeProject

使用go work

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go work init ./mod ./tools
项目目录如下
awesomeProject
├── mod
   ├── go.mod      // 子模块
   └── main.go
├── go.work         // 工作区
└── tools
    ├── fish.go
    └── go.mod      // 子模块
生成的 go.work 文件内容
go 1.18
use (
    ./mod 
    ./tools
)
新的 go.work  go.mod 语法一致也可以使用 replace 语法
go 1.18
use (...)
replace golang.org/x/net => example.com/fork/net v1.4.5
go.work 文件内共支持三个指令
go声明 go 版本号主要用于后续新语义的版本控制
use声明应用所依赖模块的具体文件路径路径可以是绝对路径或相对路径可以在应用命目录外均可
replace声明替换某个模块依赖的导入路径优先级高级 go.mod 中的 replace 指令

go test

  • 支持-fuzz, -fuzztime, and -fuzzminimizetime
  • go clean 支持 -fuzzcache

go fmt

并发读和格式化代码,执行更快

编译器

  • 寄存器传参新增支持Arm 64位/PowerPC 64位(go1.17已经支持了x86 64),性能提升10%
  • 为了支持范型,编译器的速度可能劣化15%(go 1.19会提高)
  • 运行时性能没有影响

链接器

大大减少了重定位,链接更快、需要的内存更少、生成的二进制文件更小

核心库

debug/buildinfo 可以读取二进制的构建信息BuildInfo

1
2
3
4
5
6
7
8
// BuildInfo represents the build information read from a Go binary.
type BuildInfo struct {
   GoVersion string         // Version of Go that produced this binary.
   Path      string         // The main package path
   Main      Module         // The module containing the main package
   Deps      []*Module      // Module dependencies
   Settings  []BuildSetting // Other information about the build.
}

net/netip包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type Addr struct { // (用于替换net.IP)
   // addr is the hi and lo bits of an IPv6 address. If z==z4,
   // hi and lo contain the IPv4-mapped IPv6 address.
   //
   // hi and lo are constructed by interpreting a 16-byte IPv6
   // address as a big-endian 128-bit number. The most significant
   // bits of that number go into hi, the rest into lo.
   //
   // For example, 0011:2233:4455:6677:8899:aabb:ccdd:eeff is stored as:
   //  addr.hi = 0x0011223344556677
   //  addr.lo = 0x8899aabbccddeeff
   //
   // We store IPs like this, rather than as [16]byte, because it
   // turns most operations on IPs into arithmetic and bit-twiddling
   // operations on 64-bit registers, which is much faster than
   // bytewise processing.
   addr uint128

   // z is a combination of the address family and the IPv6 zone.
   //
   // nil means invalid IP address (for a zero Addr).
   // z4 means an IPv4 address.
   // z6noz means an IPv6 address without a zone.
   //
   // Otherwise it's the interned zone name string.
   z *intern.Value
}
  • 占用内存更少
  • 不可变
  • 可比较 (== 做map的key) https://taoshu.in/go/go-1.18-netip.html net.IP [七宗罪]

reflect

  • Value.SetIterKey
  • Value.SetIterValue
  • Value.UnsafePointer
  • MapIter.Reset
  • Value.CanInt Value.CanUint Value.CanFloat Value.CanComplex
  • Value.FieldByIndexErr
  • reflect.Ptr => reflect.Pointer
  • reflect.PtrTo => reflect.PointerTo

strings

  • Cut
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // Cut slices s around the first instance of sep,
    // returning the text before and after sep.
    // The found result reports whether sep appears in s.
    // If sep does not appear in s, cut returns s, "", false.
    func Cut(s, sep string) (before, after string, found bool) {
     if i := Index(s, sep); i >= 0 {
        return s[:i], s[i+len(sep):], true
     }
     return s, "", false
    }
    
  • Clone
  • Trim,TrimLeft,TrimRight 优化:无内存分配(allocation free)

sync

Mutex.TryLock, RWMutex.TryLock, and RWMutex.TryRLock

1
2
3
4
5
6
7
8
9
10
// TryLock tries to lock m and reports whether it succeeded.
//
// Note that while correct uses of TryLock do exist, they are rare,
// and use of TryLock is often a sign of a deeper problem
// in a particular use of mutexes.
// 注意:尽管TryLock的正确使用存在但非常少。
// 是锁在特殊问题中的特殊使用
func (m *Mutex) TryLock() bool {
 ...
}
本文由作者按照 CC BY 4.0 进行授权

Comments powered by Disqus.