go 语言中的泛型

Go语言中的泛型可复用代码的友好指南 你好!我是Shrijith Venkatrama,Hexmos的创始人。目前,我正在开发,这是一款让从代码生成API文档变得极其简单的工具。Go语言的简洁性和性能使其成为开发者的心头好,但在Go 1.18之前,它缺少泛型功能。泛型让你能编写灵活、可复用的代码,同时保持类型安全。想写一个能处理多种类型的函数,或避免代码重复?泛型正是为此而生。本指南将介绍泛型是什么、如何使用以及为何它们如此实用。我会通过完整的编程示例、表格和技巧,让一切清晰易懂。 阅读详情

什么是泛型

泛型(Generics)是一种编程思想,它允许在编写代码时使用未知的类型。泛型可以增加代码的灵活性和可复用性,同时还能提高代码的安全性和可读性。

Go的泛型

Go还引入了非常多全新的概念:

  • 类型形参 (Type parameter)
  • 类型实参(Type argument)
  • 类型形参列表( Type parameter list)
  • 类型约束(Type constraint)
  • 实例化(Instantiations)
  • 泛型类型(Generic type)
  • 泛型接收器(Generic receiver)
  • 泛型函数(Generic function)

类型形参、类型实参、类型约束和泛型类型

观察下面这个简单的例子:

type IntSlice []int

var a IntSlice = []int{1, 2, 3} // 正确

var b IntSlice = []float32{1.0, 2.0, 3.0} // ✗ 错误,因为IntSlice的底层类型是[]int,浮点类型的切片无法赋值

这里定义了一个新的类型 IntSlice ,它的底层类型是 []int ,理所当然只有int类型的切片能赋值给 IntSlice 类型的变量。

接下来如果我们想要定义一个可以容纳 float32 或 string 等其他类型的切片的话该怎么办?答案是可以的,这时候就需要用到泛型了:

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

不同于一般的类型定义,这里类型名称 Slice 后带了中括号,对各个部分做一个解说就是:

T 就是上面介绍过的类型形参(Type parameter),在定义Slice类型的时候 T 代表的具体类型并不确定,类似一个占位符
int|float32|float64 这部分被称为类型约束(Type constraint),中间的 | 的意思是告诉编译器,类型形参 T 只可以接收 int 或 float32 或 float64 这三种类型的实参中括号里的 T int|float32|float64 这一整串因为定义了所有的类型形参(在这个例子里只有一个类型形参T),所以我们称其为 类型形参列表(type parameter list)
这里新定义的类型名称叫 Slice[T]这种类型定义的方式中带了类型形参,很明显和普通的类型定义非常不一样,所以我们将这种类型定义中带 类型形参 的类型,称之为 泛型类型(Generic type)

泛型类型不能直接拿来使用,必须传入类型实参(Type argument) 将其确定为具体的类型之后才可使用。

其他的泛型类型

所有类型定义都可使用类型形参,所以下面这种结构体以及接口的定义也可以使用类型形参:

// 一个泛型类型的结构体。可用 int 或 sring 类型实例化
type MyStruct[T int | string] struct {  
    Name string
    Data T
}

// 一个泛型接口(关于泛型接口在后半部分会详细讲解)
type IPrintData[T int | float32 | string] interface {
    Print(data T)
}

// 一个泛型通道,可用类型实参 int 或 string 实例化
type MyChan[T int | string] chan T

类型形参的互相套用

类型形参是可以互相套用的,如下

type WowStruct[T int | float32, S []T] struct {
    Data     S
    MaxValue T
    MinValue T
}

这个例子看起来有点复杂且难以理解,但实际上只要记住一点:任何泛型类型都必须传入类型实参实例化才可以使用。所以我们这就尝试传入类型实参看看:

var ws WowStruct[int, []int]
// 泛型类型 WowStuct[T, S] 被实例化后的类型名称就叫 WowStruct[int, []int]

上面的代码中,我们为T传入了实参 int,然后因为 S 的定义是 []T ,所以 S 的实参自然是 []int 。经过实例化之后WowStruct[T,S] 的定义类似如下:

