【Go】三、函数与方法

1、函数

1、函数定义

1、golang函数特点:

​ 1)无需声明原型

​ 2)支持不定参数

​ 3)支持多返回值

​ 4)支持命名返回参数

​ 5)支持匿名函数和闭包

​ 6)函数也是一种类型,可以赋值给变量

​ 7)不支持 嵌套(nseted)一个包不能有两个名字一样多函数

​ 8)不支持 重载(overload)

​ 9)不支持 默认参数(default parameter)

2、函数声明:

​ 1)函数声明包含一个函数名、参数列表、返回值列表和函数体;如果函数没有返回值,则返回列表可以忽略,函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。

​ 2)函数可以没有参数 或 多个参数

​ 3)注意类型在变量名之后

​ 4)函数可以返回任意数量的返回值

func test(x,y int, s string) (int, string) {
  // 类型相同的相邻参数可以合并,多返回值必须使用括号
  n := x + y
  return n, fmt.Println(s, n)
}

例子:
package main

import "fmt"

func test(fn func() int) int {
    return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) string 

func format(fn FormatFunc, s string, x, y int) string {
    return fn(s, x, y)
}

func main() {
    s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。

    s2 := format(func(s string, x, y int) string {
        return fmt.Sprintf(s, x, y)
    }, "%d, %d", 10, 20)

    println(s1, s2)
}

// 结果:100, 10 20

1、有返回值的函数,必须有明确的终止语句,否则会引发编译错误

2、没有函数体的函数声明,表示该函数不是以Go实现的,这样的声明定义了函数标识符

2、参数

​ 函数定义的时候有参数,成为形参,就像定义在函数体内的局部变量,但当调用函数,传递过来的变量就是函数的实参,两种方式传递参数:

​ 1)值传递:实际参数复制一份传递到函数中,不会影响到实际参数

​ 2)引用传递:调用函数时候将实际参数地址传递到函数中,在函数中对参数进行修改,将影响到实际参数

​ 默认情况下,Go语言使用的是值传递,调用过程中不会影响到实际参数! => map、slice、chan、指针、interface默认以引用的方式传递

不论值传递还是引用传递,传递给函数的都是变量的副本,不过值传递的是值的拷贝,引用传递,传递的是地址的拷贝,一般来说,地址拷贝更加高效,值拷贝取决于对象大小

1、不定参数传值

func myfunc(args ...int) { // 0或多个参数
  
}
func myfunc(a int, args ...int) { // 1或多个参数
  
}
func myfunc(a int, b int, args ...int) { // 2或多个参数
  
}

// 注意:其中args是一个slice,可以通过args[index]依次访问所有参数,通过len(arg)来判断传递参数的个数

2、使用interface()传递任意数据类型参数,且interface{}是类型安全的

func myfunc(args ...interface{}) {
  
}

3、返回值

"_"标识符,用来忽略函数的某个返回值

1)没有参数的return语句,返回各个变量的当前值,成为"裸"返回 => 长函数中会影响代码可读性

2)golang返回值不能用容器对象接收多返回值,只能用多个变量,或"_"忽略

3)命名返回参数允许defer延迟调用通过闭包读取和修改

package main

func add(x, y int) (z int) {
  defer func() {
    z += 100
  }
  z = x + y
  return
}

func main() {
	println(add(1, 2))
}

// 结果:103

1、defer是go中的一种延迟机制,defer后面的函数只有在当前函数执行完毕之后才能执行,通常用于释放函数

2、defer遵循先入后出,类似于栈的结构(按照代码阅读顺序,最后一个阅读到的defer先执行)

3、为什么?因为后申请的资源可能对前面申请的资源有依赖性,如果将前面的资源释放掉,对后面的资源可能造成影响

4、什么时候执行defer?1)将返回值复制给一个变量;2)执行RET执行 => defer执行在1之后,在2之前

4、匿名函数

