Go1.18新特性工作区模糊测试及泛型的使用详解

目录
  • 前言
  • Go工作区模式(Go Workspace Mode)
    • 现实的情况
      • 多仓库同时开发
      • 多个新仓库开始开发
    • 工作区模式是什么
    • 推荐的使用方法
    • 使用时的注意点
  • Go模糊测试(Go Fuzzing Test)
    • 为什么Golang要支持模糊测试
    • 模糊测试是什么
    • Golang的模糊测试如何使用
      • 最简单的实践例子
      • 提供自定义语料
    • 使用时的注意点
  • Go的泛型
    • 类型参数(Type Parameters)
    • 类型集合(Type Sets)
    • 类型推导(Type Inference)
      • 类型统一化(Type Unification)
      • 函数参数类型推导(Function Argument Type Inference)
      • 推导算法与示例
      • 约束类型推导(Constraints Type Inference)
      • 推导算法与示例
      • 简单的例子
      • 一个更复杂的例子
      • 使用约束类型推导保存类型信息
    • 泛型的使用局限
      • 成员函数无法使用泛型
      • 无法使用约束定义之外的方法
      • 无法使用成员变量
  • 总结

前言

2022年3月15日,Google发布了万众瞩目的Golang 1.18,带来了好几个重大的新特性,包括:

  • 解决本地同时开发多个仓库带来的一些问题的工作区(Workspace)
  • 能够自动探测代码分支,随机生成输入,并且检查代码是否会panic的模糊测试(Fuzzing Test)
  • 众多开发者盼星星盼月亮终于等到的泛型支持。

本文将简单讲述这三个特性的相关内容。

Go工作区模式(Go Workspace Mode)

现实的情况

多仓库同时开发

在实际的开发工作中,我们经常会同时修改存在依赖关系的多个module,例如在某个service模块上实现需求的同时,也需要对项目组的某个common模块做出修改,整个的工作流就会变成下面这样:

可以看到,每次修改Common库,都需要将代码push到远端,然后再修改本地service仓库的依赖,再通过go mod tidy从远端拉取Common代码,不可谓不麻烦。

有些同学可能会问了,这种情况,在service仓库的go.mod中添加一条replace不就能够解决吗?

但是,如果在go.mod中使用replace,在维护上需要付出额外的心智成本,万一将带有replace的go.mod推到远端代码库了,其他同学不就一脸懵逼了?

多个新仓库开始开发

假设此时我正在开发两个新的模块,分别是:

code.byted.org/SomeNewProject/Common
code.byted.org/SomeNewProject/MyService

并且MyService依赖于Common。

在开发过程中,出于各种原因,有可能不会立即将代码推送到远端,那么此时假设我需要本地编译MyService,就会出现go build(或者go mod tidy)自动下载依赖失败,因为此时Common库根本就没有发布到代码库中。

出于和上述“多仓库同时开发”相同的理由,replace也不应该被添加到MyService的go.mod文件中。

工作区模式是什么

Go工作区模式最早出现于Go开发者Michael Matloob在2021年4月提出的一个名为“Multi-Module Workspaces in cmd/go”的提案。

这个提案中提出,新增一个go.work文件,并且在这个文件中指定一系列的本地路径,这些本地路径下的go module共同构成一个工作区(workspace),go命令可以操作这些路径下的go module,在编译时也会优先使用这些go module。

使用如下命令就可以初始化一个工作区,并且生成一个空的go.work文件:

go work init .

新生成的go.work文件内容如下:

go 1.18
directory ./.

go.work文件中,directory指示了工作区的各个module目录,在编译代码时,会优先使用同一个workspace下的module。

在go.work中,也支持使用replace来指定使用本地代码库,但在大多数情况下,更好的做法是将依赖的本地代码库的路径加入directory中。

推荐的使用方法

因为go.work描述的是本地的工作区,所以也是不能提交到远端代码库的,虽然可以在.gitignore中加入这个文件,但是最推荐的做法还是在本地代码库的上层目录使用go.work。

例如上述的“多个新仓库开始开发”的例子,假设我的两个仓库的本地路径分别是:

/Users/bytedance/dev/my_new_project/common
/Users/bytedance/dev/my_new_project/my_service

那么我就可以在“/Users/bytedance/dev/my_new_project”目录下生成一个如下内容的go.work:

