数据类型

基本数据类型

关键字说明
bool布尔类型
(u)int、(u)int8、(u)int16、(u)int32、(u)int64、uintptr整数,带u:无符号整数,不带u:有符号整数。有符号整数分为两类:1、规定长度,如:int8、int16。2、不规定长度,根据操作系统来,32为就是int32,64位就是int64。Golang 中整数没有 Long 之类的,直接使用 int64。uintptr:指针,长度根据操作系统来
float32、float64浮点类型
complex64、complex128复数类型
string字符串
rune字符类型。不像其他语言中的 char 类型,现在软件基本都是本着全球化去开发,采用 UTF-8 的居多,在 UTF-8 编码中很多字符都是三个字节,char 已经满足不了,往往会造成很多坑。Golang 中采用 rune 来做字符类型,占用 4 字节
byte字节

布尔类型

  Go 语言中的布尔类型与其他语言基本一致,关键字也为 bool,可赋值为预定义的 true 和 false,示例代码如下:

var v1 bool
v1 = true
v2 := (1 == 2) // v2也会被推导为bool类型

  布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。以下的示例是一些错误的用法,会导致编译错误:

var b bool
b = 1 // 编译错误
b = bool(1) // 编译错误

  以下的用法才是正确的:

var b bool
b = (1!=0) // 编译正确
fmt.Println("Result:", b) // 打印结果为Result: true

整型

  整型是所有编程语言里最基础的数据类型。Go 语言支持下表所示的这些整型类型。

类型长度(字节)值范围
int81-128~127
uint8(即byte)10~255
int162-32768~32767
uint1620~65535
int324-2147483648~2147483647
uint3240~4294967295
int648-9223372036854775808~9223372036854775807
uint6480~18446744073709551615
int平台相关平台相关
uint平台相关平台相关
uintptr同指针在32位平台下为4字节,64位平台下为8字节

类型表示

  需要注意的是,int 和 int32 在Go语言里被认为是两种不同的类型,编译器也不会帮你自动做类型转换,比如以下的例子会有编译错误:

var value2 int32
value1 := 64 // value1将会被自动推导为int类型
value2 = value1 // 编译错误

  编译错误类似于:

cannot use value1 (type int) as type int32 in assignment。

  使用强制类型转换可以解决这个编译错误:

value2 = int32(value1) // 编译通过

  当然,开发者在做强制类型转换时,需要注意数据长度被截短而发生的数据精度损失(比如将浮点数强制转为整数)和值溢出(值超过转换的目标类型的值范围时)问题。

数值运算

  Go 语言支持下面的常规整数运算:+-*/%。加减乘除就不详细解释了,需要说下的是,% 和在 C 语言中一样是求余运算,比如:

5 % 3 // 结果为:2

比较运算

  Go 语言支持以下的几种比较运算符:><==>=<=!=。这一点与大多数其他语言相同,与 C 语言完全一致。

  下面为条件判断语句的例子:

i, j := 1, 2
if i == j {
    fmt.Println("i and j are equal.")
}

  两个不同类型的整型数不能直接比较,比如 int8 类型的数和 int 类型的数不能直接比较,但各种类型的整型变量都可以直接与字面常量(literal)进行比较,比如:

var i int32
var j int64
i, j = 1, 2

if i == j { // 编译错误
    fmt.Println("i and j are equal.")
}

if i == 1 || j == 2 { // 编译通过
    fmt.Println("i and j are equal.")
}

位运算

运算含义样例
x << y左移124<<2 // 结果为496
x >> y右移124 >> 2 // 结果为31
x ^ y异或124 ^ 2 // 结果为126
x & y124 & 2 // 结果为0
x | y124 | 2 // 结果为126
^x取反^2 // 结果为-3

  Go 语言的大多数位运算符与 C 语言都比较类似,除了取反在 C 语言中是 ~x,而在Go语言中是 ^x

浮点型

  浮点型用于表示包含小数点的数据,比如1.234就是一个浮点型数据。Go 语言中的浮点类型采用 IEEE-754 标准的表达方式。

