Go语言进阶篇——泛型

简介: Go语言进阶篇——泛型

前言

在开始今天有关泛型的介绍之前,我们先来看一个简单的例子,如果我们要设计两个int类型变量相加的函数,我们可以这样设计:

func Sum (a, b int) int {
  return a + b
}
• 1
• 2
• 3

如果变量要求是float类型或者是其他类型,我们要面对一个问题:我们真的要为每个类型去编写对应的函数吗当然这个是非常影响开发效率的,而我们要如何解决这个问题呢?这就是我们几天要提到的泛型了。

什么是泛型

泛型,顾名思义,它是为了执行逻辑与类型无关的问题,这类问题不关心给出的类型是什么,只需要完成对应的操作就足够,我们可以尝试把上面的问题改成泛型的写法:

func Sum [T int|float64](a, b T) T{
  return a + b
}
• 1
• 2
• 3

类型形参:T就是一个类型形参,形参具体是什么类型取决于传进来什么类型

类型约束int | float64构成了一个类型约束,这个类型约束内规定了哪些类型是允许的,约束了类型形参的类型范围

类型实参Sum[int](1,2),手动指定了int类型,int就是类型实参。

第一种用法,显式的指明使用哪种类型,如下

Sum[int](2012, 2022)
• 1

第二种用法,不指定类型,让编译器自行推断,如下

Sum(3.1415926, 1.114514)
• 1

通过上面的介绍,相信大家对泛型有所了解了,在日常使用泛型时,我们要注意将泛型引入项目后,开发上确实会比较方便,随之而来的是项目复杂度的增加,毫无节制的使用泛型会使得代码难以维护,所以应该在正确的地方使用泛型,而不是为了泛型而泛型。

泛型结构

泛型切片

首先我们定义一个泛型切片,它的类型约束时int|float32|float64

type GenericsSlice[T int|float32|float64] []T

我们如何去使用这种泛型切片呢?让我们来看一下下面这个案例:

package main
import "fmt"
func main() {
  type GenericsSlience[T int | float64] []T
  //创建整形切片
  var ints GenericsSlience[int]
  ints = append(ints, 1)
  //创建浮点型切片
  var floats GenericsSlience[float64]
  floats = append(floats, 1.1)
  //打印切片
  fmt.Println(ints)
  fmt.Println(floats)
}

输出为:

泛型哈希表

我们定义泛型哈希表的时候,要保证键的类型必须是可以比较的,所以我们会使用comparable接口,下面我们可以尝试定义一个哈希表:

type GenericsMap[K comparable,V int|string|byte] map[K]V

使用时:

gmap1 := GenericMap[int, string]{1: "hello world"}
gmap2 := make(GenericMap[string, byte], 0)

泛型结构体

这是一个泛型结构体,类型约束为T int | string

解释type GenericStruct[T int | string] struct {
   Name string
   Id   T
}

使用:

解释GenericStruct[int]{
   Name: "jack",
   Id:   1024,
}
GenericStruct[string]{
   Name: "Mike",
   Id:   "1024",

注意:在结构体中如果我们要使用切片,一般推荐下面这种写法:

type Company[T int | string, S int | string] struct {
  Name  string
  Id    T
  Stuff []S
}

泛型接口

我们来看一个泛型接口的简单应用:

package main
import "fmt"
type Sayable[T int | float64 | string] interface {
  Say() T
}
type Person[T int | float64 | 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())
}

泛型结构的使用注意点

  1. 泛型不能作为一个类型的基本类型
  2. 泛型类型无法使用类型断言
  3. 匿名结构体/函数不支持泛型
  4. 不支持泛型方法,这里要说明一下,主要是不支持泛型形参,比如下面这样:
package main
import "fmt"
type Sayable[T int | float64 | string] interface {
  Say() T
}
type Person[T int | float64 | 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())
}

这样是无法通过编译的。

泛型使用示例

模拟队列

