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

推荐订阅源

cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
雷峰网
雷峰网
Recent Announcements
Recent Announcements
月光博客
月光博客
G
Google Developers Blog
腾讯CDC
S
Secure Thoughts
大猫的无限游戏
大猫的无限游戏
T
Tenable Blog
云风的 BLOG
云风的 BLOG
W
WeLiveSecurity
博客园 - 【当耐特】
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 聂微东
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
P
Privacy International News Feed
MyScale Blog
MyScale Blog
K
Kaspersky official blog
T
The Blog of Author Tim Ferriss
Attack and Defense Labs
Attack and Defense Labs
Spread Privacy
Spread Privacy
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
aimingoo的专栏
aimingoo的专栏
I
Intezer
Vercel News
Vercel News
小众软件
小众软件
Simon Willison's Weblog
Simon Willison's Weblog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
N
Netflix TechBlog - Medium
P
Proofpoint News Feed
Latest news
Latest news
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tor Project blog
S
Security Affairs
P
Proofpoint News Feed
博客园 - 三生石上(FineUI控件)
博客园 - Franky
C
Cyber Attacks, Cyber Crime and Cyber Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
美团技术团队
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Security @ Cisco Blogs
L
LINUX DO - 热门话题
Know Your Adversary
Know Your Adversary
Project Zero
Project Zero
D
Docker
L
Lohrmann on Cybersecurity
F
Full Disclosure

文章列表 on polarisxu