浮点数表示

  Go 语言定义了两个类型 float32float64,其中 float32 等价于 C 语言的 float 类型,float64 等价于 C 语言的 double 类型。

  在 Go 语言里,定义一个浮点数变量的代码如下:

var fvalue1 float32

fvalue1 = 12
fvalue2 := 12.0 // 如果不加小数点,fvalue2会被推导为整型而不是浮点型

  对于以上例子中类型被自动推导的 fvalue2,需要注意的是其类型将被自动设为 float64,而不管赋给它的数字是否是用 32 位长度表示的。因此,对于以上的例子,下面的赋值将导致编译错误:

fvalue1 = fvalue2

  而必须使用这样的强制类型转换:

fvalue1 = float32(fvalue2)

浮点数比较

  因为浮点数不是一种精确的表达方式,所以像整型那样直接用 == 来判断两个浮点数是否相等是不可行的,这可能会导致不稳定的结果。

  下面是一种推荐的替代方案:

import "math"

// p为用户自定义的比较精度,比如0.00001
func IsEqual(f1, f2, p float64) bool {
    return math.Fdim(f1, f2) < p
}

复数类型

  复数实际上由两个实数(在计算机中用浮点数表示)构成,一个表示实部(real),一个表示
虚部(imag)。如果了解了数学上的复数是怎么回事,那么 Go 语言的复数就非常容易理解了。

复数表示

  复数表示的示例如下:

var value1 complex64 // 由2个float32构成的复数类型
value1 = 3.2 + 12i
value2 := 3.2 + 12i // value2是complex128类型
value3 := complex(3.2, 12) // value3结果同 value2

实部与虚部

  对于一个复数 z = complex(x, y),就可以通过 Go 语言内置函数 real(z) 获得该复数的实部,也就是 x,通过 imag(z) 获得该复数的虚部,也就是 y。

  更多关于复数的函数,请查阅 math/cmplx 标准库的文档。

字符串

  在 Go 语言中,字符串也是一种基本类型。相比之下, C/C++ 语言中并不存在原生的字符串类型,通常使用字符数组来表示,并以字符指针来传递。

  Go 语言中字符串的声明和初始化非常简单,举例如下:

var str string // 声明一个字符串变量
str = "Hello world" // 字符串赋值
ch := str[0] // 取字符串的第一个字符
fmt.Printf("The length of \"%s\" is %d \n", str, len(str))
fmt.Printf("The first character of \"%s\" is %c.\n", str, ch)

  输出结果为:

The length of "Hello world" is 11
The first character of "Hello world" is H.

  字符串的内容可以用类似于数组下标的方式获取,但与数组不同,字符串的内容不能在初始化后被修改,比如以下的例子:

str := "Hello world" // 字符串也支持声明时进行初始化的做法
str[0] = 'X' // 编译错误

  编译器会报类似如下的错误:

cannot assign to str[0]

  在这个例子中我们使用了一个 Go 语言内置的函数 len() 来取字符串的长度。这个函数非常有用,我们在实际开发过程中处理字符串、数组和切片时将会经常用到。

  还顺便用了 Printf() 函数的用法。有 C 语言基础的读者会发现,Printf() 函数的用法与 C 语言运行库中的 printf() 函数如出一辙。在以后学习更多的 Go 语言特性时,可以配合使用Println()和Printf()来打印各种自己感兴趣的信息,从而让学习过程更加直观、有趣。

  Go 编译器支持 UTF-8 的源代码文件格式。这意味着源代码中的字符串可以包含非 ANSI 的字符,比如”Hello world. 你好,世界!”可以出现在 Go 代码中。但需要注意的是,如果你的 Go 代码需要包含非 ANSI 字符,保存源文件时请注意编码格式必须选择 UTF-8。特别是在 Windows 下一般编辑器都默认存为本地编码,比如中国地区可能是 GBK 编码而不是 UTF-8,如果没注意这点在编译和运行时就会出现一些意料之外的情况。

  字符串的编码转换是处理文本文档(比如TXT、XML、HTML等)非常常见的需求,不过可惜的是 Go 语言仅支持 UTF-8 和 Unicode 编码。对于其他编码,Go 语言标准库并没有内置的编码转换支持。不过,所幸的是我们可以很容易基于 iconv 库用 Cgo 包装一个。这里有一个开源项目:
https://github.com/xushiwei/go-iconv

字符串操作

  平时常用的字符串操作如下表所示。

运算含义样例
x + y字符串连接“Hello” + “123” // 结果为Hello123
len(s)字符串长度len(“Hello”) // 结果为5
s[i]取字符“Hello”[1] // 结果为’e’

  更多的字符串操作,请参考标准库 strings 包。

字符串遍历

  Go 语言支持两种方式遍历字符串。一种是以字节数组的方式遍历:

str := "Hello,世界"
n := len(str)

for i := 0; i < n; i++ {
    ch := str[i] // 依据下标取字符串中的字符,类型为byte
    fmt.Println(i, ch)
}

  这个例子的输出结果为:

0 72
1 101
2 108
3 108
4 111
5 44
6 32
7 228
8 184
9 150
10 231
11 149
12 140

  可以看出,这个字符串长度为13。尽管从直观上来说,这个字符串应该只有9个字符。这是因为每个中文字符在 UTF-8 中占 3 个字节,而不是 1 个字节。

  另一种是以 Unicode 字符遍历:

str := "Hello,世界"

for i, ch := range str {
    fmt.Println(i, ch)//ch的类型为rune
}

  输出结果为:

0 72
1 101
2 108
3 108
4 111
5 44
6 32
7 19990
10 30028

  以 Unicode 字符方式遍历时,每个字符的类型是 rune(早期的 Go 语言用 int 类型表示 Unicode 字符),而不是 byte。

字符类型

  在 Go 语言中支持两个字符类型,一个是 byte(实际上是 uint8 的别名),代表 UTF-8 字符串的单个字节的值;另一个是 rune,代表单个 Unicode 字符。

  关于 rune 相关的操作,可查阅 Go 标准库的 unicode 包。另外 unicode/utf8 包也提供了
UTF8 和 Unicode 之间的转换。

  出于简化语言的考虑,Go 语言的多数 API 都假设字符串为 UTF-8 编码。尽管 Unicode 字符在标
准库中有支持,但实际上较少使用。

复合数据类型

关键字说明
pointer指针
array数组
slice切片,可以理解为动态数组
map字典
chan通道,用来做协程之间通信使用
struct结构体
interface接口

数组

  数组是 Go 语言编程中最常用的数据结构之一。顾名思义,数组就是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素(element),一个数组包含的元素个数被称为数组的长度。

  以下为一些常规的数组声明方法:

[32]byte // 长度为32的数组,每个元素为一个字节
[2*N] struct { x, y int32 } // 复杂类型数组
[1000]*float64 // 指针数组
[3][5]int // 二维数组
[2][2][2]float64 // 等同于[2]([2]([2]float64))

  从以上类型也可以看出,数组可以是多维的,比如 [3][5]int 就表达了一个 3行5列 的二维整型数组,总共可以存放15个整型元素。

  在 Go 语言中,数组长度在定义后就不可更改,在声明时长度可以为一个常量或者一个常量表达式(常量表达式是指在编译期即可计算结果的表达式)。数组的长度是该数组类型的一个内置常量,可以用 Go 语言的内置函数 len() 来获取。下面是一个获取数组 arr 元素个数的写法:

arrLength := len(arr)

元素访问

  可以使用数组下标来访问数组中的元素。与 C 语言相同,数组下标从 0 开始,len(array) - 1 则表示最后一个元素的下标。下面的示例遍历整型数组并逐个打印元素内容:

for i := 0; i < len(array); i++ {
    fmt.Println("Element", i, "of array is", array[i])
}

  Go 语言还提供了一个关键字 range,用于便捷地遍历容器中的元素。当然,数组也是 range 的支持范围。上面的遍历过程可以简化为如下的写法:

for i, v := range array {
    fmt.Println("Array element[", i, "]=", v)
}

  在上面的例子里可以看到,range 具有两个返回值,第一个返回值是元素的数组下标,第二个返回值是元素的值。

