Go 1.18 的那些事——工作区、模糊测试、泛型

动手点关注 干货不迷路 👆

前言

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

  1. 解决本地同时开发多个仓库带来的一些问题的工作区(Workspace)

  2. 能够自动探测代码分支,随机生成输入,并且检查代码是否会 panic 的模糊测试(Fuzzing Test)

  3. 众多开发者盼星星盼月亮终于等到的泛型支持。

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

Go 工作区模式(Go Workspace Mode)

现实的情况

多仓库同时开发

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

f6609a659a210ed5377b838a85d6f633.png

可以看到,每次修改 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”的入参生成技术,大致运行过程如下:

b0196b75af44dd021b1baad90b258422.png

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 中增加了三个新的重要特性:

  1. 在定义函数和类型时,支持使用类型参数(Type parameters)

  2. 将接口(interface)重新定义为“类型的集合”

  3. 泛型支持类型推导

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

类型参数(Type Parameters)

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

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

上述的代码中,给 Min 函数定义了一个参数类型 T,这很类似于 C++中的“template < typename T > ”,只不过在 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:

665e9d945ac890cc2c8734eceaac27ea.png

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

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

1001521c39dd0939cb92cc4e35617be3.png

其实两种看法殊途同归,但是后者显然可以更灵活,直接将一组具体类型指定成一个 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 泛型中的类型推导主要分为两大部分:

  1. 函数参数类型推导:通过函数的入参,对类型参数对应的具体类型进行推导。

  2. 约束类型推导:通过已知具体类型的类型参数,来推断出未知类型参数的具体类型。

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

类型统一化(Type Unification)

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

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

  1. 剔除类型参数后,两个类型的“结构”必须能够匹配

  2. 剔除类型参数后,结构中剩余的具体类型必须相同

  3. 如果两者均不含类型参数,那么两者的类型必须完全相同,或者底层数据类型完全相同

这里说的“结构”,指的是类型定义中的 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
2bdc637f13beeed6f2f6eaacf985db8e.png

作为反例,“[]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) // 需要手动指定

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

推导算法与示例

0c32738ab6643b3f4469d3ce6e62d02a.jpeg

还是拿 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 的所有类型”。

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

  1. 类型集合中只包含一种类型

  2. 类型集合中所有类型的底层数据类型均完全相同

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

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

推导算法与示例

fe866c73f85a3d5bc94a6a6469ebf450.png

简单的例子

结合我们刚才的例子“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 泛型依然还有不少的局限,几个主要的局限点包括:

  1. 成员函数无法使用泛型

  2. 不能使用没在约束定义中指定的方法,即使类型集合里所有的类型都实现了该方法

  3. 不能使用成员变量,即使类型集合里所有的类型都拥有该成员

下面分别举例:

成员函数无法使用泛型
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 带来了上述三个非常重要的新特性,其中:

  1. 工作区模式可以让本地开发的工作流更加顺畅。

  2. 模糊测试可以发现一些边边角角的情况,提升代码的鲁棒性。

  3. 泛型可以让一些公共库的代码更加优雅,避免像以前一样,为了“通用性”不得不采用反射的方式,不仅写起来难写,读起来难受,还增加了运行期的开销,因为反射是运行时的动态信息,而泛型是编译期的静态信息。

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

参考文献

  1. https://go.dev/blog/go1.18

  2. https://go.dev/blog/intro-generics

  3. https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md

  4. https://go.dev/blog/get-familiar-with-workspaces

  5. https://go.dev/doc/tutorial/fuzz

  6. https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18/

相关推荐

2024 抖音欢笑中国年(五):Wasm、WebGL 在互动技术中的创新应用

前言随着 Web 前端技术的不断发展,越来越多的新兴技术方案被引入到 Web 开发中,其中 Wasm 和 WebGL 作为前端领域的两大利器,为开发者带来了更多的可能性。本文将结合2024 年抖音欢笑中国年的部分项目,重点介绍如何利用 Wasm 和 WebGL 对目前流行的一些前端互动技术(比如 Lottie、渲染引擎、动画图片等)进行创新和实践,利用 Wasm 和 WebGL 等新技术方案的特性...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(四):渲染技术实践与探索