通过一个例子让你彻底掌握 Go 工作区模式 这道题正确率竟然只有 22% Go 开源项目推荐:一个简单的 Go 练手项目 go test 如何禁用缓存? Go1.18 快讯:constraints 包被移除标准库 「2022 版」轻松搞定 Go 开发环境 鹅厂小哥用行动反对加班:程序员加班能被终结吗? Go 为什么选择 Gopher 作为吉祥物? 本地如何配置多个 GitHub/Gitee 账号? 为 Java 开发者准备的 Go 教程 02:Java 有而 Go 无 GitHub 发现了 studygolang 项目依赖的漏洞 Typora 宣布收费后,这款开源 Markdown 编辑器火了 为 Java 开发者准备的 Go 教程 01:漫游了 我的 2021 年终总结 泛型版 singleflight:Go 中如何防止缓存击穿? 2021 年你写了多少代码?这个 Go 工具帮你统计 Go1.18 新特性:TryLock Go 泛型入门教程 程序员瑞士军刀:各种结构的转换工具 终于有 Go 版的 Elasticsearch 了 Go泛型系列:maps 包讲解 Go 1.18 中的 any 是什么? Go1.18 快讯:新的 IP 包 Go编程模式:详解函数式选项模式 Go泛型系列:slices 包讲解 Nginx 竟然也有 playground — Go 语言构建的 Go泛型系列:Go1.18 类型约束那些事 Go1.18 快讯:Module 工作区模式 Go1.18 快讯:新增的 Cut 函数太方便了 Go1.18 快讯:废弃了这个 API Go1.18 快讯:新增字符串 Clone API Go:如何获得项目根目录? Go 如何获取和设置环境变量 重磅变动:更快找到微信公众号里的Go文章 Go 这样设置版本号:我们的项目也可以 假期结束,推荐 2 本 manning 出的 Go 图书 Go Fiber 框架系列教程 04:测试应用 Go Fiber 框架系列教程 03:中间件 Go泛型系列:提前掌握Go泛型的基本使用 推荐一个 Go GUI 实战项目 Go Fiber 框架系列教程 02:详解相关 API 的使用 Go Fiber 框架系列教程 01: 和 Express 对比学习 Go Module:私有不合规库怎么解决引用问题 Go1.17 新特性:go get 变了 Go1.17 新特性:testing 包的相关变化 StackOverflow 上关于 Go select 死锁的问题 新书推荐:用 Gin 框架构建分布式应用 Go 官网要变天。。。 Gin 这是要成为 Go 官方框架? 扬眉吐气:刚刚,Go 已经默认支持泛型了 Go 1.17 新特性:Module 有哪些变化? Go 第三方库推荐:类型转换如此简单 担心密码提交到 GitHub?建议使用这个 Go 开源工具 Go 的时间格式化为什么是 2006-01-02 15:04:05? GitHub 为 Go 社区带来安全支持 Go开源项目推荐:500行代码确认请求时间花在哪 一道关于 len 函数的诡异 Go 面试题解析 厉害了我的 Go:推荐系统都有开源实现 Go1.17 新特性:新版构建约束 这个工具真好:看看你的Go项目依赖有无漏洞 网友很强大,发现了Go并发下载的Bug Rust 劝退系列 09:函数 Go项目实战:一步步构建一个并发文件下载器 这本 Go 新书挺期待的:100 个常见错误 Go 启用新的官方问答社区 这是要干嘛?!微软招 Go 编译器全职开发人员 Go1.17 快报之标准库越来越注重易用性 Go1.17 新特性之切片变数组 回顾 Go 官网的演变史 站长8年前的Go代码竟然进入大厂的项目里了 Go图书翻译:一个好消息,一个坏消息 不怕烂尾!决定组织翻译这本 Go 图书 Rust 劝退系列 08:模式匹配 一本花了2.5年写成的Go免费在线图书 被黑惨了:一句话,说明自己会 Go,咋整? Go1.16 中的新函数 signal.NotifyContext 怎么用? Go 1.17 新特性提前学之测试执行随机化 Rust 劝退系列 07:流程控制 官方的 Go 多版本管理:使用和原理 Rust新书:给你一个劝退的理由 周刊题解:常量表达式这个规则应该了解下 盘点那些使用 Go 语言的国外公司 编写了50万行Go代码是一种什么体验 Rust 劝退系列 06:常量 Uber 使用 Go 的规模这么大?!都自己定制的 Go 编译器了 Go Team Leader — rsc 大神新开源了一个库,增强模板功能 再一次看到了 Go 的节制:int128 类型要不要支持? Rust 劝退系列 05:复合数据类型 「卷」有理论依据:海勒姆定律—Go又是怎么卷的 Go 真的也可以进行 GUI 开发:还有这样的图书呢 Rust 劝退系列 04:基本数据类型 我的 Go 语言书单 Rust 劝退系列 03:变量 我又来推荐免费 Go 新书了:一本用 Go 讲架构的书 注释竟然还有特殊用途?一文解惑 //go:linkname 指令 这个功能,公众号、微信群会不会被玩坏? 一道 Go 闭包题,面试官说原来自己答错了:面别人也涨知识 Rust 劝退系列 02:第一个 Rust 程序 Rust 劝退系列 01:打造开发环境 答应我,这次一定彻底搞懂 Go 中的类型别名
跟着 Go 作者学泛型
2022-02-03 · via 文章列表 on polarisxu

大家好,我是 polarisxu。

在 GopherCon 2021 年大会上,Go 两位作者 Robert Griesemer 和 Ian Lance Taylor 做了泛型相关的演讲,即将在 Go1.18 发布的 Go 泛型,正是两位设计的。一直想着把他们的演讲做一个梳理,然后分享给大家。拖的有点久,趁春节假期整理出来了。

注意,本文中的 constraints 包,已经确定在 Go1.18 正式版中去除,放入 golang.org/x/exp 中。详细可以参考该文:Go1.18 这个包确定没了

Go1.18 关于泛型部分,主要有三个特性:

  • Type parameters for functions and types,即函数和类型的类型参数
  • Type sets defined by interfaces,即由接口定义的类型集合
  • Type inference,即类型推断

1、类型参数

先看函数的类型参数。

类型参数列表(Type parameter lists)

类型参数列表看起来是带方括号的普通参数列表。通常,类型参数以大写字母开头,以强调它们是类型:

[P, Q constraint1, R constraint2]

