Go 1.18 Generics

盼望着,盼望着,Go 的泛型……

  • 本来说 1.16,结果完全咕咕咕
  • 好不容易等到 1.17 扭扭捏捏的 -gcflags=-G=3 勉强开起来,还不能暴露到 package 外面
  • 今年二月份将要发布的 1.18,终于要正式支持了

真的是等了好久好久啊。

虽然现在 Go 1.18 还没有正式发布,不过已经发布了基本可用的 Beta 1,趁此机会可以来抢先体验下。

Try it out

首先需要一个正式版的 Go 环境(个人不是很建议直接装 Beta 版的 Go)。如果没有的话,自己装一个。

然后安装 Go 1.18 Beta 1

go install golang.org/dl/go1.18beta1@latest
go1.18beta1 download

默认情况下这个 SDK 会安装到 ~/sdk/go1.18beta1 下面。用 Goland(这里使用 2021.3.2,已经默认开启泛型相关特性支持)创建一个新的 Project,并选择使用这个 SDK:

准备一个最简单的项目感受一下泛型的舒适:

package main

import (
	"container/list"
	"fmt"
)

type Element[T any] struct {
	e *list.Element
}

func (e *Element[T]) Value() T {
	return e.e.Value.(T)
}

func (e *Element[T]) Prev() *Element[T] {
	prev := e.e.Prev()
	if prev == nil {
		return nil
	}
	return &Element[T]{e: prev}
}

func (e *Element[T]) Next() *Element[T] {
	next := e.e.Next()
	if next == nil {
		return nil
	}
	return &Element[T]{e: next}
}

type TypedList[T any] struct {
	l *list.List
}

func NewTypedList[T any]() TypedList[T] {
	return TypedList[T]{
		l: list.New(),
	}
}

func (t *TypedList[T]) PushFront(value T) *Element[T] {
	v := t.l.PushFront(value)
	return &Element[T]{e: v}
}

func (t *TypedList[T]) PushBack(value T) *Element[T] {
	v := t.l.PushBack(value)
	return &Element[T]{e: v}
}

func (t *TypedList[T]) Front() *Element[T] {
	front := t.l.Front()
	if front == nil {
		return nil
	}
	return &Element[T]{e: front}
}

func (t *TypedList[T]) Back() *Element[T] {
	back := t.l.Back()
	if back == nil {
		return nil
	}
	return &Element[T]{e: back}
}

func main() {
	a := NewTypedList[string]()
	a.PushFront("test")
	a.PushFront("go")
	a.PushFront("1.18")
	a.PushFront("generics")
	for i := a.Back(); i != nil; i = i.Prev() {
		fmt.Println(i.Value())
	}
}

直接运行:

> go1.18beta1 run .
test
go
1.18
generics

之后我们就直接使用这个环境开始试玩一下 Go 1.18 的泛型。需要注意的是,下面的部分 Demo 可能会在 Goland 中看到一些非预期的红色波浪线提示,这个是 Goland 自身对泛型支持还不完善导致的。

Why Generics

个人理解,泛型(比较基础的泛型)最大的好处在于:能够把运行时的类型检查提前到编译期完成,且可以为更进一步的编译期优化提供指导。

第一点应该是比较容易理解的。举个例子,大家天天都在用的猜测一个 interface{} 的类型:

package faulty_list

import (
	"container/list"
	"fmt"
)

func FaultyListDemo() {
	l := list.New()
	l.PushBack("1")
	l.PushBack(2)
	l.PushBack(3.5)
	for i := l.Front(); i != nil; i = i.Next() {
		fmt.Println("any", i.Value.(any))  // what type is i.Value ?
		if v, ok := i.Value.(string); ok { // checking type every time is quite annoying
			fmt.Println("if string", v)
		}
		fmt.Println("assertion without check", i.Value.(string)) // incorrect type assertion leads to panic
	}
}

同样运行这段代码:

> go1.18beta1 run .
any 1
if string 1
assertion without check 1
any 2
panic: interface conversion: interface {} is int, not string

goroutine 1 [running]:
github.com/yichya/generics_example/faulty_list.FaultyListDemo()
        /home/yichya/go/src/github.com/yichya/generics_example/faulty_list/faulty_list.go:18 +0x4ed
main.main()
        /home/yichya/go/src/github.com/yichya/generics_example/main.go:9 +0x17
exit status 2