作者:陈瑞、欧阳浩铸、王武俊、倪梵云前言抖音在2024年春节期间推出了欢笑中国年系列活动,为用户带来了全新的体验和乐趣。而SAR Creator则为该项目研发工作提供了重要的技术支持。SAR Creator是一款基于 Typescript 的高性能、轻量化的互动解决方案,目前支持了浏览器和跨端框架平台,服务于字节内部的各种互动业务。这些绚烂多彩的互动场景当然也离不开实时渲染技术的支持,因此本文将专...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(三):编辑器技巧与实践

前言本次春节活动中,我们大部分场景使用内部的 SAR Creator互动方案来实现。SAR Creator 是一款基于 TypeScript 的高性能、轻量化的互动解决方案,目前支持了Web和字节内部跨端框架平台,服务于字节内部的各种互动业务,包括但不限于抖音春节、抖音直播礼物、抖音UG活动等。SAR Creator 编辑器支持了图形化界面,提供了各类完善的系统(光照、动画、脚本等)供用户快速便捷...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(二):AnnieX互动容器创新玩法解析

本文基于24年抖音春节活动业务背景,介绍了字节跨端容器AnnieX在游戏互动套件上的探索,致力于提升容器在游戏互动场景的优化能力。业务背景AnnieX作为字节一方游戏统一容器,服务字节内部电商、直播、UG等跨端场景业务。在字节一方游戏互动场景,有大量的一方游戏业务对容器有特定的流量、端能力和游戏优化的诉求。因此我们不断深入互动游戏业务特点,为字节游戏提供完善游戏端能力和流量运营能力,同时提供游戏互...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(一):招财神龙互动技术揭秘

字节跳动旗下的抖音等 App 在 2024 年春节期间推出了欢笑中国年系列活动,在实现增长业务目标的同时,为用户带来了全新的体验和乐趣。「招财神龙」是其中的一个重要玩法。前言本次春节活动,使用到了字节内的主要前端、跨端、互动技术产品。主要涉及:跨端框架提供了首屏直出的方案使其具有较短的首屏时间,能够大大提升业务加载成功率。跨端框架也提供了 Canvas 作为 SAR Creator 等渲染引擎的...

字节跳动技术团队官方博客 1万+

Monorepo 解决方案 — 基于 Bazel 的 Xcode 性能优化实践

背景介绍书接上回《Monorepo 解决方案 — Bazel 在头条 iOS 的实践》,在头条工程切换至 Bazel 构建系统后,为了支持用户使用 Xcode 开发的习惯,我们使用了开源项目 Tulsi 作为生成工具,用于将 Bazel 工程转换为 Xcode 工程。但是在使用的过程中,我们发现了一些问题,其中影响较大的是,Xcode 工程卡顿:对于头条这种大型项目来说,Xcode 卡顿一直是本地...

字节跳动技术团队官方博客 1万+

抖音 ANR 自动归因平台建设实践

背景介绍本文在 2024 年初最新一期『抖音客户端基础技术大揭秘』技术沙龙活动中已做过专题分享,本次将内容重新整理文章进行分享。公众号后台回复技术沙龙可查看沙龙回放及 PPT~抖音作为一个超大型的应用,我们在 ANR 问题治理上面临着很大的挑战。首先对于存量问题的优化,由于缺少有效的归因手段,一些长期的疑难问题一直难以突破解决,例如长期位于 Top 1 的 nativePollOnce 问题。同时...

字节跳动技术团队官方博客 2万+

CVPR 2024 | CAMixerSR 动态注意力分配的超分辨率加速框架

背景随着相关技术和应用的发展,比如超高清屏幕、虚拟现实(VR)等沉浸式体验的增加,用户对超高分辨率图像和视频的需求变得越来越强烈。在这些场景中,图像的质量和清晰度对于提供最佳的用户体验至关重要。超高分辨率不仅能提供更清晰、更真实的视觉效果,还能在一定程度上增强人们的互动和沉浸感,在一些VR场景中我们需要8K甚至16K的才可以满足需求。然而要生成或者处理这些超高分辨率的内容,对算力的要求也是与日增长...

字节跳动技术团队官方博客 1万+

CVPR 2024 | Modular Blind Video Quality Assessment:模块化无参视频质量评估

无参视频质量评估 (Blind Video Quality Assessment,BVQA) 在评估和改善各种视频平台并服务用户的观看体验方面发挥着关键作用。当前基于深度学习的模型主要以下采样/局部块采样的形式分析视频内容,而忽视了实际空域分辨率和时域帧率对视频质量的影响,随着高分辨率和高帧率视频投稿逐渐普及,特别是跨分辨率/帧率视频转码档位画质评估场景中,这种影响变得更加不可忽视。在本文中,我们...

