Golang 25 个保留关键字

1.概览

Go 共有 25 个保留关键字,各有其作用,不能用作标识符。

Go 的 25 个关键字按照作用可以分为 3 类,分别为包管理、程序实体声明与定义和程序流程控制。

包管理(2个):
	import	package

程序实体声明与定义(8个):
	chan	const	func	interface	map	struct	type	var

程序流程控制(15个):
	break	case	continue	default	defer	else	fallthrough	
	for		go		goto		if		range	return	select		switch

2.包管理

2.1 import

import 用于导入包,这样就可以使用包中被导出的标识符。导入格式如下:

import _ "package path"
import . "package path"
import alias "package path"

import (
	_ "package path"
	. "package path"
	alias "package path"
)

其中包路径前面可以有三中修饰符中的某一个。下划线即空白标识符,表示不使用包中的标识符,只需要包的副作用,即计算包级变量的初始化表达式和执行导入包的init初始化函数。点号代替包的别名, 表示访问包中的导出标识符无需使用包名。alias 表示包的别名。

导入示例如下:

导入声明						Sin的本地名
import   "lib/math"         math.Sin
import m "lib/math"         m.Sin
import . "lib/math"         Sin

2.2 package

package 用于声明包的名称,需放在go文件所有代码的最前面。一个包由一个或多个go源文件组成,需放在同一个目录下,且同一个目录下的这些 go 文件的 package 的名字只能有一个。

申明格式如下:

package <packagename>

packagename 不能为空白标识符 _。

3.程序实体声明与定义

3.1 chan

chan 用于声明信道(channel)。

信道提供一种机制使两个并发执行的函数实现同步,并通过传递具体元素类型的值来通信。

channel 按照接收和发送数据分为三种。

chan T      	// 可以被用来发送和接收类型 T 的值
chan<- T  		// 只能被用来发送类型 T 的值
<-chan T      	// 只能被用来接收类型 T 的值

其中,T 表示 channel 中元素的类型。

其中 <- 操作符指定信道的方向,发送或接收。若没有给定方向,那么该信道是双向的。信道可通过类型转换或赋值被强制为只发送或只接收。

在 Golang 中,可以使用 make 函数初始化 channel。

ch := make(chan T) 		// 无缓冲信道
ch := make(chan T, 0)	// 无缓冲信道
ch := make(chan T, 100)	// 带缓冲信道

其结果值充当了对底层数据结构的引用。初始化时可以为信道设置缓冲区大小,默认值是零,表示不带缓冲的或同步的信道。

未初始化的信道值为 nil。

var ch chan T  // ch 为 nil

channel 可以使用以下操作来实现同步和通信:

  • 发送操作: 用于将数据发送到channel中,语法为 ch <- data。
  • 接收操作: 用于从channel中接收数据,语法为 data := <- ch。
  • 关闭操作: 用于关闭 channel,语法为 close(ch)。

需要注意的是,发送和接收操作都是阻塞的,即如果没有 goroutine 同时进行对应的操作,它们将一直阻塞,直到其他 goroutine 进行操作为止。而关闭操作是非阻塞的。即使 channel 已经被关闭,仍然可以从中读取数据。

3.2 const

const 用于申明常量,需指明初始值,一旦创建不可修改。

由于编译时的限制, 定义它们的表达式必须也是可被编译器求值的常量表达式。例如 1<<3 就是一个常量表达式,而 math.Sin(math.Pi/4) 则不是,因为对 math.Sin 的函数调用在运行时才会发生。

const name T = value					// 指明类型
const name0, name1 T = value0, value1	// 指明类型,定义多个常量
const name = value						// 无类型常量
const name0, name1 = value0, value1		// 无类型常量,可定义多个

// const name0 T, name1 T = value0, value1	// 错误,const 不能在同一行出现多个类型名(同一类型也不行)

// 将常量定义放在小括号中
const (
	name0 = value0
	name1 = value1
)

在小括号中的常量声明列表,const 常与 iota 常量生成器联用,用来申明连续的数值常量集。

// 无类型数值常量集(可转为整型或浮点型)
const (
	Sunday = iota		// 0
	Monday				// 1
	Tuesday				// 2
	Wednesday			// 3
	Thursday			// 4
	Friday				// 5
	Partyday			// 6
	numberOfDays 		// 7,该常量未导出
)