看一个例子。

非泛型版本的求最小值:

func min(x, y float64) float64 {
  if x < y {
    return x
  }
  return y
}

如果有 int 类型的 min 版本需求,得另外写一个类似的函数,这完全是重复代码。

那泛型版本呢?

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

注意和上面版本的区别。

  • 多了一个 [T constraints.Ordered],这就是类型参数列表,声明了一个类型 T,它的约束是 constraints.Ordered,即类型 T 满足它规定的条件
  • 参数类型 float64 变成了 T,而不是具体的某个类型

那这个泛型函数如何调用呢?

是不是很奇怪?其实仔细一琢磨,好像没问题。因为函数声明中有 [T constraints.Ordered],跟普通的函数参数有点像。调用时,提供 int,表明普通函数参数是 int 类型。

实例化

在调用时,会进行实例化过程:

1)用类型实参(type arguments)替换类型形参(type parameters)

2)检查类型实参(type arguments)是否实现了类型约束

如果第 2 步失败,实例化(调用)失败。

所以,调用过程可以分解为以下两步:

fmin := min[float64]
m := fmin(2.3, 3.4)

// 和下面等价
m := min[float64](2.3, 3.4)
// 相当于 m := (min[float64])(2.3, 3.4)

所以,实例化产生了一个非泛型函数。

类型的类型参数

类型也可以有类型参数。通过一个例子理解一下。

一个泛型版二叉树:

type Tree[T interface{}] struct {
	left, right *Tree[T]
	data        T
}

func (t *Tree[T]) Lookup(x T) *Tree[T]

var stringTree Tree[string]

注意其中的 [T interface{}] ,跟函数的类型参数语法是一样的,T 相当于是一个类型,所以,之后用到 Tree 的地方,T 都跟随着,即 Tree[T],包括方法的接收者(receiver)。

注意实例化的地方:var stringTree Tree[string],和上面两个实例化步骤中的第一步一样。

2、类型集合(Type sets)

先看值参数的类型(the type of value parameters)。

函数普通参数列表中的每个值参数都有一个类型,这个类型定义值的集合。比如 float64 定义了浮点数值的集合。

相应的有类型参数的类型(the type of type parameters),也就是说,类型参数列表中的每个类型参数都有一个类型,这个类型定义了类型的集合,这叫做类型约束(type constraint):

func min[T constraints.Ordered](x, y T) T

这里的 constraints.Ordered 是类型参数列表中的 T 参数的类型,它定义了类型的集合,即类型约束。

constraints.Ordered 是 Go1.18 内置的一个类型约束,它有两个功能:

  • 只有值支持排序的类型才能传递给类型参数 T;
  • T 类型的值必须支持 < 操作符,因为函数体中使用了该操作符。

类型约束是接口

大家都知道接口定义了方法集(method sets),演讲中给了一张图:

method sets

根据 Go 的规则,类型 P、Q、R 方法中包含了 a、b、c,因此它们实现了接口。

所以,反过来可以说,接口也定义了类型集(type sets):

type sets

上图中,类型 P、Q、R 都实现了左边的接口(因为都实现了接口的方法集),因此我们可以说该接口定义了类型集。

既然接口是定义类型集,只不过是间接定义的:类型实现接口的方法集。而类型约束是类型集,因此完全可以重用接口的语义,只不过这次是直接定义类型集:

interface defines type sets

这就是类型约束的语法,通过接口直接定义类型集:

type Xxx interface {
  int | string | bool
}

而 constraints.Ordered 的定义如下:

// Ordered is a constraint that permits any ordered type: any type
// that supports the operators < <= >= >.
// If future releases of Go add new ordered types,
// this constraint will be modified to include them.
type Ordered interface {
	Integer | Float | ~string
}

Ordered 定义了所有 interger、浮点数和字符串类型的集合。所以,< 操作符也是支持的。这其中的 Integer、Float 也在 constraints 包有定义。