​ 匿名函数是指不需要定义函数名的一种实现方式,在Go中,函数可以像普通变量一样传递或使用,Go语言支持随时在代码里定义匿名函数(不带函数名的函数声明 + 函数体)

package main

import (
	"fmt"
  "match"
)

func main() {
  getSqrt := func(a float64) float64 {
    return math.sqrt(a)
  }
  fmt.Println(getSqrt(4))
}

// 输出2

5、闭包、递归

1、闭包详解

​ a. 闭包与全局变量和局部变量的关系:1)全局变量常驻内存、污染全局;2)局部变量不长驻内存、不污染全局 => 闭包变量常驻内存但不污染全局

​ b. 闭包就是有权访问另一个函数作用域中变量的函数(即在一个函数内部a创建另外一个函数b,函数b可以函数a外部的变量)

​ c. 闭包里作用域返回的局部变量不会立刻销毁回收,但是要切记,不要过度使用闭包,导致内存占用和性能下降。

​ 闭包是由函数及其相关引用环境组合而成的实体(闭包 = 函数 + 引用环境)

<!DOCTYPE html>
<html lang="zh">
<head>
    <title></title>
</head>
<body> 
</body>
</html>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript"></script>
<script>
function a(){
    var i=0;
    function b(){
        console.log(++i);
        document.write("<h1>"+i+"</h1>");
    }
    return b;
}

$(function(){
    var c=a();
    c();
    c();
    c();
    //a(); //不会有信息输出
    document.write("<h1>=============</h1>");
    var c2=a();
    c2();
    c2();
});

</script>

1)函数b嵌套在函数a的内部;

2)函数a返回函数b;

3)执行完var c = a()后,变量c实际上指向函数b,再执行函数c()就会显示i的值 => 创建一个闭包,函数a()外的变量c引用函数a()内的函数b()

简单说:函数a()的内部函数b(),被函数a()外的一个变量引用的时候,就创建一个闭包

2)作用:在a()执行完之后,闭包使得垃圾回收机制GC不会回收a()所占用的资源,因为a()的内部函数b()的执行需要依赖a()中的变量i

2、Go语言的闭包 => 闭包复制的是原对象的指针,很容易解释延迟引用现象

package main

import (
    "fmt"
)

func a() func() int {
    i := 0
    b := func() int {
        i++
        fmt.Println(i)
        return i
    }
    return b
}

func main() {
    c := a()
    c()
    c()
    c()

    a() //不会输出i
}

// 输出1 2 3

3、Go语言递归函数

​ 递归就是在运行过程中调用自己,一个函数调用自己,就叫做递归函数。条件:

​ 1)子问题必须与原问题做同样的事情,且更简单

​ 2)不能无限制地调用本身,须有个出口,化简为非递归状况处理

6、Golang延迟调用defer

1、defer特性:

​ 1)关键字defer用于注册延迟调用

​ 2)这些调用直到return前才被执行,因此,可以用来做资源管理

​ 3)多个defer语句,按先进后出的方式执行

​ 4)defer语句中的变量,在defer声明时就决定了

2、用途:

​ 1)关闭文件句柄

​ 2)锁资源释放

​ 3)数据库连接释放

1、用来做逆序输出!

3、defer + 闭包

func main() {
  var whatever [5]struct{} // 创建一个名whatever且长度为5的空结构体睡着
  for i := range whatever { // 遍历这个空结构体数组
    defer func() { fmt.Println(i) }() // 延迟执行函数
  }
}
// 输出 4 4 4 4 4
// 函数正常执行,由于闭包用到的变量i在执行的时候已经变成4,所有输出全部是4
// 1、其实这个程序很值得我们思考:为什么输出的结构是4 4 4 4 4 而不是4 3 2 1 0
// 2、因为在defer语句中,我们把它当作defer0、defer1、defer2、defer3、defer4、defer5是不是从defer5按顺序从后往前执行
// 3、在defer5的时候,i变为1,记住它不是递归,他只是延迟执行,然后defer4的时候i也是4,以此类推,全部输出4