而如果换为我们上面例子中的泛型版本,这个 List 在编译的时候就能够保证其中的元素类型统一:

package typed_list

import "fmt"

func TypedListDemo() {
	a := NewTypedList[string]()
	a.PushFront("test")
	a.PushFront("go")
	a.PushFront(1.18)  // incorrect type here
	a.PushFront("generics")
	for i := a.Back(); i != nil; i = i.Prev() {
		fmt.Println(i.Value())
	}
}

尝试编译运行,很快得到一个错误:

> go1.18beta1 run .
# github.com/yichya/generics_example/typed_list
typed_list/typed_list.go:9:14: cannot use 1.18 (untyped float constant) as string value in argument to a.PushFront

事实上 Goland 也会直接在这里标记出类型的不匹配(只是目前 Goland 的提示似乎还不完善)。

关于第二点【编译期优化】,这个我们会在下面的【运行时与反射】和【性能测试】中简单举一个例子。

类型参数

Go 1.18 中引入的泛型,语法相当特别的使用了方括号 [] 而不是其他大多数语言中使用的尖括号 <>,据说是为了让编译器更好写

定义类型

定义类型的时候可以增加一个参数(名字不重要),用于指代满足某一要求的类型(这里用到的 comparable 下面会简单介绍)

type Set[T1 comparable] struct {
	m map[T1]bool
}

也可以直接在定义函数的时候增加一个参数,比如实现一个 min()(这里用到的 constraints.Ordered 下面会简单介绍)

package min

import (
	"constraints"
	"fmt"
)

func Min[T constraints.Ordered](v0 T, v ...T) T {
	var minT = v0
	for _, x := range v {
		if x < minT {
			minT = x
		}
	}
	return minT
}

func MinT[T constraints.Ordered](v ...T) T {
	var minT = v[0]
	for _, x := range v {
		if x < minT {
			minT = x
		}
	}
	return minT
}

func MinI(v interface{}) interface{} {
	switch vi := v.(type) {
	case []int64:
		{
			return MinT(vi...)
		}
		// other types in constraints.Ordered ...
	}
	// runtime panic for unacceptable type here
	panic("unacceptable slice type")
}

func MinTest() {
	fmt.Println(Min(9, 8, 7, 6, 1, 2, 3, 4, 5) + 10) // 11
	a := []int64{9, 8, 7, 6, 1, 2, 3, 4, 5}
	fmt.Println(MinT(a...) + 11)      // 12
	fmt.Println(MinI(a).(int64) + 12) // 13. type of return value must be asserted

	fmt.Println(Min(9.5, 8.5, 7.5, 6.5, 1.5, 2.5, 3.5, 4.5, 5.5)) // 1.5
	b := []float64{9.5, 8.5, 7.5, 6.5, 1.5, 2.5, 3.5, 4.5, 5.5}
	fmt.Println(MinI(b)) // panic: unacceptable slice type
}

这里的好处在于,比起将入参定义为 interface{},此处可以避免没有必要的 type assertion,同时也可以避免 runtime panic

使用多个类型参数

使用类型参数的地方可以同时放多个参数,比如 Pair(官方是给出了 golang.org/x/exp/maps 这个库实现了 map 的 Items() 方法的)

package typed_pair

import "fmt"

type Pair[T1 comparable, T2 any] struct {
	first  T1
	second T2
}

type Map[T1 comparable, T2 any] struct {
	m map[T1]T2
}

func (m *Map[T1, T2]) Set(k T1, v T2) {
	if m.m == nil {
		m.m = map[T1]T2{}
	}
	m.m[k] = v
}

// Items Python 2 Like items() method (unordered as well)
func (m *Map[T1, T2]) Items() []Pair[T1, T2] {
	l := make([]Pair[T1, T2], 0, len(m.m))
	for k, v := range m.m {
		l = append(l, Pair[T1, T2]{first: k, second: v})
	}
	return l
}

func TypedPairDemo() {
	m := Map[string, int]{}
	m.Set("123", 123)
	m.Set("456", 456)
	for _, item := range m.Items() { // so pythonic isn't it ?
		fmt.Println(item.first, item.second)
	}
}

编译运行,得到的结果相当符合预期:

> go1.18beta1 run .
456 456
123 123

