xfeng

xfeng

Sporting | Reading | Technology | Recording
github
bilibili

浅析Golang泛型

image

1. 概述#

泛型是很多语言的标配,合理的使用泛型便于开发,然而,Golang 一开始并不支持泛型,因为开发 Golang 的人认为泛型重要但不是必须的,所以 Golang 一开始发布的时候没有泛型。今年,在 Go 社区的呼吁下,Go1.17 发布了泛型的体验版,为 Go1.18 的正式版泛型做了铺垫。本文介绍一下 Go 中的泛型和一些使用方法。

2. 什么是泛型#

在介绍泛型之前先了解一下多态

如:

动物类

有三种对象 -> 狗,猫,鸡

有一个相同的事件 -> 叫

有三种不同的表现 -> 汪汪,喵喵,咯咯达

多态分为两类:

  • 临时性多态(根据实参类型调用对应的版本,支持调用的数量十分有限,如:函数重载)
  • 参数化多态(根据实参类型生成不同的版本,支持任意数量的调用,这就是泛型

3. 泛型困境#

泛型有好处但也有坏处,怎么平衡泛型的好处和坏处就成了泛型设计者要面临的问题了。

泛型的困境:

image

简单来说就是:编码效率低(程序员开发慢)、编译效率低(编译器编译慢)、运行效率低(用户体验差)

4. Go1.17 中泛型的要点#

  • 函数可以通过 type 关键字引入额外的类型参数 (type parameters) 列表:func F (type T)(p T) { ... } 。

  • 这些类型参数可以像一般的参数一样在函数体中使用。

  • 类型也可以拥有类型参数列表:type M (type T) [] T。

  • 每个类型参数可以拥有一个约束:func F (type T Constraint)(p T) { ... }。

  • 使用 interface 来描述类型的约束。

  • 被用作类型约束的 interface 可以拥有一个预声明类型列表,限制了实现此接口的类型的基础类型。

  • 使用泛型函数或类型时需要传入类型实参。

  • 一般情况下,类型推断允许用户在调用泛型函数时省略类型实参。

  • 如果类型参数具有类型约束,则类型实参必须实现接口。

  • 泛型函数只允许进行类型约束所规定的操作。

5. 如何使用泛型#

5.1 如何输出泛型#

package main
 
import (
  "fmt"
)
 
//使用[]来保存额外的类型参数列表
//[T any]是类型参数,表示该函数支持任意T类型
func printSlice[T any](s []T) { 
  for _, v := range s {
    fmt.Printf("%v ", v)
  }
  fmt.Print("\n")
}
 
func main() {
  printSlice[int]([]int{1, 2, 3, 4, 5})
  printSlice[float64]([]float64{1.01, 2.02, 3.03, 4.04, 5.05})
//调用printSlice[string]([]string{“Hello”,“World”})时,会被类型推导为string类型
//如果编译器可以实现类型推导,printSlice([]string{“Hello”,“World”})
  printSlice([]string{"Hello", "World"})
  printSlice[int64]([]int64{5, 4, 3, 2, 1})
}

使用泛型首先要 Go1.17 版本以上

image

如果直接运行上面的代码可能会报错

.\main.go:9:6: missing function body
.\main.go:9:16: syntax error: unexpected [, expecting (

要想顺利运行,需要加上参数:-gcflags=-G=3

image

5.2 如何约束泛型的类型范围#

每个类型都有一个类型约束,就像每个普通参数都有一个类型一样。

在 Go1.17 版本中,泛型函数只能使用类型参数所能实例化出的任意类型 都能 支持的操作

(对于下代码,add 函数中的 + 应是 int, int8, int16, int32, int64,uint, uint8, uint16, uint32, uint64,uintptr,float32,float64, complex64, complex128,string 都能支持的操作)

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("hello","world"))
}

对于没有任何约束的类型参数实例,允许对其的操作有:

  • 声明这些类型的变量。
  • 使用相同类型的值为这些变量赋值。
  • 将这些类型的变量以实参形式传给函数或从作为函数返回值。
  • 取这些变量的地址。
  • 将这些类型的值转换或赋值给 interface {} 类型变量。
  • 通过类型断言将一个接口值赋值给这类类型的变量。
  • 在 type switch 块中作为一个 case 分支。
  • 定义和使用由该类型组成的复合类型,比如:元素类型为该类型的切片。
  • 将该类型传递给一些内置函数,比如 new。

5.21 comparable 约束#

不是所有的类型都可以用 == 进行比较,Go 内置了一个 comparable 约束,表示可比较

package main

import (
	"fmt"
)

func findFunc[T comparable](a []T, v T) int {
	for i, e := range a {
		if e == v {
			return i
		}
	}
	return -1
}

func main() {
	fmt.Println(findFunc([]int{1, 2, 3, 4, 5, 6}, 5))
}

5.3 泛型中的切片#

和泛型函数一样,使用泛型类型时,首要要对其进行实例化(显示为类型参数赋值类型)如:vs:=slice{5,4,2,1}

package main

import (
	"fmt"
)

type slice[T any] []T

/*
type any interface {
  type int, string
}*/

func printSlice[T any](s []T) {
	for _, v := range s {
		fmt.Printf("%v ", v)
	}
	fmt.Print("\n")
}

func main() {
	// note1: cannot use generic type slice[T interface{}] without instantiation
	// note2: cannot use generic type slice[T any] without instantiation
	vs := slice[int]{5, 4, 2, 1}
	printSlice(vs)

5.4 泛型中的指针#

package main

import (
	"fmt"
)

func pointerOf[T any](v T) *T {
	return &v
}

func main() {
	sp := pointerOf("foo")
	fmt.Println(*sp)

	ip := pointerOf(123)
	fmt.Println(*ip)
	*ip = 234
	fmt.Println(*ip)
}

5.5 泛型中的 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) string {
		return "<" + fmt.Sprint(v*v) + ">"
	})
	fmt.Println(vs)
}

5.6 泛型中的队列#

package main

import (
	"fmt"
)

type queue[T any] []T


func (q *queue[T]) enqueue(v T) {
	*q = append(*q, v)
}

func (q *queue[T]) dequeue() (T, bool) {
	if len(*q) == 0 {
		var zero T
		return zero, false
	}
	r := (*q)[0]
	*q = (*q)[1:]
	return r, true
}

func main() {
	q := new(queue[int])
	q.enqueue(5)
	q.enqueue(6)
	fmt.Println(q)
	fmt.Println(q.dequeue())
	fmt.Println(q.dequeue())
	fmt.Println(q.dequeue())
}

6. 总结#

本文简单介绍了 Go 中的泛型用法,合理地使用泛型有利于提高开发效率。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。