字节跳动技术团队官方博客 1万+

2024 AI & 前端:回首展望,光芒未至,破晓之前!

前言回望 2023 年,ChatGPT 的突然爆火,让 AI 无疑成为最为值得注目的新兴领域之一,我们也一起见证了生成式 AI 的寒武纪大爆发。这一年来,国内外的生成式 AI 、大模型和相关产品以令人眼花缭乱的速度更新迭代,新的创业浪潮风起云涌。在这 AI 浪潮下,也让我们有了新的开发思考,探索着在各个环节中“前端 & AI”的应用场景。勇于探索的前端开发者们已经开始挥舞着 AI 的“魔法...

字节跳动技术团队官方博客 1万+

Kotlin 云端差分缓存技术

本文由字节跳动 Buildinfra 团队出品。在我们的工程上线 Monorepo 全源码后,Kotlin 编译成了整个编译中最耗时的步骤,全源码过程中大量的 BuildCache Miss 导致我们的编译数据落后原来多仓二进制时代很多,且业界没有相关的解决方案。本篇文章我们来具体阐述下 BuildInfra 团队自研的解决方案 - Kotlin 云端差分方案的原理和技术实现。一、Monorepo...

字节跳动技术团队官方博客 1万+

字节跳动基础架构SRE-Copilot获得2023 CCF国际AIOps挑战赛冠军

近日,2023 CCF国际AIOps挑战赛决赛暨“大模型时代的AIOps”研讨会在北京成功举办,活动吸引了来自互联网、运营商、科研院所、高校、软硬件厂商等领域多名专家学者参与,为智能运维的前沿学术研究、落地生产实践打开了新思路。决赛中,从初赛两百多支队伍中脱颖而出的十支入围队伍分别展示了各自的方案,并进行了现场答辩,评审专家从选题方向、创新性、实用性、完整度和实验复现结果等多角度进行了综合评定,最...

字节跳动技术团队官方博客 2万+

字节跳动百万级Metrics Agent性能优化的探索与实践

背景metricserver2 (以下简称Agent)是与字节内场时序数据库 ByteTSD 配套使用的用户指标打点 Agent,用于在物理机粒度收集用户的指标打点数据,在字节内几乎所有的服务节点上均有部署集成,装机量达到百万以上。此外Agent需要负责打点数据的解析、聚合、压缩、协议转换和发送,属于CPU和Mem密集的服务。两者结合,使得Agent在监控全链路服务成本中占比达到70%以上,对Ag...

字节跳动技术团队官方博客 1万+

西瓜视频RenderThread引起的闪退问题攻坚历程

背景影响西瓜之前存在过一类RenderThread闪退,从堆栈上看,全部都是系统so调用,给人的第一印象像是一个系统bug,无从下手。闪退集中在Android 5~6上,表现为打开直播间立即闪退。该问题在2022年占据Native Crash Top5,2023年更是上升到到Top1。因此有必要投入时间和精力再重新审视一下这个问题。在历经多周的源码分析和排查后,逐步明确了问题根因并修复,最终取得了...

字节跳动技术团队官方博客 2万+

字节电商双11 大促容量保障是如何做的?

