泛型

原文

在线阅读

导读:

  • 约束
  • 使用方法
  • 实现原理
  • 跟其它语言的泛型进行对比
  • 用例子学泛型
  • issues

泛型需满足 go1.18+

约束

go使用interface作为约束,约束的意思是约束了这个泛型都具有哪些实际类型。所以可以理解为,go将interface的职责给扩展了,让接口不仅仅作为接口 --- 解耦的,抽象化的结构体,还具有了约束,对于类型的约束作用。

type st interface{
int | string
}

这里 st约束拥有int和string,请注意这里的st是约束,不是泛型类型

go内置了很多约束,比如说 any 和 comparable ,意思是任何类型和可以比较的类型。以后应该会有一个新的内置约束的包叫做package constraints 例如any comparable ,Ordered 等等约束都会内置到标准库中

约束不仅仅可以单独写出来,还可以内置于函数内部。

func Age[T int| string,B float64| string](i T,j B){}

这种形式下,T 和 B 的约束就是仅限此函数使用

下面我们看一种形式,这种情况下约束的不仅仅是string和int,而是包含了底层是他们的所有数据,比如说 type DD int 也符合这个约束,请记住只能使用在底层类型上,如果使用~DD是不被允许的

type st interface{
~string | ~int
}

于此同时,约束也不仅仅是基础类型,约束的内容是方法也是可以的


func ConcatTo[S Stringer, P Plusser](s []S, p []P) []string {
r := make([]string, len(s))
for i, v := range s {
r[i] = p[i].Plus(v.String())
}
return r
} type Plusser interface {
Plus(string) string
}
type Stringer interface {
String() string
}

所有说这里就可以看出来,在引入泛型之后,go的interface的功能扩充了。

约束跟接口是一样的也是可以嵌套的

type ComparableHasher interface {
comparable
Hash() uintptr
} // or type ImpossibleConstraint interface {
comparable
[]int
}

这里的意义就是 and的意思 就是说这个约束是可以比较的还是必须得支持hash()uintptr

下面这种方法也是可以的

type NumericAbs[T any] interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~complex64 | ~complex128
Abs() T
}

上面的类型意思是满足数字类型,下面的意思是满足这个方法,所以最终实现这个约束,就是一个对象是数字类型,并且实现了这个接口

那么这里有一个疑问,给约束嵌入泛型,应该如何操作

type EmbeddedParameter[T any] interface {
T
}

cannot embed a type parameter 这种方法,go不允许这么做。因为T已经是泛型了,而约束里的类型应该是实际类型,所以T不能这么用,

不过如果约束里面是方法就可以这么做,这是因为T 这里只是方法的一个参数,比如说

type EmbeddedParameter[T any] interface {
~int | ~uint
me() T

如果想使用这种约束,可以这么使用

func Abs[T EmbeddedParameter[T]](t T)T{}

解释一下,中括号里面泛型的两个T表达的意思是不一样的,后面的T表达的是 约束里的泛型,表示 any,前面的T表示的是满足后面的这个约束的类型T,但是这里注意,后面T虽然之前定义的时候是any但是这里被改变了,改变为了必须满足约束 EmbeddedParameter的类型,如果说的通俗点,从any变成了,满足 int | uint and 实现 me()T方法 后文会有代码进行解释。

当然了,后面的T没有也行,如果没有后面的T就是相当于不改变后面的T的约束类型了

type Differ[T1 any] interface {
Diff(T1) int
} func IsClose[T2 Differ](a, b T2) bool {
return a.Diff(b) < 2
}

当结构体中使用泛型的时候,泛型可以直接作为嵌入使用

type Lockable[T any] struct {
T
mu sync.Mutex
}

请注意,type a[T any] interface 这种写法有可能在go1.18还不支持

当使用了泛型之后,是无法使用断言的,这是非法的,那么如果一定要在运行时的时候去判断类型怎么办呢?答案就是转变成interface{}即可,因为我们知道任何对象都已经实现了空接口,那么就可以被空接口去转化

func GeneralAbsDifference[T Numeric](a, b T) T {
switch (interface{})(a).(type) {
case int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64:
return OrderedAbsDifference(a, b)
case complex64, complex128:
return ComplexAbsDifference(a, b)
}
}

下面看一下别名的真实类型是泛型的情况

type A[T any] []T

type AliasA = A // 错误 

type AliasA = A[int] // 正确

其中错误的问题是 别名不能直接使用泛型类型 cannot use generic type A[T any] without instantiation,它需要泛型的实例化

使用方法

下面展示一下go泛型的基本使用方法

package main

import "fmt"

func main() {
fmt.Printf("%v",Age[int](12))
} func Age[T any](t T) T{
return t
}

这是函数使用泛型的写法,当函数使用泛型的时候,需要在变量前使用中括号标注泛型的具体约束,然后后面才能使用这个泛型类型,使用泛型函数的时候,中括号是可以省略的Age(12) 系统会自动推算泛型的具体实现。顺便说一下,泛型类型使用%v作为占位符,泛型类型无法进行断言,这一点跟interface{}不同。

当然了,我么也可以不用any,自定义一个约束

package main

import "fmt"

func main() {
Age[int](12)
}
type st interface{
int | string
}
func Age[T st](t T) {
fmt.Println((t))
}

看完了在函数内的泛型,我们在看一下在方法中如何使用泛型

package main

import "fmt"

func main() {
new(Age[int]).Post(12) var dd DD[int]
dd.TT(12)
} type Age[T any] struct {
I T
} func (a *Age[T]) Post(t T) {
fmt.Println(a.I, t)
} type DD[T any] []T func(dd *DD[T])TT(t T){
fmt.Println(t,len(*dd))
}

在 age 结构体声明的时候,声明了一个泛型 T ,在struct体内就可以使用这个T,值得注意的是,这个结构体方法内部仅可以使用定义在这个结构体对象上的泛型

下面是一个错误案例

func (a *Age[T])Post[B any](t T,b B) {
fmt.Println(a.I, t)
}

syntax error: method must have no type parameters

接下来我们看一下,如何使用 type a[T any] interface{} 有类型也有方法的泛型结构

package main

import "fmt"

func main() {
var d DDD
var i DDD
d = 1
i = 2
io := AbsDifference[DDD](d, i)
fmt.Println(io)
} type DDD int func (ddd DDD) Abs() DDD {
return ddd + ddd
} type NumericAbs[T any] interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~complex64 | ~complex128
Abs() T
} // AbsDifference computes the absolute value of the difference of
// a and b, where the absolute value is determined by the Abs method.
func AbsDifference[T NumericAbs[T]](a, b T) T {
d := a - b
return d.Abs()
}

