vlambda博客
学习文章列表

Go语言1.18发布,通过范型能做什么?

Go语言1.18发布已经快两个月了,与历史版本相比,最大的变更是支持了范型。废话不多说,我们通过几个例子看看通过范型能做什么,不能做什么。

创建指针

平时写 RPC 服务时,thrift/protobuf 文件里设置字段为 optional 时,生成的go struct里以指针的方式存在。使用时通常需要将一个变量转化成指针类型,为了让这个操作更方便,我们会写一些辅助函数:

package ptr

func NewString(s string) *string {
return &s
}

func NewInt64(s int64) *int64 {
return &s
}

func NewInt32(s int32) *int32 {
return &s
}

func NewInt(s int) *int {
return &s
}

// 所有类型都要遍历一遍

一段代码要copy+paste+修改十多次,写一段脚本去生成这段代码也得不偿失,看起来也不优雅。如果我们用范型去实现,就可以这样写:

package ptr

func New[T any](s T) *T {
return &s
}

代码十分简洁,使用时也不需要考虑入参类型,十分完美。

简化 if-else/switch-case

Go语言对大括号 {} 有强制要求,导致代码特别长。比如这样一段逻辑:

var ret Type
if condition {
ret = X;
} else {
ret = Y;
}

C++ 可以通过 "?:" 来实现:

Type ret = condition ? X : y

Spark/Clickhouse SQL 可以这样写:

IF(condition, X, Y) as ret

Python 可以这样写:

ret = X if condition else Y

为了将五行代码缩减成一行,我们可以利用范型实现类似于 SQL IF 的逻辑:

// If returns trueVal if isTrue, else return falseVal
func If[T any](cond bool, trueVal, falseVal T) T {
if isTrue {
return trueVal
} else {
return falseVal
}
}

将一个 condition,扩展到两个 condition,可以这样写:

// MultiIf 支持传两个condition
func MultiIf[T any](cond1 bool, ret1 T, cond2 bool, ret2 T, defaultRet T) T {
if cond1 {
return ret1
} else if cond2 {
return ret2
} else {
return defaultRet
}
}

// MultiIf3 支持传三个个condition
func MultiIf3[T any](cond1 bool, ret1 T, cond2 bool, ret2 T, cond3 bool, ret3 T, defaultRet T) T {
if cond1 {
return ret1
} else if cond2 {
return ret2
} else if cond3 {
return ret3
} else {
return defaultRet
}
}

基本运算符

经常做数据处理的代码中,我们可能需要做大量的加减乘除,对于结果为零的情况也要做容错处理。为了减少代码中充斥的类型转换,写了这些工具函数:

import "math"

// SafeDivideInt64Int64 handles by zero case
func SafeDivideInt64Int64(x, y int64) float64 {
if y == 0 {
return 0.0
}
return float64(x) / float64(y)
}

// SafeDivideFloat64Int64 handles by zero case
func SafeDivideFloat64Int64(x float64, y int64) float64 {
if y == 0 {
return 0.0
}
return float64(x) / float64(y)
}

// SafeDivideFloat64Float64 handles by zero case
func SafeDivideFloat64Float64(x float64, y float64) float64 {
if y == 0 {
return 0.0
}
return x / y
}

// SafeDivideXxxYyy ...

使用范型优化后,代码量大大减少:

func SafeDivide[K int64 | float64, V int64 | float64](y K, x V) float64 {
y1 := float64(y)
x1 := float64(x)
if x1 == 0 {
return 0
}
return y1 / x1
}

slice/map对象化

Scala/Rust 等现代编程语言都已经同时支持面向对象编程和函数式编程,在使用string/array/map 等数据类型时,可以调用 map/foreach/filter 等方法,而不是使用 for-loop 写代码。但 Go语言的 string/slice/map 还都非常原始,需要通过strings/sort/slice 等库通过函数调用来实现这些功能。

为了支持方法调用,我们可以对slice做一层包装:

func (xs Int64Slice) Size() int {
return len(xs)
}

func (xs Int64Slice) Exists(e int64) bool {
for _, x := range xs {
if x == e {
return true
}
}
return false
}

func (xs Int64Slice) Filter(fn func(s int64) bool) Int64Slice {
var output Int64Slice
for _, x := range xs {
if fn(x) {
output = append(output, x)
}
}
return output
}
func (xs Int64Slice) Deduplicate() Int64Slice
func (xs Int64Slice) Intersect(ys Int64Slice) Int64Slice
func (xs Int64Slice) Union(ys Int64Slice) Int64Slice
func (xs Int64Slice) Diff(ys Int64Slice) Int64Slice
// ...

使用时,仅仅通过类型转换,就可以使用这些方法了:

arr1 := []int64{1,2,3}
isIn := Int64Slice(arr1).Exists(int64(1))
fmt.Printf("isIn = %v\n", isIn) // isIn = true

支持多个类型

为了让代码支持多个类型,我们还要写 IntSlice, Int32Slice, StringSlice, BoolSlice 等等。但支持范型以后,情况就好转了不少 (slice/generics.go):

package slice

type Slice[T comparable] []T

func (xs Slice[T]) Size() int {
return len(xs)
}

func (xs Slice[T]) Exists(e T) bool {
for _, x := range xs {
if x == e {
return true
}
}
return false
}

func (xs Slice[T]) Filter(fn func(elem T) bool) Slice[T] {
var output Slice[T]
for _, x := range xs {
if fn(x) {
output = append(output, x)
}
}
return output
}

func (xs Slice[T]) Deduplicate() Slice[T] {
dict := make(map[T]struct{}, len(xs))
for _, x := range xs {
dict[x] = struct{}{}
}
var output Slice[T]
for k, _ := range dict {
output = append(output, k)
}
return output
}

// ...

为了验证这个,我们再写一段单元测试 (slice/generics_test.go):

package slice

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestSlice_Filter(t *testing.T) {
input := []int64{1, 2, 3}
s := Slice[int64](input)
  assert.True(t, s.Exists(1))
}

// bash: go test -v ./...

局限性

前面我们提到过 map 方法,我们可以将一个操作应用到数组的每一个元素上面,产生一个新数组,我们可以这样定义:

type Slice[T comparable] []T

func (xs Slice[T]) Map[V any](fn func(e T) V) Slice[V] {
var ret []V
for _, x := range xs {
ret = append(ret, fn(x))
}
return Slice[T](ret)
}

这时候 IDE 会告诉我们: Method cannot have type parameters,翻译过来就是普通函数支持范型,struct 的方法不支持范型。


这样的结果是map/reduce/fold/foldLeft 等方法都不能使用。下图是 Scala trait TraversableOnce 的方法,供大家参考:



虽然Go语言对范型的支持并不完善,但比之前已经进步太多。如果说下一个feature是什么,我希望是对宏(macro) 的支持,这样就不用疯狂地写 if err != nil { return xxx} 了