十二、Go语法进阶(接口和泛型)

157 阅读9分钟

Golang中文学习文档地址

1、接口

1.1 概念

  • 在 Go 语言中,接口是一种抽象类型,用于定义一组方法签名而不提供方法的实现。接口的核心理念是描述行为,而具体的行为实现由实现接口的类型提供。接口在 Go 语言中广泛用于实现多态性、松耦合和代码复用。

1.2 声明

//声明一个接口类型,接口名为Person
type Person interface {
    //接口对外暴露两个方法
    Say(string) string
    Walk(int)
}

1.3 初始化

  • 所谓接口,仅仅定义了一组规范,所以单单接口是无法初始化的,没有具体的实现
package main

import "fmt"

type Person interface {
	Name() string
	Age() int
}

func main() {
  //单接口无法初始化
  var p Person
  fmt.Println(p) //输出nil
}

1.4 接口的实现

  • 场景

    一家建筑公司,需要一台起重机,然后出了一份关于起重机的规范(接口),A公司接下了订单,根据建筑公司起重机的规范和自己的技术,造了起重机A(实现);后面建筑公司和A公司不合作,又找了个B公司,B公司也根据规范和自己的技术,造了起重机B(实现),其实建筑公司在使用起重机的方式和以前一样的。

  • go的接口实现示例

    package main
    
    import "fmt"
    
    //起重机接口,建筑公司定义的规范
    type Crane interface {
            JackUp() string
            Hoist() string
    }
    
    //起重机制造公司A
    type CraneA struct {
            name string
    }
    //公司A实现起重机规范
    func (c *CraneA) JackUp() string {
            return c.name + "======JackUp!!!!!"
    }
    //公司A实现起重机规范
    func (c *CraneA) Hoist() string {
            return c.name + "======hoist!!!!!"
    }
    
    //起重机制造公司B
    type CraneB struct {
            name string
            des  string
    }
    //公司B实现起重机规范
    func (c *CraneB) JackUp() string {
            return c.name + "======JackUp!!!!!使用" + c.des
    }
    //公司B实现起重机规范
    func (c *CraneB) Hoist() string {
            return c.name + "======hoist!!!!!使用" + c.des
    }
    
    //建筑公司
    type ConstructionCompany struct {
            name  string
            Crane Crane
    }
    
    //建筑公司改变起重机制造公司
    func (c *ConstructionCompany) changeCrane(crane Crane) {
            c.Crane = crane
    }
    
    func main() {
            cc := &ConstructionCompany{"建筑公司", &CraneA{"起重机A"}}
            fmt.Println(cc.Crane.JackUp())
            fmt.Println(cc.Crane.Hoist())
            cc.changeCrane(&CraneB{"起重机B", "B公司独家技术专利"})
            fmt.Println(cc.Crane.JackUp())
            fmt.Println(cc.Crane.Hoist())
    }
    
  • 在Go中接口的实现是隐式的,不用像Java使用implements关键字实现接口,只要结构体有接口定义的所有方法,就说明该结构体实现了该接口

1.5 空接口

type Any interface{

}
  • Any接口内部没有方法集合,按实现的定义,所有的类型都是Any接口的实现,因为所有类型的方法集都是空集的超集,所以Any接口可以保存任何类型的值。

2、泛型

2.1 简介

泛型:通过类型参数化来实现代码的复用与灵活性。又称为参数化多态。
作用:使得同一个函数或数据结构可以处理不同类型的数据,而无需为每种类型编写单独的代码。以函数来说,同一个函数,可以处理不同类型的参数。
Go中的泛型:Go是在1.18版本加入对泛型的支持。

2.2 Go泛型能解决的问题简单实例

  • 法运算函数,参数和返回值为int类型
package main

import "fmt"

func main() {
   sumValue := sumInt(1, 3)
   fmt.Println(sumValue)
}
// int 参数的加法
func sumInt(a, b int) int {
   return a + b
}
  • 当需要一个float64类型的加法运算时,需要新增一个参数和返回值为float64的函数。后续如果需要其他类型,例如:float32、int8、int16等参数时,就需要更多的函数,这些函数的处理逻辑是一样的,唯一不同的就只有参数和返回值;或者将参数和返回值声明为any,在函数内做判断处理(使用类型断言或反射)。但无论哪种方式,都是麻烦的写法。