// 一个存储int类型切片,以及切片中最大、最小值的结构体
type WowStruct[int, []int] struct {
    Data     []int
    MaxValue int
    MinValue int
}

因为 S 的定义是 []T ,所以 T 一定决定了的话 S 的实参就不能随便乱传了

几种语法错误

定义泛型类型的时候,基础类型不能只有类型形参,如下:

// 错误,类型形参不能单独使用
type CommonType[T int|string|float32] T
当类型约束的一些写法会被编译器误认为是表达式时会报错。如下:

//✗ 错误。T *int会被编译器误认为是表达式 T乘以int,而不是int指针
type NewType[T *int] []T
// 上面代码再编译器眼中:它认为你要定义一个存放切片的数组,数组长度由 T 乘以 int 计算得到
type NewType [T * int][]T 

//✗ 错误。和上面一样,这里不光*被会认为是乘号,| 还会被认为是按位或操作
type NewType2[T *int|*float64] []T 

//✗ 错误
type NewType2 [T (int)] []T 

为了避免这种误解,解决办法就是给类型约束包上 interface{} 或加上逗号消除歧义

type NewType[T interface{*int}] []T
type NewType2[T interface{*int|*float64}] []T 

// 如果类型约束中只有一个类型,可以添加个逗号消除歧义
type NewType3[T *int,] []T

//✗ 错误。如果类型约束不止一个类型,加逗号是不行的
type NewType4[T *int|*float32,] []T 

因为上面逗号的用法限制比较大,这里推荐统一用 interface{} 解决问题
3.4 特殊的泛型类型
这里讨论种比较特殊的泛型类型,如下:

type Wow[T int | string] int

var a Wow[int] = 123     // 编译正确
var b Wow[string] = 123  // 编译正确
var c Wow[string] = "hello" // 编译错误,因为"hello"不能赋值给底层类型int

这里虽然使用了类型形参,但因为类型定义是 type Wow[T int|string] int ,所以无论传入什么类型实参,实例化后的新类型的底层类型都是 int 。所以int类型的数字123可以赋值给变量a和b,但string类型的字符串 “hello” 不能赋值给c

泛型类型的套娃

泛型和普通的类型一样,可以互相嵌套定义出更加复杂的新类型,如下:

// 先定义个泛型类型 Slice[T]
type Slice[T int|string|float32|float64] []T

// ✗ 错误。泛型类型Slice[T]的类型约束中不包含uint, uint8
type UintSlice[T uint|uint8] Slice[T]  

// ✓ 正确。基于泛型类型Slice[T]定义了新的泛型类型 FloatSlice[T] 。FloatSlice[T]只接受float32和float64两种类型
type FloatSlice[T float32|float64] Slice[T] 

// ✓ 正确。基于泛型类型Slice[T]定义的新泛型类型 IntAndStringSlice[T]
type IntAndStringSlice[T int|string] Slice[T]  
// ✓ 正确 基于IntAndStringSlice[T]套娃定义出的新泛型类型
type IntSlice[T int] IntAndStringSlice[T] 

// 在map中套一个泛型类型Slice[T]
type WowMap[T int|string] map[string]Slice[T]
// 在map中套Slice[T]的另一种写法
type WowMap2[T Slice[int] | Slice[string]] map[string]T

泛型receiver

看了上的例子,你一定会说,介绍了这么多复杂的概念,但好像
我们知道,定义了新的普通类型之后可以给类型添加方法。那么可以给泛型类型添加方法吗?答案自然是可以的,如下:

type MySlice[T int | float32] []T

func (s MySlice[T]) Sum() T {
    var sum T
    for _, value := range s {
        sum += value
    }
    return sum
}

这个例子为泛型类型 MySlice[T] 添加了一个计算成员总和的方法 Sum() 。注意观察这个方法的定义:

首先看receiver (s MySlice[T]) ,所以我们直接把类型名称 MySlice[T] 写入了receiver中然后方法的返回参数我们使用了类型形参 T **(实际上如果有需要的话,方法的接收参数也可以实用类型形参)在方法的定义中,我们也可以使用类型形参 T (在这个例子里,我们通过 var sum T 定义了一个新的变量 sum )对于这个泛型类型 MySlice[T] 我们该如何使用?还记不记得之前强调过很多次的,泛型类型无论如何都需要先用类型实参实例化,所以用法如下:

var s MySlice[int] = []int{1, 2, 3, 4}
fmt.Println(s.Sum()) // 输出:10

var s2 MySlice[float32] = []float32{1.0, 2.0, 3.0, 4.0}
fmt.Println(s2.Sum()) // 输出:10.0

通过泛型receiver,泛型的实用性一下子得到了巨大的扩展。

基于泛型的队列

队列是一种先入先出的数据结构,它和现实中排队一样,数据只能从队尾放入、从队首取出,先放入的数据优先被取出来

// 这里类型约束使用了空接口,代表的意思是所有类型都可以用来实例化泛型类型 Queue[T] (关于接口在后半部分会详细介绍)
type Queue[T interface{}] struct {
    elements []T
}

// 将数据放入队列尾部
func (q *Queue[T]) Put(value T) {
    q.elements = append(q.elements, value)
}

// 从队列头部取出并从头部删除对应数据
func (q *Queue[T]) Pop() (T, bool) {
    var value T
    if len(q.elements) == 0 {
        return value, true
    }

    value = q.elements[0]
    q.elements = q.elements[1:]
    return value, len(q.elements) == 0
}

// 队列大小
func (q Queue[T]) Size() int {
    return len(q.elements)
}

💡 为了方便说明,上面是队列非常简单的一种实现方法,没有考虑线程安全等很多问题
Queue[T] 因为是泛型类型,所以要使用的话必须实例化,实例化与使用方法如下所示:

var q1 Queue[int]  // 可存放int类型数据的队列
q1.Put(1)
q1.Put(2)
q1.Put(3)
q1.Pop() // 1
q1.Pop() // 2
q1.Pop() // 3

var q2 Queue[string]  // 可存放string类型数据的队列
q2.Put("A")
q2.Put("B")
q2.Put("C")
q2.Pop() // "A"
q2.Pop() // "B"
q2.Pop() // "C"

var q3 Queue[struct{Name string}] 
var q4 Queue[[]int] // 可存放[]int切片的队列
var q5 Queue[chan int] // 可存放int通道的队列
var q6 Queue[io.Reader] // 可存放接口的队列
// ......

动态判断变量的类型

使用接口的时候经常会用到类型断言或 type swith 来确定接口具体的类型,然后对不同类型做出不同的处理,如:

var i interface{} = 123
i.(int) // 类型断言

// type switch
switch i.(type) {
    case int:
        // do something
    case string:
        // do something
    default:
        // do something
    }
}

泛型函数

在介绍完泛型类型和泛型receiver之后,我们来介绍最后一个可以使用泛型的地方——泛型函数。有了上面的知识,写泛型函数也十分简单。假设我们想要写一个计算两个数之和的函数:

func Add(a int, b int) int {
    return a + b
}

这个函数理所当然只能计算int的和,而浮点的计算是不支持的。这时候我们可以像下面这样定义一个泛型函数:

func Add[T int | float32 | float64](a T, b T) T {
    return a + b
}

上面就是泛型函数的定义。

这种带类型形参的函数被称为泛型函数它和普通函数的点不同在于函数名之后带了类型形参。这里的类型形参的意义、写法和用法因为与泛型类型是一模一样的,就不再赘述了。

和泛型类型一样,泛型函数也是不能直接调用的,要使用泛型函数的话必须传入类型实参之后才能调用。

Add[int](1,2) // 传入类型实参int,计算结果为 3
Add[float32](1.0, 2.0) // 传入类型实参float32, 计算结果为 3.0

Add[string]("hello", "world") // 错误。因为泛型函数Add的类型约束中并不包含string

或许你会觉得这样每次都要手动指定类型实参太不方便了。所以Go还支持类型实参的自动推导:

Add(1, 2)  // 1,2是int类型,编译请自动推导出类型实参T是int
Add(1.0, 2.0) // 1.0, 2.0 是浮点,编译请自动推导出类型实参T是float32

自动推导的写法就好像免去了传入实参的步骤一样,但请记住这仅仅只是编译器帮我们推导出了类型实参,实际上传入实参步骤还是发生了的。