package main
type Queue[T any] []T
func (q *Queue[T]) Push(e T) {
  *q = append(*q, e)
}
func (q *Queue[T]) Pop() (_ T) {
  if q.Size() > 0 {
    res := q.Peek()
    *q = (*q)[1:]
    return res
  }
  return
}
func (q *Queue[T]) Peek() (_ T) {
  if q.Size() > 0 {
    res := (*q)[0]
    return res
  }
  return
}
func (q *Queue[T]) Size() int {
  return len(*q)
}

PopPeek方法中,可以看到返回值是_ T,这是具名返回值的使用方式,但是又采用了下划线_表示这是匿名的,这并非多此一举,而是为了表示泛型零值。由于采用了泛型,当队列为空时,需要返回零值,但由于类型未知,不可能返回具体的类型,借由上面的那种方式就可以返回泛型零值。也可以声明泛型变量的方式来解决零值问题,对于一个泛型变量,其默认的值就是该类型的零值

上面队列的例子,由于对元素没有任何的要求,所以类型约束为any。但堆就不一样了,堆是一种特殊的数据结构,它可以在O(1)的时间内判断最大或最小值,所以它对元素有一个要求,那就是必须是可以排序的类型,但是go语言内置可以比较的类型就只有字符串和数字,同时泛型约束不允许带方法的接口,所以在对初始化的时候就需要我们传入一个自定义的比较器

下面我们来尝试来实现一个简单的最小二根堆:

package main
type Comparator[T any] func(a, b T) int
type BinaryHeap[T any] struct {
  data       []T
  comparator Comparator[T]
}
func (Heap *BinaryHeap[T]) Peek() (_ T) {
  if Heap.Size() > 0 {
    return Heap.data[0]
  }
  return
}
func (Heap *BinaryHeap[T]) Pop() (_ T) {
  if Heap.Size() > 0 {
    res := Heap.Peek()
    Heap.data = Heap.data[1:]
    return res
  }
  return
}
func (Heap *BinaryHeap[T]) Push(value T) {
  Heap.data = append(Heap.data, value)
    Heap.up(Heap.Size() - 1)
}
func (Heap *BinaryHeap[T]) Size() int {
  return len(Heap.data)
}
func (Heap *BinaryHeap[T]) up(i int) {
  if Heap.Size() == 0 || i < 0 || i >= Heap.Size() {
    return
  }
  for parentindex = i>>1 - 1; parentindex >= 0; parentindex = i>>1 - 1 {
    if Heap.comparator(Heap.data[i], Heap.data[parentindex]) < 0 {
      Heap.data[i], Heap.data[parentindex] = Heap.data[parentindex], Heap.data[i]
      i = parentindex
    } else {
      break
    }
  }
}
func (Heap *BinaryHeap[T]) down() {
      if heap.Size() == 0 || i < 0 || i >= heap.Size() {
    return
  }
  size := heap.Size()
  for lsonIndex := i<<1 + 1; lsonIndex < size; lsonIndex = i<<1 + 1 {
    rsonIndex := lsonIndex + 1
    if rsonIndex < size && heap.compare(heap.s[rsonIndex], heap.s[lsonIndex]) < 0 {
      lsonIndex = rsonIndex
    }
    // less than or equal to
    if heap.compare(heap.s[i], heap.s[lsonIndex]) <= 0 {
      break
    }
    heap.s[i], heap.s[lsonIndex] = heap.s[lsonIndex], heap.s[i]
    i = lsonIndex
  }
}

使用起来如下

type Person struct {
  Age  int
  Name string
}
func main() {
  heap := NewHeap[Person](10, func(a, b Person) int {
    return cmp.Compare(a.Age, b.Age)
  })
  heap.Push(Person{Age: 10, Name: "John"})
  heap.Push(Person{Age: 18, Name: "mike"})
  heap.Push(Person{Age: 9, Name: "lili"})
  heap.Push(Person{Age: 32, Name: "miki"})
  fmt.Println(heap.Peek())
  fmt.Println(heap.Pop())
  fmt.Println(heap.Peek())
}