package main

import "fmt"

func main() {
   sumInt := sumInt(1, 3)
   sumFloat := sumFloat(1.3, 3.4)
   fmt.Println(sumInt)
   fmt.Println(sumFloat)
}

// int 参数的加法
func sumInt(a, b int) int {
   return a + b
}

// float64 参数的加法
func sumFloat(a, b float64) float64 {
   return a + b
} 

2.3 使用泛型实现

2.3.1 泛型语法解析

  • 泛型的实现
package main

import "fmt"

func main() {
	sumInt := sum[int](1, 3)//声明类型参数
	sumFloat := sum(1.3, 3.4)//类型参数可省略,编译器自动推断
	fmt.Println(sumInt)
	fmt.Println(sumFloat)
}
func sum[T int | float64 | float32](a, b T) T {
	return a + b
}
  • Go泛型语法解析
/**
* 函数名后的[]内容:
* T:泛型名称
* int | float64 | float32:类型约束,约束T的类型只能是int|float64|float32
**/
func sum[T int | float64 | float32](a, b T) T {
	return a + b
}

2.3.2 泛型在其他数据类型上的使用

  • 泛型切片
type GenericSlice[T int | int32 | int64] []T
func main() {
	v := GenericSlice[int]{1, 2, 3}//类型参数不能省略,省略后编译不通过
	fmt.Println(v)
}
  • 泛型Map
type GenericMap[K comparable, V int | string | byte] map[K]V

func main() {
	m := GenericMap[int, string]{1: "hello", 2: "world"}
	fmt.Println(m)
}
  • 泛型结构体
type GenericStruct[T int | string] struct {
    Name string
    Id   T
}

func main() {
    s := GenericStruct[string]{
       Name: "hello",
       Id:   "123",
    }
    fmt.Println(s)
}
  • 泛型结构体:泛型切片作为结构体参数
type GenericStructSlice[T int | string, S int | string | float32] struct {
	Name  string
	Id    T
	slice []S
}

func main() {
	ss := GenericStructSlice[string, int]{
		Name:  "hello",
		Id:    "123",
		slice: []int{1, 2, 3},
	}
	fmt.Println(ss)
}
  • 泛型接口
type SayAble[T int | string] interface {
   Say() T
}

type Person[T int | string] struct {
   msg T
}

func (p Person[T]) Say() T {
   return p.msg
}

func main() {
  var s SayAble[string]
  s = Person[string]{"hello world"}
  fmt.Println(s.Say())
}

2.4 类型集

  • 类型集是Go泛型设计的核心,它定义了一个类型参数可以代表的所有类型的集合,本质上是对接口(interface)语义的扩展。
  • 普通接口(基本接口)描述了 “类型能做什么”(方法集合),而用于泛型的接口(通用接口,即类型约束)描述了 “类型是什么”(类型集合)。

2.4.1 类型集的核心概念

类型集通过接口类型来定义,这样的接口在泛型中被称为类型约束(Type Constraint) 。一个接口作为约束时,它的类型集包含以下所有类型:

  • 实现了该接口方法集的所有类型:这是传统接口的能力,例如 io.Reader 接口的类型集包含所有有 Read(p []byte) (n int, err error) 方法的类型。

    简单来说,所有实现了io.Reader接口的,都是io.Reader的类型集。

    //以下函数:泛型约束类型为io.Reader,说明在入参t的值,只要所有实现io.Reader接口的结构体,
    //		  都可以作为参数传入。
    func ReadFile[T io.Reader](p []byte,t T) int {
            n, err := t.Read(p)
            if err != nil {
                    return n
            }
            panic("读取错误")
    }
    
  • 在接口中显式列出的具体类型:通过 interface{ int | string | float64 } 这种语法,直接指定类型集中包含的类型。

type Num interface {
	int | int8 | float32 | float64
}

应用实例:

// 定义的类型集
type Num interface {
	int | int8 | float32 | float64
}

//泛型方法,使用类型集,好处:该类型集可通用
func add1[T Num](a, b T) T {
	return a + b
}
//泛型方法,该泛型类型集只能在add2函数上有效。
func add2[T int | int8 | float32 | float64](a, b T) T {
	return a + b
}