当然就 Go 本身的实现来说这里的实现是相当基础的,比起很多类型系统更强的语言(比如 TypeScript 或者 C#)来说还是有明显的能力限制,比如没有 variadic type parameters,想实现 Currying 或者 Partial 就是一个相当难受的事情(当然比起完全没有泛型来说还是要好太多太多了):

package typed_currying

import "fmt"

func PartialT2R1V2[T1, T2, R1 any](f func(t1 T1, t2 T2) R1, v1 T1, v2 T2) func() R1 {
	return func() R1 {
		return f(v1, v2)
	}
}

func PartialT3R1V1[T1, T2, T3, R1 any](f func(t1 T1, t2 T2, t3 T3) R1, v1 T1) func(v2 T2, v3 T3) R1 {
	return func(v2 T2, v3 T3) R1 {
		return f(v1, v2, v3)
	}
}

func CurryingT3R1[T1, T2, T3, R1 any](f func(t1 T1, t2 T2, t3 T3) R1) func(v1 T1) func(v2 T2) func(v3 T3) R1 {
	return func(v1 T1) func(v2 T2) func(v3 T3) R1 {
		return func(v2 T2) func(v3 T3) R1 {
			return func(v3 T3) R1 {
				return f(v1, v2, v3)
			}
		}
	}
}

func add3(i int8, j int16, k int32) int64 {
	return int64(i) + int64(j) + int64(k)
}

func CurryingDemo() {
	c1 := PartialT3R1V1[int8, int16, int32, int64](add3, 5) // you have to manually pick your PartialTxRyVz function here
	fmt.Println(c1(6, 7))                                   // 18
	c2 := PartialT2R1V2(c1, 6, 7)                           // type arguments can be omitted
	fmt.Println(c2())                                       // 18
	f := CurryingT3R1(add3)                                 // currying
	fmt.Println(f(5)(6)(7))                                 // 18
}

而且即使在使用的时候也很不方便(比如上面的 PartialTxRyVz 如果全都改名为 Partial 的话是编译不过去的)

> go1.18beta1 run .
# github.com/yichya/generics_example/typed_currying
typed_currying/implementation.go:11:6: Partial redeclared in this block
        /home/yichya/go/src/github.com/yichya/generics_example/typed_currying/implementation.go:5:6: other declaration of Partial
typed_currying/implementation.go:17:6: Partial redeclared in this block
        /home/yichya/go/src/github.com/yichya/generics_example/typed_currying/implementation.go:5:6: other declaration of Partial
typed_currying/implementation.go:28:27: got 4 type arguments but want 2

类型约束

Go 1.18 中的类型约束是复用了 interface 关键字来进行描述,但跟 interface 关系并不大。下面看几个例子。

内置约束:any 与 comparable

Go 语言的 runtime 内置了两个类型约束 anycomparable

any

any 事实上就是 interface{},被内置大概是为了图省事

// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}

Go 1.18 的 runtime 和标准库中把很多 interface{} 都换成了 any,Goland 经常一脸懵逼。

comparable

comparable 代表一类实现了 ==!= 两个运算符的内置类型,包括下面这些

// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, interfaces,
// arrays of comparable types, structs whose fields are all comparable types).

演示 comparable 最好的工具当然是自己搓一个 Set

package typed_set

import "fmt"

type Set[T comparable] struct {
	m map[T]bool
}

func (s *Set[T]) Add(value T) {
	if s.m == nil {
		s.m = map[T]bool{}
	}
	s.m[value] = true
}

func (s *Set[T]) List() []T {
	t := make([]T, 0, len(s.m))
	for i, v := range s.m {
		if v {
			t = append(t, i)
		}
	}
	return t
}

func TypedSetDemo() {
	s := Set[string]{}
	s.Add("1")
	s.Add("2")
	s.Add("1")
	s.Add("3")
	s.Add("4")
	l := s.List()
	for _, x := range l {
		fmt.Println(x)
	}
}

编译运行:

> go1.18beta1 run .
4
1
2
3

comparable 被内置,个人理解是因为 Map 的 Key 被要求是 comparable,比如把 Set[T]T 的约束写为 any 的话就会报这样的错误:

> go1.18beta1 run .
# github.com/yichya/generics_example/typed_set
typed_set/implementation.go:6:8: invalid map key type T (missing comparable constraint)
typed_set/implementation.go:11:13: invalid map key type T (missing comparable constraint)

定义约束

定义约束的语法很简单:使用 interface 并在其中填写可以使用的类型(多个类型用 | 连接)。

package define_constraints

import "fmt"

type MustSigned interface {
	int | int8 | int16 | int32 | int64
}

type UnderlyingMustSigned interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

type SomeEnum int64

const (
	SomeEnum_ONE SomeEnum = 1
	SomeEnum_TWO SomeEnum = 2
)

func MustOne[T MustSigned](v T) bool {
	return v == 1
}

func UnderlyingMustTwo[T UnderlyingMustSigned](v T) bool {
	return v == 2
}

func DefineConstraintsDemo() {
	fmt.Println(MustOne(SomeEnum_ONE))
	fmt.Println(UnderlyingMustTwo(SomeEnum_TWO))
}

符号 ~ 代表 underlying type(Types),比如上面的代码,MustOne() 使用的约束 MustSigned 没有 ~,编译会报错:

> go1.18beta1 run .
# github.com/yichya/generics_example/define_constraints
define_constraints/implementation.go:29:21: SomeEnum does not implement MustSigned (possibly missing ~ for int64 in constraint MustSigned)

但是在定义约束的时候存在一个很大的限制:因为 Go 目前还不支持实现 Operator(General notes on type sets),因此比如想针对某一自定义类型实现 Less,就会遇到阻碍:

package min_comparable_interface

import (
	"constraints"
	"fmt"
	"time"
)

type ordered interface {
	Less(other ordered) bool
}

type Ordered interface {
	constraints.Ordered | ordered
}

func MinT[T Ordered](v ...T) T {
	var minT = v[0]
	for _, x := range v {
		if ox, ok := x.(ordered); ok { // although Go generics uses a syntax which looks like interface, it is actually not
			if ox.Less(minT.(ordered)) {
				minT = x
			}
		} else {
			if x < minT {
				minT = x
			}
		}
	}
	return minT
}

type OrderedTime struct {
	t time.Time
}

func (d *OrderedTime) Less(other *OrderedTime) bool {
	return d.t.Before(other.t)
}

func (d *OrderedTime) Time() time.Time {
	return d.t
}

func MinTest() {
	var O1 = []int64{9, 8, 7, 6, 1, 2, 3, 4, 5}
	fmt.Println(MinT(O1...))

	var O2 = []*OrderedTime{
		{
			t: time.Date(2022, 1, 20, 0, 0, 0, 0, time.UTC),
		},
		{
			t: time.Date(2022, 1, 10, 0, 0, 0, 0, time.UTC),
		},
		{
			t: time.Date(2022, 1, 30, 0, 0, 0, 0, time.UTC),
		},
	}
	fmt.Println(MinT(O2...).Time())
}

编译的时候会得到如下错误,原因见 Permitting constraints as ordinary interface types

> go1.18beta1 run .
# github.com/yichya/generics_example/min_comparable_interface
min_comparable_interface/implementation.go:14:24: cannot use github.com/yichya/generics_example/min_comparable_interface.ordered in union (interface contains methods)
min_comparable_interface/implementation.go:20:16: invalid operation: cannot use type assertion on type parameter value x (variable of type T constrained by Ordered)
min_comparable_interface/implementation.go:21:15: invalid operation: cannot use type assertion on type parameter value minT (variable of type T constrained by Ordered)
min_comparable_interface/implementation.go:25:7: invalid operation: cannot compare x < minT (operator < not defined on T)

这个限制可以说极大程度上限制了目前 Go 泛型的使用场景,希望这项功能可以尽快加入到后续的 Go 版本中。

标准库中的 constraints 包

Go 1.18 自带的 constraints 包提供了一些内置约束。

  • Signed = ~int | ~int8 | ~int16 | ~int32 | ~int64
  • Unsigned = ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
  • Integer = Signed | Unsigned
  • Float = ~float32 | ~float64
  • Complex = ~complex64 | ~complex128
  • Ordered = Integer | Float | ~string

造一些常见的轮子

下面举一些相对复杂的例子来说明泛型能在哪些地方发挥作用。

Iterable

大多数语言(比如 Python 和 C#)中都很常见的概念。

package iterator

import "fmt"

type Iterator[T any] interface {
	Value() T
	Next() Iterator[T]
}

type ST[T any] []T

type sliceIterator[T any] struct {
	s   ST[T]
	pos int
}

func (i *sliceIterator[T]) Value() T {
	return i.s[i.pos]
}

func (i *sliceIterator[T]) Next() Iterator[T] {
	if i.pos+1 < len(i.s) {
		return &sliceIterator[T]{
			pos: i.pos + 1,
			s:   i.s,
		}
	}
	return nil
}

func (s ST[T]) ToIterator() Iterator[T] {
	return &sliceIterator[T]{
		pos: 0,
		s:   s,
	}
}

func ToSlice[T any](i Iterator[T]) []T {
	var t []T
	for v := i; v != nil; v = v.Next() {
		t = append(t, v.Value())
	}
	return t
}

func IteratorDemo() {
	d := []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	st := ST[int64](d)
	i := st.ToIterator()
	fmt.Println(ToSlice(i)) // [1 2 3 4 5 6 7 8 9 10]
}

Future

常见于 C#(Task<T>)、JavaScript(Promise)、Python(concurrent.Future)等,尤其多见于使用 async / await 这样的异步模型的语言。

package future

import (
	"fmt"
	"golang.org/x/sync/errgroup"
	"runtime"
	"sync"
	"time"
)

type Task[T any] struct {
	sync.RWMutex
	f      func() T
	done   bool
	result T
}

func (t *Task[T]) Start() {
	go func(self *Task[T]) {
		r := self.f()
		self.Lock()
		self.result = r
		self.done = true
		self.Unlock()
	}(t)
}

func (t *Task[T]) Done() bool {
	t.RLock()
	defer t.RUnlock()
	return t.done
}

func (t *Task[T]) Await() T {
	for {
		t.RLock()
		if t.done {
			t.RUnlock()
			return t.result
		}
		t.RUnlock()
		runtime.Gosched()
	}
}

func NewTask[T any](f func() T) *Task[T] {
	return &Task[T]{
		f: f,
	}
}

// All quite limited implementation of Javascript Promise.All or C# Task.WaitAll
func All[T any](f ...func() T) *Task[[]T] {
	resp := NewTask(func() []T { // NewTask[[]T]()
		r := make([]T, len(f))
		var eg errgroup.Group
		for index, task := range f {
			indexClosure := index
			taskClosure := task
			eg.Go(func() error {
				r[indexClosure] = taskClosure()
				return nil
			})
		}
		_ = eg.Wait()
		return r
	})
	return resp
}

func TaskTest() {
	t := NewTask(func() int64 {
		time.Sleep(3 * time.Second)
		return 10
	})
	t.Start()
	fmt.Println(t.Done()) // false
	time.Sleep(time.Second)
	fmt.Println(t.Done()) // false
	r := t.Await()
	fmt.Println(t.Done()) // true
	fmt.Println(r + 3)    // 13

	all := All(func() int64 {
		time.Sleep(3 * time.Second)
		return 3
	}, func() int64 {
		time.Sleep(4 * time.Second)
		return 4
	}, func() int64 {
		time.Sleep(5 * time.Second)
		return 5
	})
	all.Start()
	fmt.Println(all.Await()) // [3 4 5]
}

ORM

Gorm 中大量方法都使用 interface{} 作为参数,这导致很多问题都被延后到运行时发现,甚至可能会导致线上的 panic;本身 ORM 也是最适合使用泛型的场景之一。

package typed_gorm

import (
	"fmt"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type TypedGormDB[T any] struct {
	db *gorm.DB
}

func NewTypedGormDB[T any](txIn *gorm.DB) *TypedGormDB[T] {
	var tm T
	return &TypedGormDB[T]{
		db: txIn.Model(tm),
	}
}

// First simplified and typed implementation of (*gorm.DB).First
func (t *TypedGormDB[T]) First(dest *T) *TypedGormDB[T] {
	t.db = t.db.First(dest)
	return t
}

// Find simplified and typed implementation of (*gorm.DB).Find
func (t *TypedGormDB[T]) Find(dest *[]*T) *TypedGormDB[T] {
	t.db = t.db.Find(dest)
	return t
}

// Where simplified implementation of (*gorm.DB).Where
// however binding conditions to query would require Dependent Type support in the language
func (t *TypedGormDB[T]) Where(query string, conds ...any) *TypedGormDB[T] {
	t.db = t.db.Where(query, conds...)
	return t
}

// Create typed (*gorm.DB).Create
func (t *TypedGormDB[T]) Create(v *T) *TypedGormDB[T] {
	t.db = t.db.Create(v)
	return t
}

// Error return (*gorm.DB).Error
func (t *TypedGormDB[T]) Error() error {
	return t.db.Error
}

// AutoMigrate just for creating table in memory sqlite database
func (t *TypedGormDB[T]) AutoMigrate() error {
	var tm T
	return t.db.AutoMigrate(tm)
}

type Model struct {
	Id   uint64 `gorm:"column:id"`
	Name string `gorm:"column:name"`
}

func (Model) TableName() string {
	return "model"
}

func TypedGormDemo() {
	db, e0 := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
	if e0 != nil {
		panic(e0)
	}

	var result []*Model
	td := NewTypedGormDB[Model](db)

	if e1 := td.AutoMigrate(); e1 != nil {
		panic(e1)
	}

	m := []*Model{
		{
			Id:   1,
			Name: "test1",
		},
		{
			Id:   2,
			Name: "test2",
		},
		{
			Id:   3,
			Name: "test3",
		},
	}
	for _, mi := range m {
		td.Create(mi)
	}

	if e2 := td.Where("id in (?)", []int64{2, 3}).Find(&result).Error(); e2 != nil {
		panic(e2)
	}

	fmt.Println(result[0].Name, result[1].Name) // test2 test3
}

Functional Programming

有了泛型,可以很方便的实现一些之前比较难实现的函数式写法。

package functional_common

import "fmt"

func Map[T1, T2 any](s []T1, f func(T1) T2) []T2 {
	r := make([]T2, len(s))
	for i, v := range s {
		r[i] = f(v)
	}
	return r
}

func Reduce[T1, T2 any](s []T1, initializer T2, f func(T2, T1) T2) T2 {
	r := initializer
	for _, v := range s {
		r = f(r, v)
	}
	return r
}

func Filter[T any](s []T, f func(T) bool) []T {
	var r []T
	for _, v := range s {
		if f(v) {
			r = append(r, v)
		}
	}
	return r
}

func FunctionalCommonDemo() {
	fmt.Println(Reduce(Map(Filter([]int32{1, 2, 3, 4, 5}, func(t int32) bool {
		return t < 4 // [1, 2, 3]
	}), func(t int32) int64 {
		return int64(t) + 1 // [2, 3, 4]
	}), int64(0), func(t1, t2 int64) int64 {
		return t1 + t2 // 2 + 3 == 5; 5 + 4 = 9
	})) // 9
}

运行时与反射

由于不像 Java 那样有在 .class 层面的兼容性要求,Go 的运行时并不像 Java 那样对类型信息做了擦除,因此可以在运行时通过反射拿到完整的类型信息。可以很简单进行验证:

package reflection

import (
	"fmt"
	"reflect"
)

func PrintTypeOf[T1, T2 any](v1 T1, v2 T2) {
	fmt.Printf("%T %T %v\n", v1, v2, reflect.TypeOf(v1) == reflect.TypeOf(v2))
}

func ReflectionDemo() {
	a := []int64{1, 2, 3}
	b := []int32{4, 5}
	c := []int64{6}
	PrintTypeOf(a, b) // []int64 []int32 false
	PrintTypeOf(a, c) // []int64 []int64 true
}

更详细的内容,可以参考 https://github.com/akutz/go-generics-the-hard-way/tree/main/05-internals

性能测试

做一下测试:简简单单的做一个给 Slice 里面所有元素 +3,观察一下使用泛型的方式对性能的影响。

Baseline

很直接。

package performance

func Add3Int32(s []int32) []int32 {
	for index := range s {
		s[index] += 3
	}
	return s
}

func Add3Int64(s []int64) []int64 {
	for index := range s {
		s[index] += 3
	}
	return s
}

Using Interface & Interface Slice

做了两种实现:一种是比较简单的,判断一下传入的 interface{} 的类型是不是 []int64 或者 []int32;另一种则是传入一个 []interface{},并逐一判断其中每个元素。

事实上第二种会更加常见:我们希望入参至少能明确是一个 Slice,但这种情况就不得不将入参和出参都做一次复制,转换为 []interface{},无形中带来了很多性能损失;而且这种代码写起来也让人十分烦躁。

package performance

func Add3Interface(s interface{}) interface{} {
	if sint64, ok := s.([]int64); ok {
		return Add3Int64(sint64)
	}
	if sint32, ok := s.([]int32); ok {
		return Add3Int32(sint32)
	}
	if sinterface, ok := s.([]interface{}); ok {
		return Add3InterfaceSlice(sinterface)
	}
	panic("unsupported slice type")
}

func Add3InterfaceSlice(s []interface{}) []interface{} {
	for index, value := range s {
		switch vi := value.(type) {
		case int32:
			{
				s[index] = vi + 3
			}
		case int64:
			{
				s[index] = vi + 3
			}
		default:
			{
				panic("unsupported slice element type")
			}
		}
	}
	return s
}

Using Generics

使用泛型解决这一问题可以说非常优雅简洁。

package performance

type SupportedIntegers interface {
	~int64 | ~int32
}

func Add3Generics[T SupportedIntegers](s []T) []T {
	for index := range s {
		s[index] += 3
	}
	return s
}

Benchmark and Results

使用以下代码做 Benchmark

package performance

import (
	"testing"
)

func BenchmarkBaseline(t *testing.B) {
	// baseline
	si32 := []int32{1, 2, 3}
	si64 := []int64{4, 5, 6}
	for i := 0; i < t.N; i++ {
		Add3Int32(si32)
		Add3Int64(si64)
	}
}

func BenchmarkInterface(t *testing.B) {
	// using interface
	si32 := []int32{1, 2, 3}
	si64 := []int64{4, 5, 6}
	for i := 0; i < t.N; i++ {
		Add3Interface(si32)
		Add3Interface(si64)
	}
}

func BenchmarkSliceInterface(t *testing.B) {
	// using slice of interface
	sinterface1 := []interface{}{int32(1), int32(2), int64(3)}
	sinterface2 := []interface{}{int64(4), int64(5), int64(6)}
	for i := 0; i < t.N; i++ {
		Add3InterfaceSlice(sinterface1)
		Add3InterfaceSlice(sinterface2)
	}
}

func BenchmarkGenerics(t *testing.B) {
	// using generics
	si32 := []int32{1, 2, 3}
	si64 := []int64{4, 5, 6}
	for i := 0; i < t.N; i++ {
		Add3Generics(si32)
		Add3Generics(si64)
	}
}

结果如下:

> go1.18beta1 test -bench . -benchmem
goos: linux
goarch: amd64
pkg: github.com/yichya/generics_example/performance
cpu: Intel(R) Core(TM) i7-6770HQ CPU @ 2.60GHz
BenchmarkBaseline-6             143243968                8.260 ns/op           0 B/op          0 allocs/op
BenchmarkInterface-6            15730483                77.61 ns/op           48 B/op          2 allocs/op
BenchmarkSliceInterface-6       11284230               101.2 ns/op            39 B/op          5 allocs/op
BenchmarkGenerics-6             145107482                8.316 ns/op           0 B/op          0 allocs/op
PASS
ok      github.com/yichya/generics_example/performance  6.645s

可以看到,由于不涉及 Boxing,不需要做内存分配,Baseline 的性能最佳;使用泛型的性能与 Baseline 基本一致;而 interface{}[]interface{} 的性能都明显比较不理想。

Next

Go 1.18 目前仅在语法层面上支持了比较初级的泛型,但终于算是一个从无到有的突破。Go 的泛型草案中已经提到了一些目前不支持但已经提上日程的特性:

个人觉得类型系统是一个编程语言最重要的组成部分,它可以决定一个语言的表达能力的上限。编程语言的类型系统为何如此重要?

目前工业界上崭露头角的比较新的语言,大多在类型系统上都下了一番功夫(像 Go 这种 2022 年才终于憋出个泛型的,已经可以说是比较拉了)。比如 TypeScript 和 Rust 这样现代编程语言中在类型系统上走在前面的语言,最近已经得到了越来越多的关注:TypeScript 的火热程度已是有目共睹,Rust 亦在最近成为第二种被接纳进 Linux 内核的语言,可以说前途无量;传统一些的语言比如 C#、C++ 也在逐渐引入更多概念用来表达更复杂的类型。更靠近学术界的语言,强大的类型系统几乎可以说是标配,其中的代表比如 Haskell 甚至 Idris 这样的函数式语言,甚至支持包括 Dependent Type 等一系列高级特性。或许未来某一天,足够强大的类型系统将成为工业界语言的标配,或许那时的码农们可以不用再为了 panic: interface conversion 掉头发。

如果像更深入了解类型系统相关,可以先从 TypeScript 学起。TypeScript 的类型系统非常优秀,很适合用来打开新世界的大门。

Reference

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

https://bignerdranch.com/blog/exploring-go-v1-18s-generics/

https://github.com/mattn/go-generics-example

https://www.reddit.com/r/golang/comments/pj4va2/never_felt_need_for_generics_did_you/

Comments