4、defer f.Close

// 方式一
type Test struct {
  name string
}

func (t *Test) Close() {
  fmt.Println(t.name, " closed")
}

func main() {
  ts := []Test{{"a"}, {"b"}, {"c"}}
  for _, t := range ts {
    defer t.Close()
  }
}
// 输出结果 c closed(3个) 为什么不是c b a?
// 道理同上面的例子,我们来描述一下这个代码执行流程呢
// 1、创建一个包含三个Test结构体的切片ts,然后对ts中的每个元素执行defer t.Close()
// 2、在这个循环中,变量t是Test结构体的值,所以执行defer语句会创建一个新的变量t'初始化切片元素的值,然后将t'传递给Close方法
// 3、我们以Close0、Close1、Close2命名,延迟执行,所以t'会变为c,然后输出c closed;
// 4、同理这个时候延迟的Close2和Close1会开始执行,而t'也是c,不是a、b => 故而输出结果是c c c 而不是 c b a
// 5、那么如何解决呢? => 变量t变成指向Tset结构体的指针,每次循环中使用&ts[i]来获取元素的指针,保证其按照正常的顺序执行

// 方式二
func Close(t Test) {
  t.Close()
}

func main() {
  ts := []Test{{"a"}, {"b"}, {"c"}}
  for _, t := range ts {
    defer Close(t)
  }
}

// 输出结果 c b a
// 这个时候是传值,不是地址,每次指向的元素不一样,故而不会出现c c c的情况

7、异常处理

​ Golang没有结构化异常,使用pannic抛出异常,recover捕获错误

​ => Go中可以抛出一个panic异常,然后在defer中通过recover捕获这个异常,然后正常处理。

panic:

1、内置函数
2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数,按照defer的逆序执行
3、返回函数F的调用者G。在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
4、直到goroutine整个退出,并报告错误

recover:

1、内置函数
2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
3、一般的调用建议:
	1)在defer函数中,通过recover来终止一个goroutine的panicking过程,从而恢复正常代码的执行
	2)可以捕获通过panic传递的error

注意:

1、利用recover处理panic指令,defer必须放在panic之前定义,另外recover只有在defer调用的函数中才有效,否则当panic时,recover无法捕获到panic,无法仿制panic扩散

2、recover处理异常后,逻辑不会恢复到panic那个点去,函数跑到defer之后的那个点

3、多个defer会形成defer栈,后定义的defer会被最先调用

​ 说了这么多,可能对这两个概念有着似懂非懂的理解,下面我们用实际的例子来帮助大家进行更进一步的理解。

1、例子:

package main

func main() {
	test()
}

func test() {
	defer func() {
		if err := recover(); err != nil {
			println(err.(string)) // 将 interface{} 转型为具体类型。
		}
	}()

	panic("panic error!")
}

// 输出结果:panic error!
// 这段代码使用panic和recover机制
// 1、panic用在程序运行时出现错误时抛出异常,导致程序终止执行,而recover用于从panic中恢复,防止程序因异常而崩溃
// 2、test()中的defer包含recover()函数,当程序用到panic()处时,会立刻停止并抛出异常
// 3、但是由于defer中的recover()函数,程序不会直接终止,而是进入defer的流程
// 4、故而最后会输出结构: panic error!

8、单元测试

8.1、go test工具

go test命令时一个按照一定约定和组织的测试代码的驱动程序,在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

​ 在xxx_test.go中有三种类型的函数:单元测试函数、基准测试函数和示例函数

1、测试函数:前缀名为Tset,测试程序的逻辑行为是否正常

2、基准函数:前缀名为Benchmark,测试函数的性能

3、示例函数:前缀名为Example,为文档提供示例代码

