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 中的泛型用法,合理地使用泛型有利於提高開發效率。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。