/Users/bytedance/dev/my_new_project/go.work:
go 1.18
directory (
    ./common
    ./my_service
)

在上层目录放置go.work,也可以将多个目录组织成一个workspace,并且由于上层目录本身不受git管理,所以也不用去管gitignore之类的问题,是比较省心的方式。

使用时的注意点

目前(go 1.18)仅go build会对go.work做出判断,而go mod tidy并不care Go工作区。

Go模糊测试(Go Fuzzing Test)

为什么Golang要支持模糊测试

从1.18起,模糊测试(Fuzzing Test)作为语言安全的一环,加入了Golang的testing标准库。Golang加入模糊测试的原因非常明显:安全是程序员在构建软件的过程中必不可少且日益重要的考量因素。

Golang至今为止,已经在保障语言安全方面提供了很多的特性和工具,例如强制使用显式类型转换、禁止隐式类型转换、对数组与切片的越界访问检查、通过go.sum对依赖包进行哈希校验等等。

在进入云原生时代之后,Golang成为了云原生基础设施与服务的头部语言之一。这些系统对安全性的要求自然不言而喻。尤其是针对用户的输入,不被用户的输入弄出处理异常、崩溃、被操控是对这些系统的基本要求之一。

这就要求我们的系统在处理任何用户输入的时候都能保持稳定,但是传统的质量保障手段,例如Code Review、静态分析、人工测试、Unit Test等等,在面对日益复杂的系统时,自然就无法穷尽所有可能的输入组合,尤其是一些非常不明显的corner case。

而模糊测试就是业界在解决这方面问题的优秀实践之一,Golang选择支持它也就不难理解了。

模糊测试是什么

模糊测试是一种通过数据构造引擎,辅以开发者可以提供的一些初始数据,自动构造出一些随机数据,作为对程序的输入来进行测试的一种方式。模糊测试可以帮助开发人员发现难以发现的稳定性、逻辑性甚至是安全性方面的错误,特别是当被测系统变得更加复杂时。

模糊测试在具体的实现上,通常可以不依赖于开发测试人员定义好的数据集,取而代之的则是一组通过数据构造引擎自行构造的一系列随机数据。模糊测试会将这些数据作为输入提供给待测程序,并且监测程序是否出现panic、断言失败、无限循环,或者其他什么异常情况。这些通过数据构造引擎生成的数据被称为语料(corpus) 。另外模糊测试其实也是一种持续测试的手段,因为如果不限制执行的次数或者执行的最大时间,它就会一直不停的执行下去。

Golang的模糊测试由于被实现在了编译器工具链中,所以采用了一种名为“覆盖率引导的fuzzing”的入参生成技术,大致运行过程如下:

Golang的模糊测试如何使用

Golang的模糊测试在使用时,可以简单地直接使用,也可以自己提供一些初始的语料。

最简单的实践例子

模糊测试的函数也是放在xxx_test.go里的,编写一个最简单的模糊测试例子(明显的除0错误):

package main
import "testing"
import "fmt"
func FuzzDiv(f *testing.F) {
        f.Fuzz(func(t *testing.T, a, b int) {
                fmt.Println(a/b)
        })
}

可以看到类似于单元测试,模糊测试的函数名都是FuzzXxx格式,且接受一个testing.F指针对象。

然后在函数中使用f.Fuzz对指定的函数进行模糊测试,被测试的函数的第一个参数必须是“*testing.T”类型,后面可以跟任意多个基本类型的参数。

编写完成之后,使用这样的命令来启动模糊测试:

go test -fuzz .

模糊测试默认会一直进行下去,只要被测试的函数不panic不出错。可以通过“-fuzztime”选项来限制模糊测试的时间:

go test -fuzztime 10s -fuzz .

使用模糊测试对上述代码进行测试时,会碰到产生panic的情况,此时模糊测试会输出如下信息:

warning: starting with empty corpus
fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0 (total: 0)
fuzz: elapsed: 0s, execs: 1 (65/sec), new interesting: 0 (total: 0)
--- FAIL: FuzzDiv (0.02s)
    --- FAIL: FuzzDiv (0.00s)
        testing.go:1349: panic: runtime error: integer divide by zero
            goroutine 11 [running]:
            runtime/debug.Stack()
                    /Users/bytedance/.mytools/go/src/runtime/debug/stack.go:24 +0x90
            testing.tRunner.func1()
                    /Users/bytedance/.mytools/go/src/testing/testing.go:1349 +0x1f2
            panic({0x1196b80, 0x12e3140})
                    /Users/bytedance/.mytools/go/src/runtime/panic.go:838 +0x207
            mydev/fuzz.FuzzDiv.func1(0x0?, 0x0?, 0x0?)
                    /Users/bytedance/Documents/dev_test/fuzz/main_test.go:8 +0x8c
            reflect.Value.call({0x11932a0?, 0x11cbf68?, 0x13?}, {0x11be123, 0x4}, {0xc000010420, 0x3, 0x4?})
                    /Users/bytedance/.mytools/go/src/reflect/value.go:556 +0x845
            reflect.Value.Call({0x11932a0?, 0x11cbf68?, 0x514?}, {0xc000010420, 0x3, 0x4})
                    /Users/bytedance/.mytools/go/src/reflect/value.go:339 +0xbf
            testing.(*F).Fuzz.func1.1(0x0?)
                    /Users/bytedance/.mytools/go/src/testing/fuzz.go:337 +0x231
            testing.tRunner(0xc000003a00, 0xc00007e3f0)
                    /Users/bytedance/.mytools/go/src/testing/testing.go:1439 +0x102
            created by testing.(*F).Fuzz.func1
                    /Users/bytedance/.mytools/go/src/testing/fuzz.go:324 +0x5b8
    Failing input written to testdata/fuzz/FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c
    To re-run:
    go test -run=FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c
FAIL
exit status 1
FAIL        mydev/fuzz        0.059s

其中的:

Failing input written to testdata/fuzz/FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c

这一行表示模糊测试将出现panic的测试入参保存到了这个文件里面,此时尝试输出这个文件的内容:

go test fuzz v1
int(-60)
int(0)

就可以看到引发panic的入参,此时我们就可以根据入参检查我们的代码是哪里有问题。当然,这个简单的例子就是故意写了个除0错误。

提供自定义语料

Golang的模糊测试还允许开发者自行提供初始语料,初始语料可以通过“f.Add”方法提供,也可以将语料以上面的“Failing input”相同的格式,写入“testdata/fuzz/FuzzXXX/自定义语料文件名”中。

使用时的注意点

目前Golang的模糊测试仅支持被测试的函数使用这些类型的参数:

[]byte, string, bool, byte, rune, float32, float64,
int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64

Go的泛型

Golang在1.18中终于加入了对泛型的支持,有了泛型之后,我们可以这样写一些公共库的代码:

旧代码(反射):

func IsContainCommon(val interface{}, array interface{}) bool {
    switch reflect.TypeOf(array).Kind() {
    case reflect.Slice:
        lst := reflect.ValueOf(array)
        for index := 0; index < lst.Len(); index++ {
            if reflect.DeepEqual(val, lst.Index(index).Interface()) {
                return true
            }
        }
    }
    return false
}

新代码(泛型):

func IsContainCommon[T any](val T, array []T) bool {
    for _, item := range array {
        if reflect.DeepEqual(val, item) {
            return true
        }
    }
    return false
}

泛型在Golang中增加了三个新的重要特性:

  • 在定义函数和类型时,支持使用类型参数(Type parameters)
  • 将接口(interface)重新定义为“类型的集合”
  • 泛型支持类型推导

下面逐个对这些内容进行简单说明。

类型参数(Type Parameters)

现在在定义函数和类型时,支持使用“类型参数”,类型参数的列表和函数参数列表很相似,只不过它使用的是方括号:

func Min[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}

上述的代码中,给Min函数定义了一个参数类型T,这很类似于C++中的“template”,只不过在Golang中,可以为这种参数类型指定它需要满足的“约束”。在这个例子中,使用的“约束”是“constraints.Ordered”。

然后就可以按照如下方式,使用这个函数了:

x := Min[int](1, 2)
y := Min[float64](1.1, 2.2)

为泛型函数指定类型参数的过程叫做“实例化(Instantiation)”,也可以将实例化后的函数保存成为函数对象,并且进一步使用:

f := Min[int64] // 这一步保存了一个实例化的函数对象
n := f(123, 456)

同样的,自定义的类型也支持泛型:

type TreeNode[T interface{}] struct {
    left, right *TreeNode[T]
    value T
}
func (t *TreeNode[T]) Find(x T) { ... }
var myBinaryTree TreeNode[int]

如上述代码,struct类型在使用泛型时,支持自己的成员变量和自己持有同样的泛型类型。

类型集合(Type Sets)

下面稍微深入的讲一下上述例子提到的“约束”。上文的例子中的“int”“float64”“int64”在实例化时,实际上是被作为“参数”传递给了“类型参数列表”,即上文例子中的“[T constraints.Ordered]”。

就像传递普通参数需要校验参数的类型一样,传递类型参数时也需要对被传递的类型参数进行校验,检查被传递的类型是否满足要求。

例如上文例子中,使用“int”“float64”“int64”这几个类型对Min函数进行实例化时,编译器都会检查这些参数是否满足“constraints.Ordered”这个约束。而这个约束描述了所有可以使用“<”进行比较的类型的集合,这个约束本身也是一个interface。

在Go的泛型中,类型约束必须是一种interface,而“传统”的Golang中对interface的定义是“一个接口定义了一组方法集合”,任何实现了这组方法集合的类型都实现了这个interface:

不过这里就出现了一个问题:“<”的比较显然不是一个方法(Go当中不存在C++的运算符重载),而描述了这个约束的constraints.Ordered自身的确也是一个interface。

所以从1.18开始,Golang将Interface重新定义为“一组类型的集合”,按照以前对interface的看法,也可以将一个interface看成是“所有实现了这个interface的方法集合的类型所构成的集合”:

其实两种看法殊途同归,但是后者显然可以更灵活,直接将一组具体类型指定成一个interface,即使这些类型没有任何的方法。

例如在1.18中,可以这样定义一个interface:

type MyInterface interface {
    int|bool|string
}

这样的定义表示int/bool/string都可以被当作MyInterface进行使用。

那么回到constraints.Ordered,它的定义实际上是:

type Ordered interface {
    Integer|Float|~string
}
type Float interface {
    ~float32|~float64
}
type Integer interface {
    Signed|Unsigned
}
type Signed interface {
    ~int|~int8|~int16|~int32|~int64
}
type Unsigned interface {
    ~uint|~uint8|~uint16|~uint32|~uint64
}

其中前置的“~”符号表示“任何底层类型是后面所跟着的类型的类型”,例如:

type MyString string

这样定义的MyString是可以满足“~string”的类型约束的。

类型推导(Type Inference)

最后,所有支持泛型的语言都会有的类型推导自然也不会缺席。类型推导功能可以允许使用者在调用泛型函数时,无需指定所有的类型参数。例如下面这个函数:

// 将F类型的slice变换为T类型的slice
// 关键字 any 等同于 interface{}
func Map[F, T any](src []F, f func(F) T) []T {
    ret := make([]T, 0, len(src))
    for _, item := range src {
        ret = append(ret, f(item))
    }
    return ret
}

在使用时可以这样:

var myConv := func(i int)string {return fmt.Sprint(i)}
var src []int
var dest []string
dest = Map[int, string](src, myConv) // 明确指定F和T的类型
dest = Map[int](src, myConv) // 仅指定F的类型,T的类型交由编译器推导
dest = Map(src, myConv) // 完全不指定类型,F和T都交由编译器推导

泛型函数在使用时,可以不指定具体的类型参数,也可以仅指定类型参数列表左边的部分类型。当自动的类型推导失败时,编译器会报错。

Golang泛型中的类型推导主要分为两大部分:

  • 函数参数类型推导:通过函数的入参,对类型参数对应的具体类型进行推导。
  • 约束类型推导:通过已知具体类型的类型参数,来推断出未知类型参数的具体类型。

而这两种类型推导,都依赖一种名为“类型统一化(Type Unification)”的技术。

类型统一化(Type Unification)

类型统一化是对两个类型进行比较,这两个类型有可能本身是一个类型参数,也有可能包含一个类型参数。

比较的过程是对这两个类型的“结构”进行对比,并且要求被比较的两个类型满足下列条件:

  • 剔除类型参数后,两个类型的“结构”必须能够匹配
  • 剔除类型参数后,结构中剩余的具体类型必须相同
  • 如果两者均不含类型参数,那么两者的类型必须完全相同,或者底层数据类型完全相同

这里说的“结构”,指的是类型定义中的slice、map、function等等,以及它们之间的任意嵌套。