// 无类型数值常量集(可转为浮点型)
const (
	Sunday = iota + 0.1	// 0.1
	Monday				// 1.1
	Tuesday				// 2.1
	Wednesday			// 3.1
	Thursday			// 4.1
	Friday				// 5.1
	Partyday			// 6.1
	numberOfDays 		// 7.1,该常量未导出
)

关于 Go 的常量还需要知道:
(1)常量可以是类型化的或无类型化的。字面常量,true,false, iota 和某些只包含无类型化操作数的常量表达式是无类型化的;

(2)常量可由常量声明或类型转换显式地赋予其类型, 也可由变量声明或赋值以及作为表达式中的操作数隐式地赋予其类型。若常量的值不能由其类型表示就会产生一个错误。 例如,3.0 可赋予任何整数或浮点数类型的常量,而 2147483648.0 (等价于 1<<31)则只能赋予 float32, float64 或 uint32 类型的常量,而不能赋予 int32 或 string类型的常量;

(3)尽管数值常量在该语言中可拥有任意精度, 但编译器可能使用其有限精度的内部表示来实现它们。即,每个实现必须:

使用至少256位表示整数常量;
使用至少256位表示浮点常量,包括复数常量及尾数部分,和至少16位的有符号指数;
若无法精确表示一个整数常量,则给出一个错误;
若由于溢出而无法表示一个浮点或复数常量,则给出一个错误;
若由于精度限制而无法表示一个浮点或复数常量,则舍入为最近似的可表示常量。

这些要求适用于字面常量和常量表达式的求值结果。

注意,Golang 中的 const 不支持像 C/C++ 中修饰函数的参数和返回值,即下面的语句是非法的。

func test(const name *string)
func test(name *string) const *string

3.3 func

func 用于定义函数。Go 函数支持变参且返回值支持多个,但不支持默认参数。如果函数存在多个返回值形参则需要使用小括号括起来,定义格式如下:

func funcName(){}							//无参无返回值
func funcName(t T) T {}						//有参有返回值
func funcName(t T, list ...T) (T1,T1) {}	//有变参有多个返回值

代码格式上需要注意的是,函数体的第一个大括号必须函数名同行。这个是 Go 对代码格式的强制要求,在其它的语句中也是如此,比如 if else 语句、for 语句、switch 语句、select 语句等。

3.4 interface

interface 用于定义接口。一个接口是一个方法集,如果一个类型实现了一个接口中的所有方法集,那么说明该类型实现此接口。接口类型变量可以存储任何实现了该接口的类型的值。特别的,interface{}表示空接口类型,默认地,所有类型均实现了空接口,所以interface{}可以接收任意类型值。示例如下:

// 空接口
interface{}

// 一个简单的 File 接口
type File interface {
	Read(b Buffer) bool
	Write(b Buffer) bool
	Close()
}

3.5 map

map 用于声明映射变量。映射属容器类类型,是一个同种类型元素的无序组,通过唯一的键可以获取对应的值。可以使用 make 创建 map 变量,在定义 map 时可以省略容量,超出容量时会自动扩容,但尽量提供一个合理的初始值。未初始化的映射值为 nil。

注意: 由于 map 底层是一个 hash map,其并没有具体的容量,指定容量也是一个建议值,所以无门无法使用 cap() 函数来获取 map 的容量。

// 错误示例
func main() {
    m := make(map[string]int, 99)
    println(cap(m))     // error: invalid argument m1 (type map[string]int) for cap  
}    

3.5.1 创建 map

// 创建 0 容量的 map
var m = make(map[T1]T2) 
var m = map[T1]T2{}

// 创建指定容量的 map。注意指定的容量只是一个提示,map 并没有一个具体的容量。
var m = make(map[T1]T2, hint) 

// 创建并初始化 map
var m = map[string]int {
	"dable" : 27,
	"cat" : 28,
}

注意: 在定义 map 时可以省略容量,超出容量时会自动扩容,但尽量提供一个合理的初始值。注意指定的容量只是一个提示,map 并没有一个具体的容量,所以我们无法使用 cap() 函数来获取一个 map 的容量。

m := make(map[string]int, 100)
cap(m)     // error: invalid argument m (type map[string]int) for cap

