惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
N
News and Events Feed by Topic
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
The Cloudflare Blog
H
Heimdal Security Blog
Schneier on Security
Schneier on Security
Engineering at Meta
Engineering at Meta
Google Online Security Blog
Google Online Security Blog
宝玉的分享
宝玉的分享
AI
AI
The GitHub Blog
The GitHub Blog
MongoDB | Blog
MongoDB | Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
The Last Watchdog
The Last Watchdog
T
Troy Hunt's Blog
S
Security @ Cisco Blogs
H
Hacker News: Front Page
F
Fortinet All Blogs
博客园_首页
S
Secure Thoughts
N
News and Events Feed by Topic
P
Proofpoint News Feed
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
Spread Privacy
Spread Privacy
Hacker News - Newest:
Hacker News - Newest: "LLM"
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Hugging Face - Blog
Hugging Face - Blog
Hacker News: Ask HN
Hacker News: Ask HN
C
CXSECURITY Database RSS Feed - CXSecurity.com
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
L
LINUX DO - 最新话题
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Schneier on Security
Know Your Adversary
Know Your Adversary
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Scott Helme
Scott Helme
P
Privacy & Cybersecurity Law Blog
S
Securelist
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
O
OpenAI News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
PCI Perspectives
PCI Perspectives
L
LangChain Blog
雷峰网
雷峰网
Security Archives - TechRepublic
Security Archives - TechRepublic
V2EX - 技术
V2EX - 技术

ipfans's Blog

程序员的提示工程手册 AI 产品的破局之道:以人为本 使用 Go 开发 AI Agent的选择:Genkit for Go RAG 技术在实际工程中的应用:OpenAI 的最佳实践分享 从零学习 Hypothetical Document Embeddings (HyDE) - 2 从零学习 Hypothetical Document Embeddings (HyDE) - 1 No GIL Python 的冒险 使用 Ollama 快速部署本地开源大语言模型 使用子解释器运行Python并行应用 Twirp初相识 Twirp基本概念:Hooks和Interceptors 去年的一点小工作(1):从BFF谈起 Byebye 2022, Hello 2023 给hugo添加mermaid支持 一些实用工具列表 Go Web应用中常见的反模式 什么是事件建模Event Modeling? 构建属于你自己的dapr绑定组件 构建属于你自己的dapr服务发现
Go 1.17 泛型尝鲜
2021-08-18 · via ipfans's Blog

Featured image of post Go 1.17 泛型尝鲜

今天,Go的1.17版本终于正式发布,除了带来各种优化和新功能外,1.17正式在程序中提供了尝鲜的泛型支持,这一功能也是为1.18版本泛型正式实装做铺垫。意味着在6个月后,我们就可以正式使用泛型开发了。那在Go 1.18正式实装之前,我们在1.17版本中先尝鲜一下泛型的支持吧。

泛型有什么作用?

在使用Go没有泛型之前我们怎么实现针对多类型的逻辑实现的呢?有很多方法,比如说使用interface{}作为变量类型参数,在内部通过类型判断进入对应的处理逻辑;将类型转化为特定表现的鸭子类型,通过接口定义的方法实现逻辑整合;还有人专门编写了Go的函数代码生成工具,通过批量生成不同类型的相同实现函数代替手工实现等等。这些方法多多少少存在一些问题:使用了interface{}作为参数意味着放弃了编译时检查,作为强类型语言的一个优势就被抹掉了。同样,无论使用代码生成还是手工书写,一旦出现问题,意味着这些方法都需要重复生成或者进行批量修改,工作量反而变得更多了。

在Go中引入泛型会给程序开发带来很多好处:通过泛型,可以针对多种类型编写一次代码,大大节省了编码时间。你可以充分应用编译器的编译检查,保证程序变量类型的可靠性。借助泛型,你可以减少代码的重复度,也不会出现一处出现问题需要修改多处地方的尴尬问题。这也让很多测试工作变得更简单,借助类型安全,你甚至可以少考虑很多的边缘情况。