满足这几个条件时,类型统一性对比才算做成功,编译器才能进一步对类型参数进行推测,例如:

如果我们此时有“T1”、“T2”两个类型参数,那么“[]map[int]bool”可以匹配如下类型:

[]map[int]bool // 它本身
T1 // T1被推断为 []map[int]bool
[]T1 // T1被推断为 map[int]bool
[]map[T1]T2 // T1被推断为 int, T2被推断为 bool

作为反例,“[]map[int]bool”显然无法匹配这些类型:

int
struct{}
[]struct{}
[]map[T1]string
// etc...

函数参数类型推导(Function Argument Type Inference)

函数参数类型推导,顾名思义是在泛型函数被调用时,如果没有被完全指定所有的类型参数,那么编译器就会根据函数实际入参的类型,对类型参数所对应的具体类型进行推导,例如本文最开始的Min函数:

func Min[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}
ans := Min(1, 2) // 此时类型参数T被推导为int

和其他支持泛型的语言一样,Golang的函数参数类型推导只支持“能够从入参推导的类型参数”,如果类型参数用于标记返回类型,那么在使用时必须明确指定类型参数:

func MyFunc[T1, T2, T3 any](x T1) T2 {
    // ...
    var x T3
    // ...
}
ans := MyFunc[int, bool, string](123) // 需要手动指定

类似这样的函数,部分的类型参数仅出现在返回值当中(或者仅出现在函数体中,不作为入参或出参出现),就无法使用函数参数类型推导,而必须明确手动指定类型。

推导算法与示例

还是拿Min函数作为例子,讲解一下函数参数类型推导的过程:

func Min[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}

先来看看第一种情况:

Min(1, 2)

此时两个入参均为无类型字面值常量,所以第一轮的类型统一化被跳过,且入参的具体类型没有被确定,此时编译器尝试使用两个参数的默认类型int,由于两个入参在函数定义处的类型都是“T”,且两者都使用默认类型int,所以此时T被成功推断为int。

然后来看第二种情况:

Min(1, int64(2))

此时第二个参数有一个明确的类型int64,所以在第一轮的类型统一化中,T被推断为int64,且在尝试为第一轮漏掉的第一个参数“1”确定类型时,由于“1”是一个合法的int64类型值,所以T被成功推断为int64。

再来看第三种情况:

Min(1.5, int64(2))

此时第二个参数有一个明确的类型int64,所以在第一轮的类型统一化中,T被推断为int64,且在尝试为第一轮漏掉的第一个参数“1.5”确定类型时,由于“1.5”不是一个合法的int64类型值,类型推导失败,此时编译器报错。

最后看第四种情况:

Min(1, 2.5)

和第一种情况类似,第一轮的类型统一化被跳过,且两个入参的具体类型没有被确定,此时编译器开始尝试使用默认类型。两个参数的默认类型分别是int和float64,由于在类型推导中,同一个类型参数T只能被确定为一种类型,所以此时类型推导也会失败。

约束类型推导(Constraints Type Inference)

约束类型推导是Golang泛型的另一个强大武器,它可以允许编译器通过一个类型参数来推导另一个类型参数的具体类型,也可以通过使用类型参数来保存调用者的类型信息。

约束类型推导可以允许使用其他类型参数来为某个类型参数指定约束,这类约束被称为“结构化约束”,这种约束定义了类型参数必须满足的数据结构,例如:

// 将一个整数slice中的每个元素都x2后返回
func DoubleSlice[S ~[]E, E constraints.Integer](slice S) S {
    ret := make(S, 0, len(slice))
    for _, item := range slice {
        ret = append(ret, item + item)
    }
    return ret
}

在这个函数的定义中,“[]E”就是一个简写的对S的结构化约束,其完整写法应是“interface{[]E}”,即以类型集合的方式来定义的interface,且其中只包含一种定义“~[]E”,意为“底层数据类型是[]E的所有类型”。

注意,一个合法的结构化约束所对应的类型集合,应该满足下列任意一个条件:

  • 类型集合中只包含一种类型
  • 类型集合中所有类型的底层数据类型均完全相同

在这个例子中,S使用的结构化约束中,所有满足约束的类型的底层数据类型均为[]E,所以是一个合法的结构化约束。