func main() {
	fmt.Println(add1(1, 2))
	fmt.Println(add1(1.2, 2.5))、
    //编译不通过,string类型不在类型集上
	fmt.Println(add1("1", "2"))
}
  • 通过 ~T 包含的底层类型为 T 的所有类型~ 符号表示 “底层类型(underlying type)匹配”,例如 ~int 的类型集包含 int 以及所有用 type MyInt int 定义的自定义类型。
type Int1 int
type Int2 Int1

func add1[T int](a, b T) T {
	return a + b
}

func add2[T ~int](a, b T) T {
	return a + b
}

func main() {
    //Int1类型的底层类型是int
	var v1 Int1 = 1
	var v2 Int1 = 1
    //Int2类型的类型是Int1,但Int1底层类型是int,所以,Int2底层类型也是int。
	var v3 Int2 = 1
	var v4 Int2 = 1
	var v5 int = 1
	var v6 int = 1
	//add1入参只能是int类型,v1、v2为Int1类型,v3、v4为Int1类型,所以编译不通过
	fmt.Println(add1(v1, v2))
	fmt.Println(add1(v3, v4))
	fmt.Println(add1(v5, v6))

	//add2泛型类型为~int,即只要底层类型是int的所有类型,都可作为入参
	fmt.Println(add2(v1, v2))
	fmt.Println(add2(v3, v4))
	fmt.Println(add2(v5, v6))

2.4.2 类型集的并集、交集和空集

  • 并集
// 有符号整型
type Integer interface {
	int | int8 | int16 | int32 | int64
}
// 无符号整型
type UnInteger interface {
	uint | uint8 | uint16 | uint32 | uint64
}

// 整型并集
type UnionInteger interface {
	//int | int8 | int16 | int32 | int64
	//uint | uint8 | uint16 | uint32 | uint64
	Integer | UnInteger
}
  • 交集
// 有符号整型
type Integer interface {
	int | int8 | int16 | int32 | int64
}

// 无符号整型
type UnInteger interface {
	int | uint | uint8 | uint16 | uint32 | uint64
}

// 整型交集集
type IntersectionInteger interface {
	//int类型在Integer和UnInteger中都有,所以IntersectionInteger的类型集为int
	Integer
	UnInteger
}

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

func main() {
	var v1 int = 4
	var v2 uint = 8
	fmt.Println(add(v1, v1))
	//无法编译通过,因为add的泛型类型集为int,没有uint
	fmt.Println(add(v2, v2))
}
  • 空集 两个类型集没有交集,就形成了空集。
// 有符号整型
type Integer interface {
	int | int8 | int16 | int32 | int64
}

// 无符号整型
type UnInteger interface {
	uint | uint8 | uint16 | uint32 | uint64
}

// 整型空集
type EmptyInteger interface {
	//以下两个类型集没有交集,形成了空集
	Integer
	UnInteger
}
  • 空接口和空集

    空接口是所有类型集的集合,即无论什么类型都可以传入,空接口一般用any

    空集是任何类型都不能传入。

2.4.3 泛型使用注意事项

  • 泛型不能作为一种类型声明。
// 声明Generic是一个泛型,这种写法是不允许的,泛型不是一种类型,而是一个类型约束。
type Generic[T int | int8] T

// 声明Generic是一个切片,切片类型是泛型,这种声明方式是允许的。
type Generic[T int | int8] []T
  • 泛型不能做类型断言

    泛型主要目的是为了类型无关,如果需要做类型断言,则入参类型应该是any,而不是泛型。

    func add[T int](a T,c any) T {
        // a是泛型,a.(int)编译会不通过
        a1 := a.(int)
        // c是any,可以使用c.(int)判断类型
        c1 := c.(int)
        return a
    }
    
  • 匿名结构体和匿名函数不支持使用泛型

    匿名结构体和匿名函数在声明时,必须初始化,所以参数类型都必须是已经确定好的。

    // 匿名结构体使用泛型,无法编译
    tempStruct := struct [T int | int8]{
                    name string
                    age T
            }
    // 匿名函数使用泛型,无法编译
    f := func[T int | int 8](a T){
    
    }
    
  • 方法的参数不支持泛型,所以泛型只能声明在方法的接收者上。-