​ go test命令会遍历所有的_test.go文件中符合👆命名规则的函数,生成一个临时的main包用于调用相应的测试函数,然后构建并运行,报告测试结果,最后清理生成的临时文件。

1、文件名必须以xxx_test.go命名
2、方法必须是Test[^a-z]开头
3、方法参数必须 t *testing.T
4、使用go test执行单元测试

8.2、测试函数

​ 每个测试函数必须导入testing包,测试函数的基本格式:

func TestName(t *testing.T) {
  // ...
}

// 由这个例子可以看出来:1)测试函数比如Test开头;2)参数必须是 t *testing.T;3)testing.T也就是参数t用于报告测试失败和附加的日志信息

// 补充testing.T拥有的方法
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool

8.3、测试函数示例

// 1、定义一个split包,包中定义了一个Split函数
package split

import "strings"

func Split(s, sep string) (result []string) {
  i := strings.Index(s, sep) // 查找sep在s中的索引
  for i > -1 { // 找到的话
    result = append(result, s[:i]) // 不被包含的那一部分加入到result中
    s = s[i+1:] // 对s进行重新赋值
    i = strings.Index(result, s)  
  }
  result = append(result, s)
  return
}

// 2、在当前目录下,创建一个split_test.go的测试文件,并定义一个测试函数如下:
package split

import (
	"reflect"
	"testing"
)

func TestSplit(t *testing.T) { // 测试名必须以Test开头,必须接收一个*testing.T类型参数
	got := Split("a:b:c", ":")
	want := []string{"a", "b", "c"}
	if !reflect.DeepEqual(want, got) {
		t.Errorf("excepted:%v, got:%v", want, got)
	}
}

// 运行结果:
PASS
ok      _/Users/chenzhihui/go/src/goProjects/GoBase/testexample 0.637s

8.4、压力测试

​ Go语言自带一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试,建议安装gotests插件自动生成测试代码 => go get -u -v github.com/cweill/gotests/...

1、如何编写测试用例

​ 1)新建一个项目目录gotest,这样我们所有代码和测试代码都在这个目录下

​ 2)创建两个文件:gotest.go和gotest_test.go

2、如何编写压力测试

​ 压力测试必须遵循这样的格式:func BenchmarkXXX(b *testing.B) {...}

​ 1)go test默认不会执行压力测试的函数,如果需要的话使用 go test -test.bench="xxx"

2、方法

1、方法定义

​ Golang方法总是绑定对象实例,并隐式将实例作为第一实参(receiver)

1、只能为当前包内类型定义方法
2、参数 receiver 可以任意命名,如方法中未曾使用,可以省略参数名
3、参数 receiver 类型可以是T 或 *T,基类型T不能是接口或指针
4、不支持方法重载,receiver只是参数签名的组成部分
5、可用实例,value或pointer调用全部方法,编译器自动转换

🌟一个方法就是包含了接受者的函数,接受者可以是命名类型或结构类型的一个值或一个指针

func (receiver Type) methodName(参数列表)(返回值列表){} => 参数和返回值可以省略

1、例子1:展示方法的定义的所有情况

package main

type Test struct{}

// 无参数、无返回值
func (t Test) method0() {

}

// 单参数、无返回值
func (t Test) method1(i int) {

}

// 多参数、无返回值
func (t Test) method2(x, y int) {

}

// 无参数、单返回值
func (t Test) method3() (i int) {
    return
}

// 多参数、多返回值
func (t Test) method4(x, y int) (z int, err error) {
    return
}

// 无参数、无返回值
func (t *Test) method5() {

}

// 单参数、无返回值
func (t *Test) method6(i int) {

}

// 多参数、无返回值
func (t *Test) method7(x, y int) {

}

// 无参数、单返回值
func (t *Test) method8() (i int) {
    return
}

// 多参数、多返回值
func (t *Test) method9(x, y int) (z int, err error) {
    return
}

func main() {} 

2、例子2

package main