当存在无法通过函数参数类型推导确定具体类型的类型参数,且类型参数列表中包含结构化约束时,编译器会尝试进行约束类型推导。

推导算法与示例

简单的例子

结合我们刚才的例子“DoubleSlice”函数,讲一下约束类型推导的具体过程:

type MySlice []int
ans := DoubleSlice(MySlice{1, 2, 3})

在这个调用中,首先执行的是普通的函数参数类型推导,这一步会得到一个这样的推导结果:

S => MySlice

此时编译器发现,还有一个类型参数E没有被推导,且当前存在一个使用结构化约束的类型参数S,此时开始约束类型推导。

首先需要寻找已经完成类型推导的类型参数,在这个例子里是S,它的类型已经被推导出是MySlice。

然后会将S的实际类型“MySlice”,与S的结构化约束“~[]E”进行类型统一化,由于MySlice的底层类型是[]int,所以结构化匹配之后,得到了这样的匹配结果:

E => int

此时所有的类型参数都已经被推断,且符合各自的约束,类型推导结束。

一个更复杂的例子

假设有这样一个函数:

func SomeComplicatedMethod[S ~[]M, M ~map[K]V, K comparable, V any](s S) {
    // comparable 是一个内置的约束,表示所有可以使用 == != 运算符的类型
}

然后我们这样去调用它:

SomeComplicatedMethod([]map[string]int{})

编译时产生的类型推导过程如下,首先是函数参数类型推导的结果:

S => []map[string]int

然后对S使用约束类型推导,对比 []map[string]int 和 ~[]M,得到:

M => map[string]int

再继续对M使用约束类型推导,对比 map[string]int 和 ~map[K]V,得到:

K => string
V => int

至此类型推导成功完成。

使用约束类型推导保存类型信息

约束类型推导的另一个作用就是,它能够保存调用者的原始参数的类型信息。

还是以这一节的“DoubleSlice”函数做例子,假设我们现在实现一个更加“简单”的版本:

func DoubleSliceSimple[E constraints.Integer](slice []E) []E {
    ret := make([]E, 0, len(slice))
    for _, item := range slice {
        ret = append(ret, item + item)
    }
    return ret
}

这个版本只有一个类型参数E。此时我们按照之前的方式去调用它:

type MySlice []int
ans := DoubleSliceSimple(MySlice{1, 2, 3}) // ans 的类型是 []int !!!

此时的类型推导仅仅是最基础的函数参数类型推导,编译器会对MySlice和[]E直接做结构化比较,得出E的实际类型是int的结论。

此时DoubleSliceSimple这个函数返回的类型是[]E,也就是[]int,而不是调用者传入的MySlice。而之前的DoubleSlice函数,通过定义了一个使用结构化约束的类型参数S,并且直接用S去匹配入参的类型,且返回值类型也是S,就可以保留调用者的原始参数类型。

泛型的使用局限

目前Golang泛型依然还有不少的局限,几个主要的局限点包括:

  • 成员函数无法使用泛型
  • 不能使用没在约束定义中指定的方法,即使类型集合里所有的类型都实现了该方法
  • 不能使用成员变量,即使类型集合里所有的类型都拥有该成员

下面分别举例:

成员函数无法使用泛型

type MyStruct[T any] struct {
    // ...
}
func (s *MyStruct[T]) Method[T2 any](param T2) { // 错误:成员函数无法使用泛型
    // ...
}

在这个例子中,MyStruct[T]的成员函数Method定义了一个只属于自己的函数参数T2,然而这样的操作目前是不被编译器支持的(今后也很可能不会支持)。

无法使用约束定义之外的方法

type MyType1 struct {
    // ...
}
func (t MyType1) Method() {}
type MyType2 struct {
    // ...
}
func (t MyType2) Method() {}
type MyConstraint interface {
    MyType1 | MyType2
}
func MyFunc[T MyConstraint](t T) {
    t.Method() // 错误: MyConstraint 不包含 .Method() 方法
}

这个例子中,MyConstraint集合中的两个成员MyType1和MyType2尽管都实现了.Method()函数,但是也无法直接在泛型函数中调用。

如果需要调用,则应该将MyConstraint改写为如下形式:

type MyConstraint interface {
    MyType1 | MyType2
    Method()
}

无法使用成员变量