既然支持泛型函数,那么泛型方法呢?

既然函数都支持泛型了,那你应该自然会想到,方法支不支持泛型?很不幸,目前Go的方法并不支持泛型,如下:

type A struct {
}

// 不支持泛型方法
func (receiver A) Add[T int | float32 | float64](a T, b T) T {
    return a + b
}

但是因为receiver支持泛型, 所以如果想在方法中使用泛型的话,目前唯一的办法就是曲线救国,迂回地通过receiver使用类型形参:

type A[T int | float32 | float64] struct {
}

// 方法可以使用类型定义中的形参 T 
func (receiver A[T]) Add(a T, b T) T {
    return a + b
}

// 用法:
var a A[int]
a.Add(1, 2)

var aa A[float32]
aa.Add(1.0, 2.0)

变得复杂的接口

有时候使用泛型编程时,我们会书写长长的类型约束,如下:

// 一个可以容纳所有int,uint以及浮点类型的泛型切片
type Slice[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64] []T理所当然,这种写法是我们无法忍受也难以维护的,而Go支持将类型约束单独拿出来定义到接口中,从而让代码更容易维护:

type IntUintFloat interface {
    int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}

type Slice[T IntUintFloat] []T

这段代码把类型约束给单独拿出来,写入了接口类型 IntUintFloat 当中。需要指定类型约束的时候直接使用接口 IntUintFloat 即可。

不过这样的代码依旧不好维护,而接口和接口、接口和普通类型之间也是可以通过 | 进行组合:

type Int interface {
    int | int8 | int16 | int32 | int64
}

type Uint interface {
    uint | uint8 | uint16 | uint32
}

type Float interface {
    float32 | float64
}

type Slice[T Int | Uint | Float] []T // 使用 ‘|’ 将多个接口类型组合上面的代码中,我们分别定义了 Int, Uint, Float 三个接口类型,并最终在 Slice[T] 的类型约束中通过使用 | 将它们组合到一起。

同时,在接口里也能直接组合其他接口,所以还可以像下面这样:

type SliceElement interface {
    Int | Uint | Float | string // 组合了三个接口类型并额外增加了一个 string 类型
}

type Slice[T SliceElement] []T 

Go基础:一文掌握Go语言泛型的使用 Go 1.18引入泛型是Go语言的重大更新,支持类型参数、约束等概念,允许编写处理多种类型的通用代码。泛型函数和结构体可减少重复代码,如通用求和函数、栈/队列实现等。实际应用中,泛型适合集合操作、数据结构和工具函数等场景,但需合理使用类型约束,避免过度泛化。结合接口使用可提升代码复用性,同时保持类型安全和可维护性。泛型显著提升了Go代码的灵活性和扩展性,是处理通用需求的强大工具。 阅读详情

相关推荐

99、Go语言中的接口与泛型详解

本文详细解析了Go语言中的接口与泛型特性,包括它们的基本概念、使用方法以及结合实际项目中的高级应用。通过丰富的示例代码,展示了如何利用接口实现多态性、抽象化和依赖注入,并通过泛型提高代码的通用性和复用性。此外,还介绍了接口与泛型在工厂模式、策略模式、装饰器模式等设计模式中的灵活运用,帮助开发者编写高效、可靠的Go代码。

139

Go 1.18 泛型全面讲解:一篇讲清泛型的全部

GO语言1.18版本新增功能泛型原理

nihaihaoma的专栏 1万+

36、Go语言中的泛型编程实践

本文详细介绍了Go语言中的泛型编程,包括其基本概念、使用方法以及实际应用场景。通过泛型,可以编写更加通用和灵活的代码,而不需要为每种类型重复编写相同的逻辑。文章涵盖了泛型类型和函数的定义与使用,还探讨了泛型在数据结构、算法、并发编程和错误处理中的具体应用。同时分析了泛型的优势和局限性,并展望了未来Go语言中泛型的发展方向。希望这篇文章能够帮助开发者更好地理解和应用Go语言中的泛型编程。

119

go语言基础之泛型

当我们编写的是操作 Go 语言定义的特殊容器类型(slice、map和chennel)的函数。如果函数具有包含这些类型的参数,并且函数的代码并不关心元素的类型,那么使用类型参数可能是有用的。例如:返回任何类型map中所有的key// MapKeys 返回m中所有key组成的切片return s。