实现原理

泛型的第一种方法是在编译这个泛型时,使用一个字典,里面包含了这个泛型函数的全部类型信息,然后当运行时,使用函数实例化的时候从这个字典中取出信息进行实例化即可,这种方法会导致执行性能下降,一个实例化类型int, x=y可能通过寄存器复制就可以了,但是泛型必须通过内存了(因为需要map进行赋值),不过好处就是不浪费空间

还有一种方法就是把这个泛型的所有类型全部提前生成,这种方法也有一个巨大的缺点就是代码量直线上升,如果是一个包的情况下还能根据具体的函数调用去实现该实现的类型,如果是包输出的的情况下,那么就得不得不生成所有的类型。

所以将两者结合在一起或许是最好的选择。

这种方法是这样的,如果类型的内存分配器/垃圾回收器呈现的方式一致的情况下,只给它生成一份代码,然后给它一个字典来区分不同的具体行为,可以最大限度的平衡速度和体积

跟其它语言的泛型进行对比

  • c语言:本身不具有泛型,需要程序员去实现一个泛型,实现复杂,但是不增加语言的复杂度(换言之只增加了程序员的)
  • c++和rust:跟go基本保持一种方式,就是增加编译器的工作量
  • Java:将泛型装箱为object,在装箱和拆箱擦除类型的过程中,程序执行效率会变低

用例子学泛型

理论学习完了,不使用例子进行复习的话会忘的很快的。跟着我看几个例子吧

例子一: 函数泛型 map-filter-reduce

package main

import (
"fmt"
) func main() {
vM := Map[int]([]int{1, 2, 3, 4, 5}, func(i int) int { return i + i
})
fmt.Printf("map的结果是:%v", vM)
vF := Filter[int]([]int{1, 2, 3, 4, 5}, func(t int) bool {
if t > 2 {
return true
}
return false
})
fmt.Printf("filter的结果是:%v", vF)
vR := Reduce[Value, *Result]([]Value{
{name: "tt", year: 1},
{name: "bb", year: 2},
{name: "7i", year: 3},
{name: "8i", year: 4},
{name: "u4i", year: 5},
{name: "uei", year: 6},
{name: "uwi", year: 7},
{name: "uti", year: 8},
}, &Result{}, func(r *Result, v Value) *Result {
r.value = r.value + v.year
return r
})
fmt.Println("reduce的结果是:", vR.value) } // Map:类似于洗菜,进去的菜和出来的菜不一样了所以需要两种种类
func Map[T1, T2 any](arr []T1, f func(T1) T2) []T2 {
result := make([]T2, len(arr))
for k, v := range arr {
result[k] = f(v)
}
return result
} // Filter:类似于摘菜,进去的菜和出来的菜是一种,不过量减少了
func Filter[T any](arr []T, f func(T) bool) []T {
var result []T
for _, v := range arr {
if f(v) {
result = append(result, v)
}
}
return result
} // Reduce:类似于做菜,将菜做成一道料理,所以需要两种类型
func Reduce[T1, T2 any](arr []T1, zero T2, f func(T2, T1) T2) T2 {
result := zero
for _, v := range arr {
result = f(result, v)
}
return result
} type Value struct {
name string
year int
}
type Result struct {
value int
}