使用示例:

package main

import "fmt"

func main() {
	nameAge := make(map[string]int)
	nameAge["bob"] = 18                     //增
	nameAge["tom"] = 16                     //增
	delete(nameAge, "bob")                  //删
	nameAge["tom"] = 19                     //改
	v := nameAge["tom"]                     //查
	fmt.Println("v=",v)
	v, ok := nameAge["tom"]                 //查,推荐用法
	if ok { 
		fmt.Println("v=",v,"ok=",ok)
	}   
	for k, v :=range nameAge {      		//遍历
		fmt.Println(k, v)
	}   
}

输出结果:

v= 19
v= 19 ok= true
tom 19

3.5.2 map 遍历

(1)遍历所有 key。

// 方式一
for k := range mapVar {
	...
}

// 方式二(不推荐)
for k, _ := range mapVar {
	...
}

(2)遍历所有 value。

for _, v := range mapVar {
	...
}

(3)遍历所有 key 与 value。

for k, v := range mapVar {
	...
}

注意,map 在没有被修改的情况下,使用 range 多次遍历 map 时输出的 key 和 value 的顺序可能不同。这是 Go 语言的设计者们有意为之,在每次 range 时的顺序被随机化,旨在提示开发者们,Go 底层实现并不保证 map 遍历顺序稳定,请大家不要依赖 range 遍历结果顺序。Golang 官方博文对此有详细说明:Go maps in action

3.5.3 map 增删改查

向 map 写入元素时,键值对不存在时会自动添加,键值存在时将被新值覆盖。使用 delete() 删除某键值对,使用 len() 获取元素个数。

// 新增或修改
m["name"] = "wade"
 
// 删除,key 不存在则啥也不干
delete(m, "name")
 
// 三种查询方式 
// 查询,key 不存在返回 value 类型的零值
v := m["name"]
v, ok := m["name"]
_, ok := m["name"]

3.5.4 注意事项

map 使用起来非常方便,但也有些必须要注意的地方,否则可能会导致程序异常甚至 panic。

(1)map 默认初始值为 nil。
map 声明时未初始化的情况下值为 nil。对 nil map 取值,返回对应类型的零值,不会引发 panic;但写入会 引发 panic,所以推荐做法是向 map 写入时先判断 map 是否为 nil;

(2)map range 的顺序是随机的。

(3)map 值传递表现出引用传递的效果。
Go 没有引用传递,只有值传递与指针传递。所以 map 作为函数实参传递时本质上也是值传递,只不过因为 map 底层数据结构是通过指针指向实际的元素存储空间,在被调函数中修改 map,对调用者同样可见,所以 map 作为函数实参传递时表现出了引用传递的效果。因此,传递 map 时,函数形参无需使用指针;

(4)map 的元素不可取址。
map 中的元素并不是一个变量,而是一个值,对 map 元素取值将报运行时错误,因此当 map 的元素为结构体类型的值,那么无法直接修改结构体中的字段值。如果想修改,有两个解决办法,一是存储 struct 的指针类型,二是使用临时变量,每次取出来后再设置回去;

(5)map 并发读写不安全。
需要加锁,或使用 sync.Map 代替,否则会引发 panic。

相关示例说明详见 Golang map 三板斧第一式:快速上手

3.6 struct

struct 用于定义结构体。结构体属容器类型,是多个相同或不同类型值的集合。

package main

import "fmt"

type Vertex struct {
	X, Y int
}

var (
	v1 = Vertex{1, 2}  // 类型为 Vertex
	v2 = Vertex{X: 1}  // Y:0 被省略
	v3 = Vertex{}      // X:0 和 Y:0
	p  = &Vertex{1, 2} // 类型为 *Vertex
)

func main() {
	fmt.Printf("%#v %#v %#v %#v\n", v1, v2, v3, p)
}

输出结果:

main.Vertex{X:1, Y:2} main.Vertex{X:1, Y:0} main.Vertex{X:0, Y:0} &main.Vertex{X:1, Y:2}

3.7 type

type 用于定义类型,比如定义 struct、interface、func 与等价类型。

// 定义struct
type Person struct { name string }

// 定义接口
type Person interface {
	speak(word string)
}

// 定义函数类型
type FuncType func(int, int) int