liu289747235的专栏 4148

35、Go语言中的泛型与数据结构

本文深入探讨了Go语言中泛型的特性及其在数据结构、并发编程、错误处理和设计模式中的应用。从泛型函数和类型的定义到实际项目中的灵活使用,内容覆盖全面,旨在帮助开发者提升代码的复用性和可维护性,并结合性能优化策略确保高效开发。

65

Go语言中的泛型是什么?

在很多编程语言中,泛型是一项重要的功能,因为它可以提高代码的可重用性和灵活性。总结起来,Go语言中的泛型是一项强大的特性,它允许我们编写更通用、更灵活的代码。通过使用泛型,我们可以将特定类型的代码抽象为通用的代码,从而提高代码的可重用性和可维护性。通过在函数、结构体、接口等定义中使用类型参数,可以在编写代码时将特定的类型替换为泛型类型。通过使用类型参数,我们可以创建通用的数据结构和接口,从而提高代码的灵活性和可复用性。这样,我们可以在不同的上下文中使用相同的函数,而不需要为不同的类型编写重复的代码。

MxTransforms的博客 98

Go 1.18新特性——泛型

泛型是一种编程范式,它将类型参数化,可以用于编写灵活且可重用的函数或数据结构,这样我们就可以用自己的类型来代替那些被硬编码的类型。泛型的好处是可以在编译时期发现类型不匹配的错误,而不是在运行时期。

m0_54644067的博客 2661

82、Go语言中的泛型与集合类型详解

本文详细介绍了Go语言中泛型的引入及其在集合类型中的应用。从泛型的基础知识到具体实践,包括泛型栈、队列、数组、切片、映射和通道等数据结构的实现,还探讨了泛型与接口、并发编程以及设计模式的结合使用,帮助开发者更好地理解和运用泛型来优化代码复用性、可维护性和性能。

192

一文了解Go泛型

泛型是一种编程语言的特性,允许在编写代码时不指定具体的数据类型,而在运行时动态确定。这种特性使得我们能够编写更加灵活、通用的代码,而不必为每种数据类型编写相似的逻辑。

weixin_53623989的博客 2587

38、Go语言中的泛型与容器类型实践

本文详细探讨了Go语言中泛型的引入及其在容器类型中的应用,重点展示了如何使用泛型实现高效的栈、队列、排序列表、二叉树和多映射等数据结构。文章还分析了不同实现方式的性能差异,并通过实际案例说明了这些数据结构在Web开发、算法实现、任务调度和表达式求值等场景中的用途。此外,还介绍了在并发环境下确保泛型数据结构线程安全的方法以及相关的性能优化技巧。

79

93、Go语言中的泛型与并发编程实践

本文深入探讨了Go语言中的泛型与并发编程,详细介绍了泛型类型、函数和接口的使用,并结合实际代码示例展示如何构建高效并发程序。同时涵盖了Go模块管理、错误处理机制以及高级语言特性如反射和接口的综合应用。通过这些内容,帮助开发者更好地掌握Go语言在现代软件开发中的最佳实践和应用场景。

110

Go语言中的泛型

本文介绍了Go中泛型的基础知识。使用泛型,你可以声明和使用具有为调用代码提供的一组类型的函数或类型。在下面,我们将声明两个简单的非泛型函数,然后我们用一个泛型函数来实现相同的功能。

胡海龙 998

61、Go语言中的泛型与数据结构实现

本文详细介绍了Go语言中泛型的引入及其在数据结构实现中的应用。通过具体代码示例,展示了如何利用泛型特性实现栈、队列、双向队列和优先队列等常见数据结构,并探讨了泛型在排序、搜索和图算法中的使用。同时讨论了泛型与接口结合、错误处理、并发编程及性能优化的最佳实践,旨在帮助开发者提高代码复用性和开发效率。

80

Go语言中的未来:从泛型到WebAssembly