type MyType1 struct {
    Name string
}
type MyType2 struct {
    Name string
}
type MyConstraint interface {
    MyType1 | MyType2
}
func MyFunc[T MyConstraint](t T) {
    fmt.Println(t.Name) // 错误: MyConstraint 不包含 .Name 成员
}

在这个例子当中,虽然MyType1和MyType2都包含了一个Name成员,且类型都是string,也依然无法以任何方式在泛型函数当中直接使用。

因为类型约束本身是一个interface,而interface的定义中只能包含类型集合,以及成员函数列表。

总结

Golang 1.18带来了上述三个非常重要的新特性,其中:

  • 工作区模式可以让本地开发的工作流更加顺畅。
  • 模糊测试可以发现一些边边角角的情况,提升代码的鲁棒性。
  • 泛型可以让一些公共库的代码更加优雅,避免像以前一样,为了“通用性”不得不采用反射的方式,不仅写起来难写,读起来难受,还增加了运行期的开销,因为反射是运行时的动态信息,而泛型是编译期的静态信息。

本文也是简单讲了这几方面的内容,希望能让大家对Golang中的这些新玩意儿有一个基本的了解。

参考文献

《Go 1.18 is released!》

《An Introduction To Generics》

《Get familiar with workspaces》

《Tutorial: Getting started with fuzzing》

《Go 1.18新特性前瞻:原生支持Fuzzing测试》

以上就是Go 1.18新特性工作区 模糊测试 泛型的使用详解的详细内容,更多关于Go 1.18 工作区模糊测试泛型的资料请关注我们其它相关文章!

(0)