map的结果是:[2 4 6 8 10] filter的结果是:[3 4 5] reduce的结果是: 36

例子二: 方法上的泛型 sets

package main

import (
"fmt"
) func main() { // 这里 Sets的具体类型和Make的具体类型都是int,所以可以正常赋值
var s Sets[int] = Make[int]()
//
s.Add(1)
s.Add(2)
fmt.Println(s)
fmt.Println(s.Contains(3))
fmt.Println(s.Len())
s.Iterate(func(i int) {
fmt.Println(i)
})
fmt.Println(s)
s.Delete(2)
fmt.Println(s)
} // Sets 一个key 存储对象
type Sets[T comparable] map[T]struct{} // Make 实例化一个map
func Make[D comparable]() Sets[D] {
// 泛型就像一个管道一样,只要实例化的时候管子里的东西一致,那么就是一根管子
return make(Sets[D])
} // Add 向这个sets添加内容
func (s Sets[T]) Add(t T) {
s[t] = struct{}{}
} // delete ,从这个sets中删除内容
func (s Sets[T]) Delete(t T) {
delete(s, t)
} // Contains 播报t是否属于这个sets
func (s Sets[T]) Contains(t T) bool {
_, ok := s[t]
return ok
} //Len sets拥有的长度 func (s Sets[T]) Len() int {
return len(s)
} // Iterate 迭代器,并且给予每个元素功能 func (s Sets[T]) Iterate(f func(T)) {
for k := range s {
f(k)
}
}

map[1:{} 2:{}] false 2 1 2 map[1:{} 2:{}] map[1:{}]

例子三: 外部定义的约束 实现一个sort接口类型

package main

import "fmt"

func main() {
fmt.Println("Hello, 世界")
}
// ~ 代表只要底层满足这些类型也可以算满足约束
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uintptr |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 | ~string
}
type orderedSlice[T Ordered] []T func (s orderedSlice[T]) Len() int { return len(s) }
func (s orderedSlice[T]) Less(i, j int) bool { return s[i] < s[j] }
func (s orderedSlice[T]) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func OrderedSlice[T Ordered](s []T) {
sort.Sort(orderedSlice[T](s))
}

issues

问题一: 关于泛型中的零值

在go里面对泛型的零值并没有一个所谓的泛型零值可以使用,需要根据不同的实践去实现,比如

package main

import "fmt"

func main() {

}

type Aget[T any] struct {
t *T
}
// 根据实际判断,如果a的t不等于nil再返回,如果是nil就返回一个T类型的nil(意思就是只声明)
func (a *Aget[T]) Approach() T {
if a.t != nil {
return *a.t
}
var r T
return r
}

实际上目前(1.18还没发布),还没一个确切的泛型的零值,那么我们要做的只能是按照实际来具体分析,按照提案,以后有可能使用return ... return _ return return nil return T{} 这些都是可能的结果,我个人比较喜欢 return T{} 来表示泛型的零值,或许在go1.19或者go2的时候能实现,拭目以待吧。

问题二: 无法识别使用了底层数据的其它类型

type Float interface {
~float32 | ~float64
} func NewtonSqrt[T Float](v T) T {
var iterations int
switch (interface{})(v).(type) {
case float32:
iterations = 4
case float64:
iterations = 5
default:
panic(fmt.Sprintf("unexpected type %T", v))
}
// Code omitted.
} type MyFloat float32 var G = NewtonSqrt(MyFloat(64))

这里约束 Float拥有的约束类型是 ~float32float64当在switch中定义了float32和flaot64时,无法识别下面的新类型 MyFloat即使它的底层时 float32 ,go的提议是以后在switch中使用 case ~float32: 来解决这个问题,目前尚未解决这个问题

问题三: 即便约束一致,类型也是不同的

func Copy[T1, T2 any](dst []T1, src []T2) int {
for i, x := range src {
if i > len(dst) {
return i
}
dst[i] = T1(x) // x 是 T2类型 不能直接转化为 T1类型
}
return len(src)
}

T1,和T2 虽然都是any的约束,但是啊,它不是一个类型啊!

Copy[int,string]() // 这种情况下,你能说可以直接转化吗???

这种代码可以更改一下

dst[i]= (interface{})(x).(T1)

确认是一种类型以后才能转化

参考资料