Go语言的未来充满了可能性,从泛型到WebAssembly,这些新特性为Go语言带来了更多的应用场景和发展空间。作为一个务实的后端开发者,我建议关注Go语言的发展趋势,及时学习和应用新特性,提升自己的技术能力。记住:技术在不断发展,我们也需要不断学习和进步。我见过不少开发者,对新特性持怀疑态度,不愿意尝试新东西。其实,新特性的出现往往是为了解决实际问题,我们应该保持开放的心态,积极尝试和学习。泛型和WebAssembly只是Go语言发展的一部分,未来还会有更多的新特性和改进。

好代码自己会说话,烂代码才需要注释来圆谎 5820

86、Go语言中的接口与泛型:实现与应用

本博客详细探讨了Go语言中的接口与泛型的实现及应用,涵盖接口定义、实现和使用,以及泛型类型的定义、约束和优化。通过实例展示了接口和泛型的结合如何提升代码复用性和灵活性,并深入解析了泛型在实际开发中的应用场景,如并发处理、错误封装、网络编程和数据库操作等。

159

Agent 团队管理手册:如何设定目标、分配角色与解决冲突

本文从Agent团队的底层逻辑出发,完整讲解了Agent团队管理的三大核心模块:目标设定(分层拆解+对齐校验)、角色分配(能力匹配+最优算法)、冲突处理(分类处理+仲裁投票),并提供了可直接运行的完整代码示例。通过本文的方法,你可以快速搭建一个高效协作的Agent团队,大幅提升复杂任务的完成效率和准确率。Agent团队是未来AI应用的核心形态,现在正是落地的黄金期,建议你从一个小场景开始动手实践,逐步优化自己的Agent团队管理方法论。

AI天才研究院 232

Go语言中的实现类似泛型的方法

Go语言中提供了一些方法来处理类似的泛型函数。 1。使用类型断言 2。Go语言反射功能 3。自定义类型接口和类型package main import ( "fmt" )func max(first interface{}, rest ...interface{}) interface{} { max := first for _, v := range rest {

Achiberx 8913

ADS与MATLAB数据互通全指南:从Snp文件导入到TXT导出完整流程

本文详细介绍了在射频与微波工程中,实现ADS与MATLAB高效数据互通的完整流程。核心内容包括利用SPOutput控件、数据显示窗口及测量方程等多种方法,将ADS仿真数据(如S参数)导出为Touchstone或TXT格式,并导入MATLAB进行深度分析与可视化;同时指导如何将MATLAB处理后的数据回灌至ADS,用于模型验证与协同仿真,旨在构建自动化、无损的数据交换工作流,提升工程师的工作效率。

pluto的博客 390

Go语言工程化实践,基于slices泛型的1024个项目中的复用模式总结

掌握Go 1.21 slices泛型特性,提炼1024个项目中的代码复用技巧,总结高频工程化复用模式。适用于集合操作、数据过滤与通用算法封装,提升开发效率与代码可维护性,值得收藏。

ByteShoal的博客 1045

【实战指南】OpenClaw 使用 Bird Skill 接入 X(Twitter)的终极省心方案:告别每次都要手动喂 Cookie 的痛苦

OpenClaw 使用 Bird Skill 接入 X(Twitter)的终极省心方案:告别每次都要手动喂 Cookie 的痛苦

hampeter的博客 1800

2024美团秋招硬件开发笔试真题及答案解析

在 Linux 内核的网络设备驱动中,数据链路层的功能通常由网络协议栈的低层(如以太网驱动)实现,而不是作为网络设备驱动的一个独立层次。在 FreeRTOS 中,任务通知(Task Notifications)是一种轻量级的事件传递机制,用于在任务之间或任务与中断之间传递信号。小美正在开发一个简单的密码生成器,其中一个功能是将输入的字符串中的所有字母按字母顺序循环前移一位(即 'a' 变为 'z','B' 变为 'A',以此类推)。它包含了网络设备的各种参数和方法,用于管理网络设备的初始化、配置和操作。

XU157303764的博客 1674
上一篇: go语言中的配置管理神器 --viper 二 实战
下一篇: 字节赫兹 框架教程 一 Engine
过去日记
过去日记 新星创作者: 后端开发技术领域 新星创作者: 后端开发技术领域
博客等级 码龄3年 5342粉丝 225原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

过去日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值