// 定义等价类型,rune等价于int32
type rune int32

3.8 var

var 用于申明函数级变量和包级变量。

var name T							// 指明类型,使用类型零值
var name T = value					// 指明类型,指明初始值
var name0, name1 T					// 指明类型,使用类型零值定义多个变量
var name0, name1 T = value0, value1	// 指明类型,指明初始值定义多个变量

var name = value					// 根据值推断变量类型
var name0, name1 = value0, value1	// 根据值推断变量类型,可定义多个不同类型变量

// var name0 T, name1 T				// 错误,var 不能在同一行出现多个类型名(同一类型也不行)

// 将变量定义放在括号中
var (
	name0 = value0
	name1 = value1
)

定义变量可以使用短变量申明方式(:=)来替代 var,但是短变量申明方式只能用于函数体内申明函数级变量,且需指明初始值。申明时不能指明变量类型,类型由初始化值确定。

name := value					// 申明一个变量
name0, name1 := value0, value1	// 申明多个变量,变量类型可以不同

注意:申明多个变量时,只要有一个是新的即可。

func main() {
	oldVar := 1
	oldVar, newVar := 2, 3
	fmt.Printf("oldVar=%v newVar=%v\n", oldVar, newVar) // oldVar=2 newVar=3
}

可以看出 var 与短变量申明方式的区别有如下几点:
(1)var 既可以申明函数级变量,也可以申明包级变量,而短变量申明方式只能申明函数级变量,这是二者最大的区别;
(2)var 可以不指定初始值,而短变量申明方式必须指定初始值;
(3)var 可以指定数据类型,而短变量申明方式不能指定数据类型。

4.程序流程控制

4.1 for range break continue

(1)for 与 range
for 是 Go 中唯一用于循环结构的关键词。有三个使用方式,分别是单个循环条件,经典的初始化/条件/后续形式,还有和 range关键词结合使用来遍历容器类对象(数组、切片、映射、信道)。

// 单条件
i := 1
for i <= 3 {
	fmt.Println(i)
	i = i + 1
}

// 初始化/条件/后续形式
// 注意 Go 中没有前置自增与自减运算符,即++i是非法的
for i := 0; i < 3; i++ {
	fmt.Println(i)
}

// for range 遍历数组
array :=[...]int{0,1,2,3,4,5}
for i, v := range array {
	fmt.Println(i,v)
}

// 只遍历下标
for i := range array {
	fmt.Println(i)
}

(2)break
break 用于终止最内层的"for"、“switch"或"select"语句的执行。break 可以携带标签,用于跳出多层。如果存在标签,则标签必须放在"for”、"switch"或"select"语句开始处。

// 终止for
L:
for i < n {
	switch i {
	case 5:
		break L
	}
}

(3)continue
continue通常用于结束当前循环,提前进入下一轮循环。也可以像break一样携带标签,此时程序的执行流跳转到标签的指定位置,可用于跳出多层"for"、“switch"或"select”,提前进入下一轮的执行。示例如下:

// 提前进入下一轮循环
for i:=0; i < 3; i++ { 
	if i == 1 {
		continue
	}
	fmt.Println(i)
}
// 输出结果
0
2

// 提前进入标签处for的下一轮循环
L:
for i:=0; i < 2; i++ { 
	for j:=0; j < 3; j++{
		if j == 1 {
			continue L
		}
		fmt.Println(i, j)
	}
}
//输出结果
0 0
1 0

4.2 goto

goto 用于将程序的执行转移到与其标签相应的语句。可以使用 goto 退出多层 for、switch 或 select,功能类似于 break 携带标签。

// 终止for
L:
	for i < n {
		switch i {
		case 5:
			goto L
		}
	}

注意事项:
(1)执行"goto"不能在跳转过程中跳过变量的定义,不然会报编译错误。例如:

	goto L  //编译报错
	v := 3
L:
	fmt.Println(v)

(2)在块外的 goto 语句不能跳转至该块中的标签。例如:

if n%2 == 1 {
	goto L1
}
for n > 0 {
	f()
	n--
L1:
	f()
	n--
}

是错误的,因为标签 L1 在 for 语句的块中而 goto 则不在。
(3)程序设计时,应尽量避免使用 goto 语句,因为程序执行流的随意跳转会破坏结构化设计风格,导致代码可读性下降。

