在go语言中自定义泛型的变长参数

简介: 【7月更文挑战第8天】在Go语言中,由于官方1.18以前的版本不支持泛型,可以通过空接口和反射模拟泛型。泛型适用于通用数据结构和函数,虽牺牲了一些性能,但提高了代码复用和类型安全性。

1 简介

本文示例代码展示了一个自定义Container结构,利用反射创建指定类型切片并进行类型检查。
PutGet方法确保元素类型与容器匹配,否则返回错误。
代码在尝试添加不兼容类型或使用错误类型获取元素时会引发错误,体现了使用反射增强程序健壮性的特点。

2 自定义泛型:变长参数支持

在某些场景下,目前依然能使用反射来实现,比如泛型。
因为现在 Go 官方尚未在1.18版本之前的语法层面提供对泛型的支持,我们只能通过空接口结合反射来实现。

空接口 interface{} 本身可以表示任何类型的泛型,不过这个泛型太泛了,我们必须结合反射在运行时对实际传入的参数做类型检查,让其变得可控,

从而确保程序的健壮性,否则很容易因为传递进来的参数类型不合法导致程序崩溃。

下面我们通过一个自定义容器类型的实现来演示如何基于空接口和反射来实现泛型:

3 接口和容器的例子

基于 空接口和反射的 容器,实现泛型

     package main

    import (

        "fmt"

        "reflect"

    )

通过传入存储元素类型 和 容量 来初始化 容器

    type Container struct {

        s reflect.Value

    }

基于切片类型实现的容器,这里通过反射动态初始化这个底层切片

    func NewContainer(t reflect.Type, size int) *Container {

        return &Container{s: reflect.MakeSlice(reflect.SliceOf(t), 0, size)}

    }

通过反射对 实际传递来的 元素类型进行运行时检查。

如果与容器初始化设置的元素类型不同,则返回错误信息。

c.s.Type() 对应的是 切片类型,c.s.Type().Elem()对应的才是切片元素类型

    func (c *Container) Put(val interface{}) error {

    if reflect.ValueOf(val).Type() != c.s.Type().Elem() {

c.s 切片元素类型 与 传入参数不同

        return fmt.Errorf("put error:cannot put a %T into a slice of %s", c.s.Type().Elem())

       }
  • 如果类型检查通过则将其添加到容器

      c.s = reflect.Append(c.s, reflect.ValueOf(val))
    
           return nil
    
      }
    
      func (c *Container) Get(val interface{}) error {
    

还是通过反射对元素 类型进行检查,如果不通过则返回错误信息。

kind 与 Type 相比范围更大,表示类别,如指针,而Type则对应具体类型,如 *int

由于 val是指针类型,所有需要通过reflect.ValueOf(val).Elem() 获取指针指向的类型

    if reflect.ValueOf(val).Kind() != reflect.Ptr || reflect.ValueOf(val).Elem().Type() != c.s.Type().Elem() {

        return fmt.Errorf("get error:needs *%s but got %T", c.s.Type().Elem(), val)

      }

将容器第一个索引位置值赋值给 val 指针

    reflect.ValueOf(val).Elem().Set(c.s.Index(0))

然后删除容器第一个索引位置值

    c.s = c.s.Slice(1, c.s.Len()) 
    return nil 
    }

    func main() {

    nums := []int{1, 2, 3, 4, 5}

初始化容器,元素类型和nums中的元素类型相同

    c := NewContainer(reflect.TypeOf(nums[0]), 16)

    for _, n := range nums {

        if err := c.Put(n); err != nil {

            panic(err)

        }

从容器读取元素,将返回结果初始化为0

        num := 0

        if err := c.Get(&num); err != nil {

            panic(err)

        }

打印返回结果值

        fmt.Printf("%v, (%T)\n", num, num)

        }

        err := c.Put("s")     //put error:cannot put a *reflect.rtype into a slice of %!s(MISSING)

        err2 := c.Get("s100") //get error:needs *int but got string

        fmt.Println(err, err2)

    }
    ```

具体细节都已经在代码注释中详细标注了,执行上述代码,打印结果如下:

   -> chapter04 git:(main) x go run reflect/generic.go
   1(int)

如果我们试图添加其他类型元素到容器:

    ```
    if err := c.Put("s"); err != nil {    panic(err)}
    ```

或者存储返回结果的变量类型与容器内元素类型不符:

    ```
    if err := c.Get(num); err != nil {    panic(err)}
    ```

都会报错:

   -> generator git:(main) x go run reflect/generic.go
   panic: put error: can not put staring into a slice of int

   goroutine 1 [running]:
   main.main()
      ...
   exit status 2

  -> generator git:(main) x go run reflect/generic.go
   panic: get error: needs *int but got int

   goroutine 1 [running]:
   main.main()
      ...
   exit status 2

在这里,为了提高程序的健壮性,我们引入了错误处理机制,这块内容我们即将在下个章节中详细给大家介绍。

4 小结

  • 泛型的使用场景

    对特殊函数进行操作的函数

    使用反射时,可能更慢,它不是类型检查
    

    通用数据结构

    切片器数据结构,如链表 和 二叉树
    
    类型参数 替换 接口 存储数据,可避免类型断言
    

    不同的类型 需要实现一些通用方法时,有 通用方法时 可使用 类型

    许多函数都有类似代码,而不同点仅仅是 类型不同,此时使用 类型参数

    • 使用类型参数的场景的方式建议

         func ReadFour(r io.Reader) ([]byte, error)
      

不建议的方式

         func ReadFour[T io.Reader](r T) ([]byte, error)
目录
相关文章
|
10月前
|
Java 编译器 Go
【Golang】(5)Go基础的进阶知识!带你认识迭代器与类型以及声明并使用接口与泛型!
好烦好烦好烦!你是否还在为弄不懂Go中的泛型和接口而烦恼?是否还在苦恼思考迭代器的运行方式和意义?本篇文章将带你了解Go的接口与泛型,还有迭代器的使用,附送类型断言的解释
426 3
|
10月前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
454 2
|
Cloud Native Go API
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
680 0
|
Cloud Native Java Go
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
503 0
|
Cloud Native Java 中间件
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
529 0
|
Cloud Native Java Go
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
1098 0
|
存储 编译器 Go
Go 新版泛型使用:80余行代码构建一个哈希表
  2018 年,我使用 Go 语言实现了一个玩具性质的哈希表 (1),以便学习 Go 的 map 等数据类型如何工作。这个版本只支持字符串作为 key 以及 value。   两年后的 2020 年 6 月,Go 团队发布了一篇题为《泛型的下一步 (1) 》的文章,提供了一个新版的泛型草案设计,它基于扩展 Go 现有的接口,而不是添加 contract 等新概念来实现。如果你还没看过,我强烈建议你至少浏览一下新的设计草案文档 (2)。我不是专家,只能以我有限的经验和时间来谈论这个设计。   这篇文章将分享如何将我的玩具 hashtable 移植到新的泛型设计。如果你想跳过介绍并直接查看泛
319 0
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
Cloud Native 安全 Java
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
690 1