细心的朋友应该发现了 ~string,类型前面的 ~~T 意味着包含底层类型 T 的所有类型集合。

如果约束中的所有类型都支持一个操作,则该操作可以与相应的类型参数一起使用

除了将约束单独定义为类型外,还可以写成字面值的形式,比如:

[S interface{~[]E}, E interface{}]

这看着有点晕,其实可以直接这么写:

Go1.18 中,any 是 interface{} 的别名,因此可以进一步写为:

E 是切片的元素类型,~[]E 表示底层是 []E 切片类型的都符合该约束。

3、类型推断

在调用泛型函数时,提供类型实参感觉有点多余。Go 虽然是静态类型语言,但擅长类型推断。因此泛型这里,Go 也实现了类型推断。

调用泛型版的 min,可以不提供类型实参,而是直接由 Go 进行类型推断:

var a, b, m float64
m := min[float64](a, b)

类型推断的细节很复杂,但使用起来还是很简单,大部分时候,跟普通函数调用没有区别。

关于类型推断,演讲中给了一个例子:

func Scale[E constraints.Integer](s []E, c E) []E {
	r := make([]E, len(s))
	for i, v := range s {
		r[i] = v * c
	}
	return r
}

这个函数的目的是希望对 s 中的每个元素都乘以参数 c,最后返回一个新的切片。

接着定义一个类型:

type Point []int32

func (p Point) String() string {
	// 实现细节不重要,忽略
	return "point"
}

很显然,Point 类型的切片可以传递给 Scale:

func ScaleAndPrint(p Point) {
	r := Scale(p, 2)
	fmt.Println(r.String())
}

我们希望对 p 进行 Scale,得到一个新的 p,但发现返回的 r 根本不是 Point:

func main() {
	p := Point{3, 2, 4}
	ScaleAndPrint(p)
}

会报错:r.String undefined (type []int32 has no field or method String)

所以,我们应该这样修改 Scale 函数:

func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
	r := make(S, len(s))
	for i, v := range s {
		r[i] = v * c
	}
	return r
}

注意其中的变化:加入了泛型 S,以及额外的类型约束 ~[]E

调用 Scale 时,不需要 r := Scale[Point, int32](p, 2),因为 Go 会进行类型推断。

正确的完整代码如下:

package main

import (
	"constraints"
	"fmt"
)

func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
	r := make(S, len(s))
	for i, v := range s {
		r[i] = v * c
	}
	return r
}

type Point []int32

func (p Point) String() string {
	// 实现细节不重要,忽略
	return "point"
}

func ScaleAndPrint(p Point) {
	r := Scale(p, 2)
	fmt.Println(r.String())
}

func main() {
	p := Point{3, 2, 4}
	ScaleAndPrint(p)
}

4、什么时候用泛型

泛型的加入,无疑增加了复杂度。我个人认为,能不用泛型就不用泛型。在演讲中,两位大佬提到,在以下场景可以考虑使用泛型:

  • 对于 slice、map、channel 等类型,如果它们的元素类型是不确定的,操作这类类型的函数可以考虑用泛型
  • 一些通用目的的数据结构,比如前面提到的二叉树等
  • 如果一些函数行为相同,只是类型不同,可以考虑用泛型重构

注意,目前 Go 方法不支持类型参数,所以,如果方法有需要泛型的场景,可以转为函数的形式。

此外,不要为了泛型而泛型。比如这样的泛型就很糟糕:

func ReadFour[T io.Reader](r T) ([]byte, error)

而应该使用非泛型版本:

func ReadFour(r io.Reader) ([]byte, error)

5、总结

泛型是一把双刃剑。泛型的加入,让 Go 不那么简单了。有些代码写出来,可读性可能非常差。我们应该按没有泛型时候写代码,当发现在 Repeat Yourself 时,再考虑能不能用泛型重构,千万别玩什么花样!

最后,放上演讲的视频地址,有兴趣的可以观看:https://www.youtube.com/watch?v=Pa_e9EeCdy8