值类型

  需要特别注意的是,在 Go 语言中数组是一个值类型(value type)。所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据复制。因此,在函数体中无法修改传入的数组的内容,因为函数内操作的只是所传入数组的一个副本。

  下面用例子来说明这一特点:

package main
import "fmt"

func modify(array [10]int) {
    array[0] = 10 // 试图修改数组的第一个元素
    fmt.Println("In modify(), array values:", array)
}
func main() {
    array := [5]int{1,2,3,4,5} // 定义并初始化一个数组
    modify(array) // 传递给一个函数,并试图在函数体内修改这个数组内容
    fmt.Println("In main(), array values:", array)
}

  该程序的执行结果为:

In modify(), array values: [10 2 3 4 5]
In main(), array values: [1 2 3 4 5]

  从执行结果可以看出,函数 modify() 内操作的那个数组跟 main() 中传入的数组是两个不同的实例。那么,如何才能在函数内操作外部的数据结构呢?采用指针,这里不做过多獒述。

数组切片

  在上面里已经提过数组的特点:数组的长度在定义之后无法再次修改;数组是值类型,每次传递都将产生一份副本。显然这种数据结构无法完全满足开发者的真实需求。

  不用失望,Go 语言提供了数组切片(slice)这个非常酷的功能来弥补数组的不足。

  初看起来,数组切片就像一个指向数组的指针,实际上它拥有自己的数据结构,而不仅仅是个指针。数组切片的数据结构可以抽象为以下3个变量:

  • 一个指向原生数组的指针;
  • 数组切片中的元素个数;
  • 数组切片已分配的存储空间。

  从底层实现的角度来看,数组切片实际上仍然使用数组来管理元素,因此它们之间的关系让 C++ 程序员们很容易联想起 STL 中 std::vector 和数组的关系。基于数组,数组切片添加了一系列管理功能,可以随时动态扩充存放空间,并且可以被随意传递而不会导致所管理的元素被重复复制。

创建数组切片

  创建数组切片的方法主要有两种——基于数组 和 直接创建,下面来简要介绍一下这两种方法。

  • 基于数组

  数组切片可以基于一个已存在的数组创建。数组切片可以只使用数组的一部分元素或者整个数组来创建,甚至可以创建一个比所基于的数组还要大的数组切片。下面代码演示了如何基于一个数组的前5个元素创建一个数组切片。

package main

import "fmt"

func main() {
    // 先定义一个数组
    var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    // 基于数组创建一个数组切片
    var mySlice []int = myArray[:5]
    fmt.Println("Elements of myArray: ")

    for _, v := range myArray {
        fmt.Print(v, " ")
    }

    fmt.Println("\nElements of mySlice: ")
    for _, v := range mySlice {
        fmt.Print(v, " ")
    }

    fmt.Println()
}

  运行结果为:

Elements of myArray:
1 2 3 4 5 6 7 8 9 10
Elements of mySlice:
1 2 3 4 5

  这里已经注意到,Go 语言支持用 myArray[first:last] 这样的方式来基于数组生成一个数组切片,而且这个用法还很灵活,比如下面几种都是合法的。

  基于 myArray 的所有元素创建数组切片:

mySlice = myArray[:]

  基于myArray的前5个元素创建数组切片:

mySlice = myArray[:5]

  基于从第5个元素开始的所有元素创建数组切片:

mySlice = myArray[5:]
  • 直接创建

  并非一定要事先准备一个数组才能创建数组切片。Go 语言提供的内置函数 make() 可以用于灵活地创建数组切片。下面的例子示范了直接创建数组切片的各种方法。

  创建一个初始元素个数为5的数组切片,元素初始值为0:

mySlice1 := make([]int, 5)

  创建一个初始元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间:

mySlice2 := make([]int, 5, 10)

  直接创建并初始化包含5个元素的数组切片:

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

  当然,事实上还会有一个匿名数组被创建出来,只是不需要我们来操心而已。

元素遍历

  操作数组元素的所有方法都适用于数组切片,比如数组切片也可以按下标读写元素,用 len() 函数获取元素个数,并支持使用 range 关键字来快速遍历所有元素。

  传统的元素遍历方法如下:

for i := 0; i <len(mySlice); i++ {
    fmt.Println("mySlice[", i, "] =", mySlice[i])
}

  使用 range 关键字可以让遍历代码显得更整洁。range 表达式有两个返回值,第一个是索引,第二个是元素的值:

for i, v := range mySlice {
    fmt.Println("mySlice[", i, "] =", v)
}

  对比上面的两个方法,我们可以很容易地看出使用 range 的代码更简单易懂。

动态增减元素

  可动态增减元素是数组切片比数组更为强大的功能。与数组相比,数组切片多了一个存储能力(capacity)的概念,即元素个数和分配的空间可以是两个不同的值。合理地设置存储能力的值,可以大幅降低数组切片内部重新分配内存和搬送内存块的频率,从而大大提高程序性能

  假如你明确知道当前创建的数组切片最多可能需要存储的元素个数为50,那么如果你设置的存储能力小于50,比如20,那么在元素超过20时,底层将会发生至少一次这样的动作——重新分配一块“够大”的内存,并且需要把内容从原来的内存块复制到新分配的内存块,这会产生比较明显的开销。给“够大”这两个字加上引号的原因是系统并不知道多大才是够大,所以只是一个简单的猜测。比如,将原有的内存空间扩大两倍,但两倍并不一定够,所以之前提到的内存重新分配和内容复制的过程很有可能发生多次,从而明显降低系统的整体性能。但如果你知道最大是50并且一开始就设置存储能力为50,那么之后就不会发生这样非常耗费 CPU 的动作,从而达到空间换时间的效果。

  数组切片支持 Go 语言内置的 cap() 函数和 len() 函数,下面代码简单示范了这两个内置函数的用法。可以看出,cap() 函数返回的是数组切片分配的空间大小,而 len() 函数返回的是数组切片中当前所存储的元素个数。

package main

import "fmt"

func main() {
    mySlice := make([]int, 5, 10)
    fmt.Println("len(mySlice):", len(mySlice))
    fmt.Println("cap(mySlice):", cap(mySlice))
}

  该程序的输出结果为:

len(mySlice): 5
cap(mySlice): 10

  如果需要往上例中 mySlice 已包含的5个元素后面继续新增元素,可以使用 append() 函数。下面的代码可以从尾端给 mySlice 加上3个元素,从而生成一个新的数组切片:

mySlice = append(mySlice, 1, 2, 3)

  函数 append() 的第二个参数其实是一个不定参数,我们可以按自己需求添加若干个元素,甚至直接将一个数组切片追加到另一个数组切片的末尾:

mySlice2 := []int{8, 9, 10}
// 给mySlice后面添加另一个数组切片
mySlice = append(mySlice, mySlice2...)

  需要注意的是,我们在第二个参数 mySlice2 后面加了三个点,即一个省略号,如果没有这个省略号的话,会有编译错误,因为按 append() 的语义,从第二个参数起的所有参数都是待附加的元素。因为 mySlice 中的元素类型为 int,所以直接传递 mySlice2 是行不通的。加上省略号相当于把 mySlice2 包含的所有元素打散后传入。

  上述调用等同于:

mySlice = append(mySlice, 8, 9, 10)

  数组切片会自动处理存储空间不足的问题。如果追加的内容长度超过当前已分配的存储空间(即 cap() 调用返回的信息),数组切片会自动分配一块足够大的内存。

基于数组切片创建数组切片

  类似于数组切片可以基于一个数组创建,数组切片也可以基于另一个数组切片创建。下面的例子基于一个已有数组切片创建新数组切片:

oldSlice := []int{1, 2, 3, 4, 5}
newSlice := oldSlice[:3] // 基于oldSlice的前3个元素构建新数组切片

  有意思的是,选择的 oldSlicef 元素范围甚至可以超过所包含的元素个数,比如 newSlice 可以基于 oldSlice 的前6个元素创建,虽然 oldSlice 只包含5个元素。只要这个选择的范围不超过 oldSlice 存储能力(即 cap() 返回的值),那么这个创建程序就是合法的。newSlice 中超出 oldSlice 元素的部分都会填上0。