相关推荐

  • Go1.18新特性之泛型使用三步曲(小结)

    目录 01 Go中的泛型是什么 1.1 传统的函数编写方式 1.2 泛型函数编写方式 02 从泛型被加入之前说起 2.1 针对每一种类型编写一套重复的代码 2.2 使用空接口并通过类型断言来判定具体的类型 2.3 传递空接口并使用反射解析具体类型 2.4 通过自定义接口类型实现 03 深入理解泛型--泛型使用“三步曲” 3.1 第一步:类型参数化 3.2 第二步:给类型添加约束 3.3 第三步:类型参数实例化 04 泛型类型约束和普通接口的区别 总结 01 Go中的泛型是什么 众所周知,Go是一

  • 浅谈Go1.18中的泛型编程

    目录 前言 以前的Go泛型 泛型是什么 Go的泛型 泛型函数 泛型类型 类型集合 和接口的差异 总结 前言 经过这几年的千呼万唤,简洁的Go语言终于在1.18版本迎来泛型编程.作为一门已经有了14年历史的强类型语言,很难相信它到现在才开始有一个正式的泛型. 以前的Go泛型 虽然直到1.18版本才加入泛型,但是在2014年便有相关的讨论要在Go中加入泛型设计.但是由于各种原因没有实现.而之后的接口(interface)的提出,让泛型进一步搁置.但是由于接口的缺陷,最终Go团队还是在1.18的版本中

  • Go1.18 新特性之多模块Multi-Module工作区模式

    目录 背景 举例:未发布的 module Go1.18 新特性:多模块(Multi-Module)工作区模式 Go1.18 工作区模式 初始化一个新的工作区 go.work 文件结构 go.work 文件优先级高于 go.mod 中定义在 如何禁用工作区 背景 在 go 中使用多个模块可能真的是一件苦差事.特别是当您的一个模块依赖于另一个模块时,您需要同时编辑这两个模块! 您编辑父模块,但是然后您需要将其推送到repo.然后在依赖模块中运行 update 以下载新版本.最终使用2行修复您需要的.

  • Go1.18新特性使用Generics泛型进行流式处理

    前言 Stream 是一个基于 Go 1.18+ 泛型的流式处理库, 它支持并行处理流中的数据. 并行流会将元素平均划分多个的分区, 并创建相同数量的 goroutine 执行, 并且会保证处理完成后流中元素保持原始顺序. GitHub - xyctruth/stream: A Stream library based on Go 1.18+ Generics (Support Parallel Stream) 安装 需要安装 Go 1.18+ 版本 $ go get github.com/xy

  • Go1.18新特性对泛型支持详解

    目录 1.泛型是什么 2.泛型类型的定义 2.1.声明一个自定义类型 2.2.内置的泛型类型any和comparable 2.3.泛型中的~符号是什么 1.泛型是什么 Go1.18增加了对泛型的支持,泛型是一种独立于使用的特定类型编写代码的方式.现在可以编写函数和类型适用于一组类型集合的任何一种.泛型生命周期只在编译期,旨在开发中减少重复代码的编写. 由于go属于静态强类型语言,例如在比较两个数的大小时,没有泛型的时候,仅仅只是传入类型不一样,我们就要再复制一份一样的函数,如果有了泛型就可以减少

  • Go1.18都出泛型了速来围观

    go泛型使用的官方说明:https://go.dev/doc/tutorial/generics 在使用之前先把go更新到1.18或者以上的版本:https://go.dev/doc/install 如果用过c++或者Java的话,那么对泛型这个概念应该是不陌生的.(下面这段定义摘抄自百度百科)泛型程序设计(generic programming)是程序设计语言的一种风格或范式.泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型. 我们可以用个

  • Go1.18新特性工作区模糊测试及泛型的使用详解

    目录 前言 Go工作区模式(Go Workspace Mode) 现实的情况 多仓库同时开发 多个新仓库开始开发 工作区模式是什么 推荐的使用方法 使用时的注意点 Go模糊测试(Go Fuzzing Test) 为什么Golang要支持模糊测试 模糊测试是什么 Golang的模糊测试如何使用 最简单的实践例子 提供自定义语料 使用时的注意点 Go的泛型 类型参数(Type Parameters) 类型集合(Type Sets) 类型推导(Type Inference) 类型统一化(Type Un

  • Spring Boot 2.4 新特性之一键构建Docker镜像的过程详解

    背景 在我们开发过程中为了支持 Docker 容器化,一般使用 Maven 编译打包然后生成镜像,能够大大提供上线效率,同时能够快速动态扩容,快速回滚,着实很方便.docker-maven-plugin 插件就是为了帮助我们在 Maven 工程中,通过简单的配置,自动生成镜像并推送到仓库中. spotify .fabric8 这里主要使用的主要是如下两种插件 spotify .fabric8 , - -配置通过 xml 定义出 Dockerfile 或者挂载外部 Dockerfile 通过调用

  • SpringBoot 3.0 新特性内置声明式HTTP客户端实例详解

    目录 http interface 什么是声明式客户端 测试使用 1. maven 依赖 2. 创建 Http interface 类型 3. 注入声明式客户端 4. 单元测试调用 http interface http interface 从 Spring 6 和 Spring Boot 3 开始,Spring 框架支持将远程 HTTP 服务代理成带有特定注解的 Java http interface.类似的库,如 OpenFeign 和 Retrofit 仍然可以使用,但 http inte

  • Java8新特性时间日期库DateTime API及示例详解

    Java8新特性的功能已经更新了不少篇幅了,今天重点讲解时间日期库中DateTime相关处理.同样的,如果你现在依旧在项目中使用传统Date.Calendar和SimpleDateFormat等API来处理日期相关操作,这篇文章你一定不要错过.来刷新你的知识库吧! 背景 Java对日期.日历及时间的处理一直以来都饱受诟病,比如java.util.Date和java.util.Calendar类易用性差,不支持时区,非线程安全:还有用于格式化日期的类DateFormat也是非线程安全的等问题. J

  • ES6新特性:使用export和import实现模块化详解

    在ES6前, 前端就使用RequireJS或者seaJS实现模块化, requireJS是基于AMD规范的模块化库,  而像seaJS是基于CMD规范的模块化库,  两者都是为了为了推广前端模块化的工具, 更多有关AMD和CMD的区别, 后面参考给了几个链接: 现在ES6自带了模块化, 也是JS第一次支持module, 在很久以后 ,我们可以直接作用import和export在浏览器中导入和导出各个模块了, 一个js文件代表一个js模块: 现代浏览器对模块(module)支持程度不同, 目前都是

  • MySQL8新特性:自增主键的持久化详解

    前言 自增主键没有持久化是个比较早的bug,这点从其在官方bug网站的id号也可看出(https://bugs.mysql.com/bug.php?id=199).由Peter Zaitsev(现Percona CEO)于2003年提出.历史悠久且臭名昭著. 首先,直观的重现下. mysql> create table t1(id int auto_increment primary key); Query OK, 0 rows affected (0.01 sec) mysql> inser

随机推荐