Go语言官方有详细的泛型提案文档可以在这里这里查看详情。

如何使用泛型

前面理论我们仅仅只做介绍,这次尝鲜还是以实践为主。让我们先从一个小例子开始。

从简单的例子开始

让我们先从一个最简单的例子开始:

package main

import (
    "fmt"
)

type Addable interface {
	type int, int8, int16, int32, int64,
		uint, uint8, uint16, uint32, uint64, uintptr,
		float32, float64, complex64, complex128,
		string
}

func add[T Addable](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(add(1,2))
    fmt.Println(add("1", "2"))
}

这个函数可以实现任何需要使用+符号进行运算的类型,我们通过定义Addable类型,枚举了所有可能可以使用add方法的所有的类型。比如我们在main函数中就使用了intstring两种不同类型。

但是如果这时我们使用简单的go run命令运行,会发现提示语法错误:

$ go version
go version go1.17 darwin/arm64
$ go run ~/main.go
# command-line-arguments
../main.go:8:2: syntax error: unexpected type, expecting method or interface name
../main.go:15:6: missing function body
../main.go:15:9: syntax error: unexpected [, expecting (

因为在Go 1.17中,泛型并未默认开启,你需要定义gcflags方式启用泛型:

$ go run -gcflags=-G=3 ~/main.go
3
12

如果你觉得这种方式太过于复杂,每次都需要添加,也可以通过定义环境变量形式让每次都带此参数(不推荐,尤其是多版本环境时低版本Go中会报错):

$ export GOFLAGS="-gcflags=-G=3"
$ go run ~/main.go
3
12

在Go中,泛型可以做什么更多更复杂的事情吗?当然可以。除了最基础的算法实现以外,我们可以通过后面的几个场景看一下泛型可用的场景。

实现类型安全的Map

在现实开发过程中,我们往往需要对slice中数据的每个值进行单独的处理,比如说需要对其中数值转换为平方值,在泛型中,我们可以抽取部分重复逻辑作为map函数:

package main

import (
    "fmt"
)

func mapFunc[T any, M any](a []T, f func(T) M) []M {
    n := make([]M, len(a), cap(a))
    for i, e := range a {
        n[i] = f(e)
    }
    return n
}

func main() {
    vi := []int{1,2,3,4,5,6}
    vs := mapFunc(vi, func(v int) int {
        return v*v
    })
    fmt.Println(vs)
}
$ go run -gcflags=-G=3 main.go
[1 4 9 16 25 36]

在这个例子中,我们定义了一个M类型,因此除了进行同样类型的转换外,也可以做不同类型的转换:

-     vs := mapFunc(vi, func(v int) int {
-        return v*v
+     vs := mapFunc(vi, func(v int) string {
+        return "<"+fmt.Sprint(v)+">"
$ go run -gcflags=-G=3 main.go
[<1> <2> <3> <4> <5> <6>]

实现类型安全的Map/Filter

除了操作数据以外,我们通常还需要对数据进行筛选。在前面的例子上,我们可以通过实现filterFunc实现更好的通用逻辑:

package main

import (
	"crypto/rand"
	"fmt"
	"math/big"
	"strings"
)

func mapFunc[T any, M any](a []T, f func(T) M) []M {
    n := make([]M, len(a), cap(a))
    for i, e := range a {
        n[i] = f(e)
    }
    return n
}


func filterFunc[T any](a []T, f func(T) bool) []T {
    var n []T
    for _, e := range a {
        if f(e) {
            n = append(n, e)
        }
    }
    return n
}


func main() {
    vi := filterFunc(
		mapFunc([]int{1,2,3,4,5,6},
			func(v int) int {
				return v*v
			},
		), 
		func(v int) bool {
			return v < 40
		})
    fmt.Println(vi)

	vs := filterFunc(
		mapFunc([]string{"a", "b", "c", "d", "e"},
			func(v string) string {
				// 需要使用crypto/rand增加随机性
				n, _ :=rand.Int(rand.Reader, big.NewInt(5))
				
				i := int(n.Int64())+1
				return strings.Repeat(v, i)
			},
		), 
		func(v string) bool {
			return len(v)>3
		})
    fmt.Println(vs)
}
$ go run -gcflags=-G=3 main.go
[1 4 9 16 25 36]
[aaaa dddd eeeee]

实现类型可靠的Worker Pool

除了上面这个例子,我们还可以通过泛型实现一个类型可靠的通用批量类型转换函数:

package main

import (
	"fmt"
	"strconv"
	"sync"
)

type T1 interface{}
type T2 interface{}

func ParallelMap(parallelism int, in []T1, f func(T1) (T2, error)) ([]T2, error) {
	var wg sync.WaitGroup
	defer wg.Wait()

	inc, outc, errc := make(chan T1), make(chan T2), make(chan error)

	donec := make(chan struct{})
	defer close(donec)

	wg.Add(parallelism)
	for i := 0; i < parallelism; i++ {
		go func() {
			defer wg.Done()
			for x := range inc {
				y, err := f(x)
				if err != nil {
					select {
					case errc <- err:
					case <-donec:
					}
					return
				}
				select {
				case outc <- y:
				case <-donec:
					return
				}
			}
			select {
			case errc <- nil:
			case <-donec:
			}
		}()
	}

	go func() {
		for _, x := range in {
			inc <- x
		}
		close(inc)
	}()

	out := make([]T2, 0, len(in))
	for rem := parallelism; rem > 0; {
		select {
		case err := <-errc:
			if err != nil {
				return nil, err
			}
			rem--
		case y := <-outc:
			out = append(out, y)
		}
	}
	return out, nil
}

func main() {
	in := []T1{"1", "2", "3", "4", "5"}
	out, err := ParallelMap(4, in, func(x T1) (T2, error) {
		return strconv.Atoi(x.(string))
	})
	if err != nil {
		fmt.Println("error: ", err)
		return
	}
	fmt.Println(out)

	in2 := []T1{1, 2, 3, 4, 5}
	out2, err := ParallelMap(4, in2, func(x T1) (T2, error) {
		return fmt.Sprintf("<%d>", x), nil
	})
	if err != nil {
		fmt.Println("error: ", err)
		return
	}
	fmt.Println(out2)
}
$ go run -gcflags=-G=3 main.go
[3 5 2 4 1]
[<1> <4> <5> <3> <2>]

其他应用

我们可以预见在Go 1.18版本中,多个标准库会被新增或者扩展,包括:类型定义库constraints,通用slice操作库slices,通用类型安全mapmaps等等。因为这些会进入标准库,大家可以先自行实现试用,真正线上使用建议等待标准库添加内容即可。

Go泛型的实现原理

我们回归到最原始的例子快速看一下Go中是如何实现泛型的。为了方便分析,我们在所有func上添加go:noinline防止内联,然后编译程序进行分析。这里可能Go 1.17实现问题未能支持如go toolgo build -gcflags=all=-S之类的命令传递-G=3参数,因此这里我们选择第三方的反汇编工具看一下具体的实现:

ASM

可以看到目前Go会根据类型将泛型展开成对应类型函数,这样也会小小的增加编译时间和编译后文件大小。因为我测试使用Apple Silicon平台,考虑大家可能不熟悉相关汇编,具体执行逻辑不再具体展示。

其他注意事项

目前Go的泛型仍在开发过程中,即便在1.17beta到正式版过程中,很多泛型的corner case也正在完善过程中,比如在之前测试中我发现某些代码在beta版本无法正确编译,但是在RC中已可以正确编译。目前的泛型实现未必代表1.18版本中是相同的实现细节,甚至可能在1.18中提供更多的功能。同时,目前1.17泛型类型是无法在package中导出的,这导致在1.17版本中它的应用场景大大的受限。如果你仍有计划在某些场景中使用,我仍旧建议单元测试覆盖你使用的场景情况,防止出现版本迭代可能导致的问题。