内容复制

  数组切片支持 Go 语言的另一个内置函数 copy(),用于将内容从一个数组切片复制到另一个数组切片。如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行复制。下面的示例展示了 copy() 函数的行为:

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

copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

map

  在 C++/Java 中,map 一般都以库的方式提供,比如在 C++ 中是 STL 的 std::map<>,在 C# 中是 Dictionary<>,在 Java 中是 Hashmap<>,在这些语言中,如果要使用 map,事先要引用相应的库。而在 Go 中,使用 map 不需要引入任何库,并且用起来也更加方便。

  map 是一堆键值对的未排序集合。比如以身份证号作为唯一键来标识一个人的信息,则这个 map 可以定义为如下的方式。

package main

import "fmt"

// PersonInfo是一个包含个人详细信息的类型
type PersonInfo struct {
    ID string
    Name string
    Address string
}

func main() {
    var personDB map[string] PersonInfo
    personDB = make(map[string] PersonInfo)

    // 往这个map里插入几条数据
    personDB["12345"] = PersonInfo{"12345", "Tom", "Room 203,..."}
    personDB["1"] = PersonInfo{"1", "Jack", "Room 101,..."}

    // 从这个map查找键为"1234"的信息
    person, ok := personDB["1234"]
    // ok是一个返回的bool型,返回true表示找到了对应的数据
    if ok {
        fmt.Println("Found person", person.Name, "with ID 1234.")
    } else {
        fmt.Println("Did not find person with ID 1234.")
    }
}

  上面这个简单的例子基本上已经覆盖了 map 的主要用法,下面对其中的关键点进行细述。

变量声明

  map 的声明基本上没有多余的元素,比如:

var myMap map[string] PersonInfo

  其中,myMap 是声明的 map 变量名,string 是键的类型,PersonInfo 则是其中所存放的值类型。

创建

  我们可以使用 Go 语言内置的函数 make() 来创建一个新 map。下面的这个例子创建了一个键类型为 string、 值类型为 PersonInfo 的 map:

myMap = make(map[string] PersonInfo)

  也可以选择是否在创建时指定该 map 的初始存储能力,下面的例子创建了一个初始存储能力为100的 map:

myMap = make(map[string] PersonInfo, 100)

  创建并初始化 map 的代码如下:

myMap = map[string] PersonInfo{
    "1234": PersonInfo{"1", "Jack", "Room 101,..."},
}

元素赋值

  赋值过程非常简单明了,就是将键和值用下面的方式对应起来即可:

myMap["1234"] = PersonInfo{"1", "Jack", "Room 101,..."}

元素删除

  Go 语言提供了一个内置函数 delete(),用于删除容器内的元素。下面简单介绍一下如何用 delete() 函数删除 map 内的元素:

delete(myMap, "1234")

  上面的代码将从 myMap 中删除键为“1234”的键值对。如果“1234”这个键不存在,那么这个调用将什么都不发生,也不会有什么副作用。但是如果传入的 map 变量的值是nil,该调用将导致程序抛出异常(panic)。

元素查找

  在 Go 语言中,map 的查找功能设计得比较精巧。而在其他语言中,我们要判断能否获取到一个值不是件容易的事情。判断能否从 map 中获取一个值的常规做法是:

  • 声明并初始化一个变量为空;
  • 试图从 map 中获取相应键的值到该变量中;
  • 判断该变量是否依旧为空,如果为空则表示 map 中没有包含该变量。

  这种用法比较啰唆,而且判断变量是否为空这条语句并不能真正表意(是否成功取到对应的值),从而影响代码的可读性和可维护性。有些库甚至会设计为因为一个键不存在而抛出异常,让开发者用起来胆战心惊,不得不一层层嵌套 try-catch 语句,这更是不人性化的设计。在 Go 语言中,要从 map 中查找一个特定的键,可以通过下面的代码来实现:

value, ok := myMap["1234"]
if ok { // 找到了
    // 处理找到的value
}

  判断是否成功找到特定的键,不需要检查取到的值是否为 nil,只需查看第二个返回值 ok,这让表意清晰很多。配合 := 操作符,让你的代码没有多余成分,看起来非常清晰易懂。

  参考:《Go语言编程》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值