import "fmt"

// User 结构体
type User struct {
	Name string
	Email string
}

// Notify 方法:属于无参数,无返回值
func (u User) Notify() {
	fmt.Printf("%v : %v \n", u.Name, u.Email)
}
func main() {
	// 值类型调用方法
	u1 := User{"chenzhihui","czh1074@163.com"}
	u1.Notify()
	// 指针类型调用方法
	u2 := User{"hyt","xxx.com"}
	u3 := &u2
	u3.Notify()
}

// 解释:首先,定义一个叫User的结构体,定义一个该类型的方法叫Notify,该方法的接受者是一个User类型的值,要调用Notify方法需要一个User类型或者指针

3、普通函数与方法的区别

​ 1)普通函数:接收者为值类型的时候,不能将指针类型的数据直接传递,反之亦然

​ 2)方法:接收者为值类型的时候,可以直接用指针类型的变量调用方法,反过来同样也可以

package main

import "fmt"

// 1、普通函数
// 接收值类型参数的函数
func valueIntTest(a int) int {
	return a + 10
}

// 接收指针类型参数的函数
func pointerIntTest(a *int) int {
	return *a + 10
}

func structTestValue() {
	a := 2
	fmt.Println("valueIntTest:", valueIntTest(a)) // 此时函数的参数为值类型,不能直接将指针作为参数传递

	b := 5
	fmt.Println("pointInttest:", pointerIntTest(&b)) // 函数作为指针类型,也不能将值类型直接作为参数传递

}

// PersonD 2、方法
type PersonD struct {
	id int
	name string
}

// 接受者为值类型
func (p PersonD) valueShowName() {
	fmt.Println(p.name)
}

// 接受者为指针类型
func (p *PersonD) pointShowName() {
	fmt.Println(p.name)
}

func structTestFunc() {
	// 值类型调用方法
	personValue := PersonD{101, "hello world"}
	personValue.valueShowName()
	personValue.pointShowName()

	// 指针类型调用方法
	personPoint := PersonD{202, "hello golang"}
	personPoint.valueShowName()
	personPoint.pointShowName()

	// 与普通函数不同,接受者为指针类型和值类型的方法,其变量可以互相调用
}

func main() {
	structTestValue()
	structTestFunc()
}


// 运行结果:
valueIntTest: 12
pointInttest: 15
hello world
hello world
hello golang
hello golang

2、匿名字段

​ Golang匿名字段:可以像字段成员那样访问匿名字段,编译器负责查找

package main

import "fmt"

type User2 struct {
	Name string
	Age int
}

type Manager struct {
	User2
}

func (u2 *User2) ToString() string  {
	return fmt.Sprintf("User2: %p, %v\n", u2, u2) // 通过匿名字段,获得类似继承的能力(根据类型,去查找对应u2的值)
}

func main() {
	m := Manager{User2{"czh",18}}
	fmt.Printf("Manager : %p\n", &m)
	fmt.Println(m.ToString())
}

结果:
Manager : 0x14000114018
User2: 0x14000114018, &{czh 18}

// 根据得到的结果分析
// 1、第一行输出,给我们举例,输出的是m的地址
// 2、第二行是我们定义的方法,这个方法,会通过匿名字段去显示对应的值,如%p找到地址,%v找到对象的值

3、方法集(🌟值得钻研)

​ Golang方法集:每个类型都有与之关联的方法集,这会影响到接口实现规则

1、类型T方法集包含全部 receiver T 方法
2、类型*T方法集包含全部receiver T + *T方法
3、如类型S包含匿名字段T,则S和*S方法集包含T方法
4、如类型S包含匿名字段*T,则S和*S方法集包含T、*T方法
5、不管嵌入T 或 *T、*S方法集总是包含T、*T方法

// todo:后续会在单独出一个文章来讲解下方法集是什么,为什么,作用在什么场景下!

4、表达式