输出

{9 lili}
{9 lili} 
{10 John}

有泛型的加持,原本不可排序的类型传入比较器后也可以使用堆了,这样做肯定比以前使用interface{}来进行类型转换和断言要优雅和方便很多。

总结

go的一大特点就是编译速度非常快,编译快是因为编译期做的优化少,泛型的加入会导致编译器的工作量增加,工作更加复杂,这必然会导致编译速度变慢,事实上当初go1.18刚推出泛型的时候确实导致编译更慢了,go团队既想加入泛型又不想太拖累编译速度,开发者用的顺手,编译器就难受,反过来编译器轻松了(最轻松的当然是直接不要泛型),开发者就难受了,现如今的泛型就是这两者之间妥协后的产物。

相关文章
|
1天前
|
程序员 Go
go语言中的控制结构
【11月更文挑战第3天】
74 58
|
1天前
|
存储 编译器 Go
go语言中的变量、常量、数据类型
【11月更文挑战第3天】
15 9
|
1天前
|
数据采集 监控 Java
go语言编程学习
【11月更文挑战第3天】
17 7
|
1天前
|
Go 数据处理 API
Go语言在微服务架构中的应用与优势
本文摘要采用问答形式,以期提供更直接的信息获取方式。 Q1: 为什么选择Go语言进行微服务开发? A1: Go语言的并发模型、简洁的语法和高效的编译速度使其成为微服务架构的理想选择。 Q2: Go语言在微服务架构中有哪些优势? A2: 主要优势包括高性能、高并发处理能力、简洁的代码和强大的标准库。 Q3: 文章将如何展示Go语言在微服务中的应用? A3: 通过对比其他语言和展示Go语言在实际项目中的应用案例,来说明其在微服务架构中的优势。
|
2天前
|
Ubuntu 编译器 Linux
go语言中SQLite3驱动安装
【11月更文挑战第2天】
16 7
|
1天前
|
Go 数据处理 调度
探索Go语言的并发模型:Goroutines与Channels的协同工作
在现代编程语言中,Go语言以其独特的并发模型脱颖而出。本文将深入探讨Go语言中的Goroutines和Channels,这两种机制如何协同工作以实现高效的并发处理。我们将通过实际代码示例,展示如何在Go程序中创建和管理Goroutines,以及如何使用Channels进行Goroutines之间的通信。此外,本文还将讨论在使用这些并发工具时可能遇到的常见问题及其解决方案,旨在为Go语言开发者提供一个全面的并发编程指南。
|
2天前
|
安全 Go
用 Zap 轻松搞定 Go 语言中的结构化日志
在现代应用程序开发中,日志记录至关重要。Go 语言中有许多日志库,而 Zap 因其高性能和灵活性脱颖而出。本文详细介绍如何在 Go 项目中使用 Zap 进行结构化日志记录,并展示如何定制日志输出,满足生产环境需求。通过基础示例、SugaredLogger 的便捷使用以及自定义日志配置,帮助你在实际开发中高效管理日志。
11 1
|
JavaScript 前端开发 Go
终于!Go 1.18 将支持泛型,来听听Go 核心技术团队 Russ Cox怎么说
终于!Go 1.18 将支持泛型,来听听Go 核心技术团队 Russ Cox怎么说
终于!Go 1.18 将支持泛型,来听听Go 核心技术团队 Russ Cox怎么说
|
3天前
|
JavaScript Java Go
探索Go语言在微服务架构中的优势
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出。本文将深入探讨Go语言在构建微服务时的性能优势,包括其在内存管理、网络编程、并发模型以及工具链支持方面的特点。通过对比其他流行语言,我们将揭示Go语言如何成为微服务架构中的一股清流。
|
2天前
|
关系型数据库 Go 网络安全
go语言中PostgreSQL驱动安装
【11月更文挑战第2天】
18 5
下一篇
无影云桌面