前言Rhino 简介Rhino是字节自研全链路容量评估产品,致力于构建完整的全链路容量评估解决方案(覆盖:容量预估->资源准备->数据准备->容量验证->监控->分析->决策->处理反馈);围绕容量在稳定性、成本、效率 三方面提供业务全方位基础支撑。Rhino 目前已经成为字节各业务容量评估主流解决方案,并且历年来在业务大型活动稳定性保障中(抖音春节项目、...

字节跳动技术团队官方博客 10万+

使用火山引擎 APMPlus 解决抖音Top 1 Java 崩溃的通用优化方案

背景近3个月,抖音 Android 版面临一个多次触发线上报警的崩溃问题,全量版本和灰度版本的异常数据激增,该问题不仅容易触发报警,更成为了 Java Top 1 崩溃问题,带来巨大困扰,急需攻坚解决。本文展现了具体的分析过程、优化思路和解决方案,同时提供了已集成该方案的实用工具。初步分析多维特征我们以某发版期间数据为例进行分析:机型方面:比较分散,有聚集部分samsung sm-s9180 占比...

字节跳动技术团队官方博客 2万+

用 Addon 增强 Node.js 和 Electron 应用的原生能力

前言Node.js Addon 是 Node.js 中为 JavaScript 环境提供 C/C++ 交互能力的机制。其形态十分类似 Java 的 JNI,都是通过提供一套 C/C++ SDK,用于在 C/C++ 中创建函数方法、进行数据转换,以便 JavaScript / Java 等语言进行调用。这样编写的代码通常叫做 Bindings。此外还有基于 C ABI Calling Convent...

字节跳动技术团队官方博客 2万+

火山引擎 ByteHouse 的增强型数据导入技术实践

作为企业数字化建设的必备要素,易用的数据引擎能帮助企业提升数据使用效率,更好提升数据应用价值,夯实数字化建设基础。数据导入是衡量OLAP引擎性能及易用性的重要标准之一,高效的数据导入能力能够加速数据实时处理和分析的效率。作为一款OLAP引擎,火山引擎云原生数据仓库ByteHouse源于开源ClickHouse,在字节跳动多年打磨下,提供更丰富的能力和更强性能,能为用户带来极速分析体验,支撑实时数据...

字节跳动技术团队官方博客 1万+

打造企业级智能问答系统的秘密:如何使用云数据库 PostgreSQL 版实现向量检索

本文就如何利用火山引擎云数据库 PostgreSQL 版和大语言模型技术(Large Language Model,简称 LLM),实现企业级智能交互式问答系统进行介绍。通过本文,你将会了解交互式问答系统的原理,学习 PostgreSQL 的向量化存储和检索技术,以及大语言模型交互技术等。背景在大数据的浪潮下,众多企业建立了自己的知识库,以便于信息检索和知识查询。然而,随着知识库内容的膨胀,传统的...

字节跳动技术团队官方博客 2万+

抖音大型直播的画质优化实践

面临挑战随着抖音内容生态的不断丰富,越来越多的大型赛事在抖音平台进行直播,世界杯/春晚/亚运会等各项赛事节目引来大量用户观看。卡塔尔世界杯期间,抖音提供的稳定高质直播画面为观众带来了完美的观赛体验,决赛的 PCU 高达 3700W+。不同赛事节目涉及链路众多,且不同赛事之间存在差异,如何保障各链路的画质稳定并进一步提升画质,是一个巨大的挑战。如何应对挑战?画质优化链路大型赛事直播涉及链路较长,不同...

字节跳动技术团队官方博客 2万+

抖音直播新一代BVC编码器正式亮相

面临挑战在直播行业发展如火如荼的今天,用户对视频体验的要求也水涨船高。视频基础体验的关键要素包括清晰度、流畅度、低延迟等,而这些要素的“第一性原理”,就是视频本身的编码效率,也就是压缩率。视频编码是整个技术体系的基座,编码效率的显著提升,能够在同等码率下极大提高画质,从而改善用户体验。视频编码效率的重要性不言而喻,但进一步地提升也并非易事,尤其在直播场景中,对编码速度、延迟、码率控制等方面都有很高...

字节跳动技术团队官方博客 7356

Go Metrics SDK Tag 校验性能优化实践

背景Metrics SDK 是与字节内场时序数据库 ByteTSD 配套的用户指标打点 SDK,在字节内数十万服务中集成,应用广泛,因此 SDK 的性能优化是个重要和持续性的话题。本文主要以 Go Metrics SDK 为例,讲述对打点 API 的 hot-path 优化的实践。用户在使用 SDK API 进行打点时,需要传入指标对应的 Tag:tags:=[]m.T{{Name:"foo...

字节跳动技术团队官方博客 1万+

云上智能驾驶三维重建最佳实践

智能驾驶技术的不断发展,正在改变着我们的出行方式和交通系统。作为其中的一个关键技术,三维重建在智能驾驶系统中起着重要的作用。除去车端本身的感知、重建算法,自动驾驶技术的落地与发展需要庞大的云端重建能力支撑,火山引擎多媒体实验室通过行业领先的自研三维重建技术,结合强大的云平台资源与能力,助力相关技术在云端大规模重建、自动标注、真实感仿真等场景的落地与应用。本文重点介绍火山引擎多媒体实验室三维重建技术...

字节跳动技术团队官方博客 6305

火山引擎实时、低延时拥塞控制算法的优化实践

摘要火山引擎智能拥塞控制算法 VICC(Volcano Intelligent Congestion Control)是一种自适应的拥塞控制算法,旨在解决全球不同网络环境下,不同音视频应用对带宽利用率和延时的差异化要求。它结合了传统拥塞控制算法(如 GCC 和 BBR)的优点,并且能够根据不同的网络条件、业务偏好和码率特征进行自适应调整,包括自适应拥塞响应速度、自适应带宽探测幅度、自适应丢包检测策...

字节跳动技术团队官方博客 6735

veImageX 演进之路:Web 图片加载提速50%

背景说明火山引擎veImageX演进之路主要介绍了veImageX在字节内部从2012年随着字节成长过程中逐步演进的过程,演进中包括V1、V2、V3版本并最终面向行业输出;整个演进过程中包括服务端、客户端、网络库、业务场景与优化等多个角度介绍在图像处理压缩、省成本与体验优化的经验与方案;本篇文章重点介绍在web端演进和提供的能力,图片是 Web 站点中的重要元素,图片体积、格式、分辨率以及渲染方式...

字节跳动技术团队官方博客 6589

自研多模态追踪算法 PICO 为「手柄小型化」找到新思路

作者:张韬、林泽一 、闻超 、赵洋研发背景作为头戴的追踪配件,VR手柄可以通过HMD(头戴显示设备)的inside-out光学追踪定位原理,计算出手柄的空间运动轨迹,同时结合6轴传感器实现6DoF空间定位。与此同时,结合手柄控制器的物理按键、马达反馈、摇杆等,用户还能获得逼真、细腻的触觉反馈,进一步增强虚拟现实人机交互的能力以及沉浸感,这也是目前无手柄方案所难以实现的。目前主流VR手柄的追踪技术方...

字节跳动技术团队官方博客 7819

如何利用播放器节省20%点播成本

点播成本节省的点其实涉及诸多部分,例如:CDN、转码、存储等,而利用播放器降本却是很多客户比较陌生的部分。火山引擎基于内部支撑抖音集团相关业务的实践,播放器恰恰是成本优化中最重要和最为依赖的部分。火山引擎的视频团队做了份数据统计,在一个很经典的视频业务中,我们在2022年至2023年大约1年半的时间里,针对这个业务进行了33次成本优化点,其中13次是播放器主导的优化,其余的有12次也是需要播放器强...

字节跳动技术团队官方博客 6176

火山引擎 ByteHouse:ClickHouse 如何保证海量数据一致性

背景ClickHouse是一个开源的OLAP引擎,不仅被全球开发者广泛使用,在字节各个应用场景中也可以看到它的身影。基于高性能、分布式特点,ClickHouse可以满足大规模数据的分析和查询需求,因此字节研发团队以开源ClickHouse为基础,推出火山引擎云原生数据仓库ByteHouse。在日常工作中,研发人员经常会遇到业务链路过长,导致流程稳定性和数据一致性难保障的问题,这在分布式、跨服务的场...

字节跳动技术团队官方博客 6900

抖音集团都在用的画质评估工具,确定不试试吗?

导读本文从抖音集团内部画质评估体系的建设历程着笔,主要分享了画质评测对于业务的重要性、主要应用场景和内部产品的一些典型实践案例。通过分享业务视角遇到的一些问题和我们的解决思路,希望能抛砖引玉,为遇到类似困扰的伙伴们提供有价值的参考。画质评估体系建设历程为何评测画质如此重要?我们通过线上业务大量实验发现,图片画质优劣对点击率、 停留时长等消费类指标有正相关影响,间接影响用户收益指标。因此,建设一套行...

字节跳动技术团队官方博客 7940

VLDB 2023 | CDSBen: 字节跳动 veDB 数据库存储系统性能测试模型

背景随着业务爆炸式增长与云原生技术的日渐成熟,大量云原生分布式数据库产品如雨后春笋般涌现,其中一部分主打 OLTP 场景的分布式数据库强调的是从计算-存储分离架构获得弹性收益;对于业界各种计算-存储分离架构的数据库而言,怎么用真实的端到端数据库 workload 去 benchmark 其底层存储系统一直存在以下难题:对于数据库专用存储系统,不存在如 fio 一样的“事实标准” benchmark...

字节跳动技术团队官方博客 5896
上一篇: 数据质量动态探查及相关前端实现
下一篇: 字节跳动 Flink 状态查询实践与优化
 字节跳动技术团队
字节跳动技术团队 企业官方账号 企业官方账号
博客等级 码龄7年 8153粉丝 316原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值