​ Golang表达式:根据调用者不同,方法分为2种表现形式 -> instance.method(args…) 或者 .func(instance, args…),前者称为method value、后者称为method expression。

​ 区别在于:method value绑定实例、method expression须显式传参

package main

import "fmt"

type User3 struct {
	id int
	name string
}

func (self *User3) Test() {
	fmt.Printf("%p %v\n", self, self)
}

func main() {
	u3 := User3{1,"czh"}
	u3.Test() // 正常方式调用

	mValue := u3.Test // 前者,绑定实例
	mValue()

	mExpression := (*User3).Test // 没有绑定实例,但是需要显示传参
	mExpression(&u3)

}

// 结果:
0x1400009c018 &{1 czh}
0x1400009c018 &{1 czh}
0x1400009c018 &{1 czh}

5、自定义error

1、返回异常

package main

import (
	"errors"
	"fmt"
)

func getCircleArea(radius float32) (area float32, err error) {
	if radius < 0 {
		// 构建个异常对象
		err = errors.New("半径不能为负")
		return
	}
	area = 3.14 * radius * radius
	return
}

func main() {
	area, err := getCircleArea(-3)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(area)
	}
}

// 执行结果:半径不能为负
// 1、从main函数开始,调用getCircleArea()
// 2、传的半径是个负值,所以在这里我们构建一个异常对象err,并告知错误的原因
// 3、main函数中接收到err,根据err是否为nil判断我们是不是发生错误了进行输出的选择

2、自定义error

package main

import (
    "fmt"
    "os"
    "time"
)

type PathError struct {
    path       string
    op         string
    createTime string
    message    string
}

func (p *PathError) Error() string {
    return fmt.Sprintf("path=%s \nop=%s \ncreateTime=%s \nmessage=%s", p.path,
        p.op, p.createTime, p.message)
}

func Open(filename string) error {

    file, err := os.Open(filename)
    if err != nil {
        return &PathError{
            path:       filename,
            op:         "read",
            message:    err.Error(),
            createTime: fmt.Sprintf("%v", time.Now()),
        }
    }

    defer file.Close()
    return nil
}

func main() {
    err := Open("/Users/5lmh/Desktop/go/src/test.txt")
    switch v := err.(type) {
    case *PathError:
        fmt.Println("get path error,", v)
    default:

    }

}
输出结果:
get path error, path=/Users/pprof/Desktop/go/src/test.txt 
op=read 
createTime=2018-04-05 11:25:17.331915 +0800 CST m=+0.000441790 
message=open /Users/pprof/Desktop/go/src/test.txt: no such file or directory

题外话

1、命名规范

1、Go文件命名规范

        通过这七个方面进行认识:文件命名、包命名、变量、常量、接口、结构体和方法。

  • 文件命名:一律常用小写,不用驼峰,尽量做到见名思义(取名也是一门艺术),看到文件名就大概知道这个文件下的内容,如:stringutil.go(这个告诉我们这个是一个string工具文件)
  • 包命名:1)小写;2)短命名,尽量不要和标准库冲突;3)统一使用单数形式,如:package strutil(代表string工具包)
  • 变量:驼峰命名方式,特有名词(公开:全部大写、私有:全部小写),如DNS、HTTP等
  • 常量:只有一个目的(表达清楚语义,不嫌名字长)
  • 接口:单个函数的接口接口以er为后缀,如:type Reader interface {};两个函数的接口名综合两个函数名,如:type WriteFlusher interface{};三个及以上接口类似于结构体名,如:type Car interface {}
  • 结构体:名词或短语,如:Account、Person等
  • 方法:动词或动词短语,采用驼峰命名法,将必要的功能体现在名字中,如:updateNameById;结构体方法,Receiver的名称应该缩写,一般使用一个或两个字符作为Receiver名称,如func (z zoo) method() {}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Coder陈、

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

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

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

打赏作者

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

抵扣说明:

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

余额充值