4.3 switch case default fallthrough

这四个关键词是结合使用的。switch 语句提供多路执行,表达式或类型说明符与 switch 中的 case 相比较从而决定执行哪一分支。如果存在一个且最多只能存在一个 default 默认分支,所有的 case 分支都不满足时将执行 default 分支,且 default 分支不一定要放在最后的位置。Go switch 语句在执行完某个 case 子句后,不会再顺序地执行后面的 case 子句,而是结束当前 switch 语句。使用 fallthrough 可以继续执行下一个 case 或 default 子句。case 表达式可以提供多个待匹配的值,使用逗号分隔。

switch 有两种形式,表达式选择和类型选择。下面分别演示两种形式下 switch case default fallthrough 的用法。

4.3.1 表达式选择

表达式选择可以没有表达式,缺省为 true,这种写法也习惯地取代 if-else-if-else 语句链。表达式可以不是常量。表达式前面可以有简单语句,比如短变量申明语句。可见 Go switch 相对于 C 有较大的区别且更加灵活。

switch tag {
default: s3()			// default 子句可以出现在任意位置,不一定是最后一个
case 0, 1, 2, 3: s1()	// case 表达式可以提供多个待匹配的值,使用逗号分隔
case 4, 5, 6, 7: s2()
}

switch {				// 缺失的表达式为 true
case x < y: f1()
fallthrough				// 强制执行下一个 case 子句
case x < z: f2()
// 此处没有 fallthrough,switch 执行流在此终止	
case x == 4: f3()
}

switch x := f() {		// 缺省表达式试为 true 且前面存在一条短变量申明语句
case x < 0: return -x	// case 表达式无需为常量
default: return x
}

4.3.2 类型选择

类型选择比较类型而不是值。它类似于表达式选择,由一个特殊的表达式表示类型,该表达式的形式是使用保留字 type 的类型断言而不是实际的类型。

switch x.(type) {
// cases
}

然后使用实际类型 T 与表达式 x 的动态类型进行匹配。与类型断言一样,x 必须是接口类型,列出的每个非接口类型 T 必须实现 x 且不能相同。

switch i := x.(type) {
case int:
	printInt(i)                            // i 类型为 int
case float64:
	printFloat64(i)                        // i 类型为 float64
case func(int) float64:
	printFunction(i)                       // i 类型为 func(int) float64
case bool, string:
	printString("type is bool or string")  // i 类型为 bool or string
default:
	printString("don't know the type")     // i 类型未知
}

4.4 if else

if 与 else 实现条件控制,与 C 有许多相似之处,但也有其不同之处。变化主要有三点:
(1)可省略条件表达式的括号;
(2)支持初始化语句,可定义代码块局部变量;
(3)if与else块中只有一条语句也需要添加大括号;
(4)起始大括号必须与if与else同行。

if err := file.Chmod(0664); err != nil {
	log.Print(err)
	return err
}

4.5 return defer

(1)return

return 用于函数执行的终止并可选地提供一个或多个返回值。 任何在函数 F 中被推迟的函数会在 F 返回给其调用者前执行。如果返回值在函数返回形参中指定了名字,那么 return 时可不带返回值列表。

// 无返回值
func noResult() {
	return
}

// 单返回值
func simpleF() int {
	return 2
}

// 多返回值
func complexF2() (float64, float64) {
	re = 7.0
	im = 4.0
	return re, im
}

// 返回值已具名
unc complexF3() (re float64, im float64) {
	re = 7.0
	im = 4.0
	return
}

(2)defer

defer 用于预设一个函数调用,推迟函数的执行。 被推迟的函数会在执行 defer 的函数返回之前立即执行。

显得非比寻常, 但却是处理一些事情的有效方式,例如无论以何种路径返回,都必须释放资源的函数。 典型的例子就是解锁和关闭文件。

//将文件的内容作为字符串返回。
func Contents(filename string) (string, error) {
	f, err := os.Open(filename)
	if err != nil {
		return "", err
	}
	defer f.Close()  // f.Close 会在函数结束后运行

	var result []byte
	buf := make([]byte, 100)
	for {
		n, err := f.Read(buf[0:])
		result = append(result, buf[0:n]...)
		if err != nil {
			if err == io.EOF {
				break
			}
			return "", err  // 我们在这里返回后,f 就会被关闭
		}
	}
	return string(result), nil // 我们在这里返回后,f 就会被关闭
}