go泛型教程的更多相关文章

  1. Java 理论和实践: 了解泛型

    转载自 : http://www.ibm.com/developerworks/cn/java/j-jtp01255.html 表面上看起来,无论语法还是应用的环境(比如容器类),泛型类型(或者泛型) ...

  2. Java 泛型具体解释

    在Java SE1.5中.添加了一个新的特性:泛型(日本语中的总称型).何谓泛型呢?通俗的说.就是泛泛的指定对象所操作的类型.而不像常规方式一样使用某种固定的类型去指定. 泛型的本质就是将所操作的数据 ...

  3. HowToDoInJava Java 教程·翻译完成

    原文:HowToDoInJava 协议:CC BY-NC-SA 4.0 欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以走的更远. ApacheCN 学习资源 目录 核心 Java 教程 什 ...

  4. Java JDK 版本的区别

    jdk6和jdk5相比的新特性有: 1.instrumentation 在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument.本地代码 ...

  5. HashMap vs ConcurrentHashMap — 示例及Iterator探秘

    如果你是一名Java开发人员,我能够确定你肯定知道ConcurrentModificationException,它是在使用迭代器遍历集合对象时修改集合对象造成的(并发修改)异常.实际上,Java的集 ...

  6. 超级火的java自学网站

    学靠的是毅力和自律,一定要坚持,否则就会前功尽弃,我自己也一直在边学边工作,当然自学要配合好的学习资料. 我是通过这个地方去学习的,它可以添加学习计划,从java基础到高级,从后台到前端,从细节到框架 ...

  7. java泛型使用教程

    参考: java 泛型    Java泛型中E.T.K.V等的含义 一.Java泛型中E.T.K.V等的含义 E - Element (在集合中使用,因为集合中存放的是元素) T - Type(Jav ...

  8. Java-Runoob-高级教程:Java 泛型

    ylbtech-Java-Runoob-高级教程:Java 泛型 1.返回顶部 1. Java 泛型 Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检 ...

  9. Java基础教程:泛型基础

    Java基础教程:泛型基础 引入泛型 传统编写的限制: 在Java中一般的类和方法,只能使用具体的类型,要么是基本数据类型,要么是自定义类型.如果要编写可以应用于多种类型的代码,这种刻板的限制就会束缚 ...

随机推荐

  1. K8S的安装部署以及基础知识

    Kubernetes(K8S)概述 Kubernetes又称作k8s,是Google在2014年发布的一个开源项目. 最初Google开发了一个叫Borg的系统(现在命名为Omega),来调度近20多 ...

  2. mysql加强(6)~子查询简单介绍、子查询分类

    一.子查询简单介绍 1.什么是子查询? 一个查询之中嵌套了其他的若干查询. 在使用select 语句查询时,有时候where的查询条件中的限制条件不是一个确定的值,而是一个来自于另一个查询的结果. 子 ...

  3. HTML 基础2

    当浏览器读到一个样式表,它就会按照这个样式表来对文档进行格式化.有以下三种方式来插入样式表: 外部样式表 内部样式 内联样式 外部样式表 当样式需要被应用到很多页面的时候,外部样式表将是理想的选择.使 ...

  4. python 小兵(5)参数

    我们目前为止,已经可以完成一些软件的基本功能了,那么我们来完成这样一个功能:约x 1 2 3 4 5 pint("拿出手机") print("打开陌陌") pr ...

  5. 关于将px转换为vw vh的解决方案

    什么是vw(Viewport Width)和vh(Viewport Height)? vw和vh是前端开发中的一个动态单位,是一个相对于网页视口的单位. 系统会将视口的宽度和高度分为100份,1vw占 ...

  6. application/x-www-form-urlencoded、application/json、multipart/form-data、text/xml简单总结

    最近在数据传输时,一直不明白这四种的区别,查了很多资料,也还是感到很模糊,因此,简单总结一下,以后再完善 1.在GET方式传输数据中,这四种格式,后台都可以接收数据(原生的request.getPar ...

  7. ittun.com的使用方法

    [如果这篇文章对你有所作用,请加关注哦!] 步骤一: 进入官网http://ittun.com/ Windows 64位下载http://ittun.com/upload/17.2/ittun_win ...

  8. PHP操作Mysql疑问?

    1.Mysql控制台乱码 set character_set_results = 'utf8';

  9. 计算机网络再次整理————tcp周边[八]

    前言 tcp的包的格式可以看我以前的计算机网络整理,下面这些周边只是为了开发时候我们能用到一些理论知识. 正文 首先要介绍的就是域名,为啥有域名这东西呢?单纯站在网络的角度上讲这属于应用层的东西了. ...

  10. LNMP 架构 与 部署 uwsgi 服务

    内容概要 nginx 配置文件中 location 匹配符号 LNMP 架构 uwsgi 服务部署 内容详细 一.location 使用 Nginx Location 可以控制访问网站的路径,但一个 ...