推迟诸如 Close 之类的函数调用有两点好处:
第一,它能确保你不会忘记关闭文件。如果你以后又为该函数添加了新的返回路径时,这种情况往往就会发生;
第二,它意味着“关闭”离“打开”很近,这总比将它放在函数结尾处要清晰明了。

使用 defer 时,需要注意三点:

(1)defer 函数的入参在 defer 时确定。

被推迟函数的实参(如果该函数为方法还包括接收者)在推迟执行时就会求值,而不是在调用执行时才求值。这样不仅无需担心变量在 defer 函数执行前被改变,还意味着可以给 defer 函数传递不同实参。

for i := 0; i < 5; i++ {
	defer fmt.Printf("%d ", i)
}

(2)多个 defer 函数的执行顺序为后进先出。

被推迟的函数按照后进先出(Last In First Out,LIFO)的顺序执行,因此以上代码在函数返回时会打印 4 3 2 1 0。

(3)defer 函数在 return 语句赋值返回值与 ret 之间执行。

return 语句不是原子操作,而是被拆成了两步。

rval = xxx
ret

而 defer 函数就是在这两条语句之间执行。

rval = xxx
defer_func
ret

所以被 defer 的函数可以读取和修改带名称的返回值。

// 返回值为 2
func c() (i int) {
    defer func() { i++ }()
    return 1
}

4.6 go

go 用于创建 Go 程(goroutine),实现并发编程。Go 程是与其它 Go 程并发运行在同一地址空间的函数,相比于线程与进程,它是轻量级的。Go 程在多线程操作系统上可实现多路复用,因此若一个线程阻塞,比如说等待 I/O,那么其它的线程就会运行。Go 程的设计隐藏了线程创建和管理的诸多复杂性。

在函数或方法前添加 go 关键字能够在新的 Go 程中调用它。当调用完成后,该 Go 程也会安静地退出。效果有点像 Unix Shell 中的 & 符号,它能让命令在后台运行。

package main

import (
	"fmt"   
	"time"  
)

func main() {
	go func(){ 
		fmt.Println("in first goroutine")
	}()     
	go func(){ 
		fmt.Println("in second goroutine")
	}()     

	fmt.Println("main thread start sleep, and other goroutine start execute")
	time.Sleep(10*time.Second)
}

输出结果:

main thread start sleep, and other goroutine start execute
in second goroutine
in first goroutine

注意,从输出结果可以看出,Go 程的执行顺序和创建的顺序是没有关系的,也就是说存在多个 Go 程时,其执行的顺序是随机的。

4.7 select

select 是 Go 语言中用于处理并发操作的一个关键字,主要用于在多个通信操作中选择一个执行。

它类似于 switch 语句,但是它的 case 必须是一个通信操作。

也就是说 select 是用来监听和 channel 有关的 IO 操作,它与 select,poll,epoll 相似,当 IO 操作发生时,触发相应的动作,实现 IO 多路复用。

package main

import "fmt"

func main() {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)

    ch1 <- 3
    ch2 <- 5

    select {
    case <- ch1:
        fmt.Println("ch1 selected")
    case <- ch2:
        fmt.Println("ch2 selected")
	default:
		// 如果ch1与ch2没有数据到来,则进入default处理流程。如果没有default子句,则select一直阻塞等待ch1与ch2的数据到来
		fmt.Println("default")
	}
}

输出结果:

ch1 selected

// 或者
ch2 selected

从输出结果可以看出,当存在多个 case 满足条件,即有多个 channel 存在数据时,会随机地选择一个执行。

注意,如果想让某个 Go 程永久阻塞,可以使用没有 case 和 default 语句的 select:

select{}

# 等效于
for{}

参考文献

The Go Programming Language Specification
Effective Go - The Go Programming Language
Go编程语言规范.关键字
郝林.Go并发编程实战[M].人民邮电出版社.C2.1.2关键字.P16-17
简书.【golang】select关键字用法
CSDN.【GoLang笔记】遍历map时的key随机化问题及解决方法
CSDN.Golang map 使用与注意事项

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值