Go语言学习

Go语言学习

Go语言学习

1 安装

1.1 下载

可以访问Go语言官网下载go语言安装包.如下图选择Download Go:
在这里插入图片描述
进入下载页面后根据自己的操作系统选择对应的go语言安装包即可下载:
在这里插入图片描述

1.2 安装和配置

我的操作系统是linux的,所以我选择下载了linux版本的安装包.将下载好的go语言安装包复制到安装目录然后解压,解压好后配置环境变量:

GOROOT=/home/deepin/dev/env/go/go
export PATH=$PATH:$JAVA_HOME/bin:$CLASS_PATH:$MAVEN_HOME/bin:$NODE_HOME/bin:$GOROOT/bin/

配置好环境变量后,在控制台输入如下命令,出现类似输出则表示配置成功:

$ go version
go version go1.12.5 linux/amd64

注意:配置好环境变量后可能要使配置文件被重新加载source 配置文件或者注销当前账户重新登录!!!

2 入门Hello World

2.1 新建一个文件main.go

新建一个文件`main.go`,并在文件中键入以下内容:

package main
import "fmt"
func main() {
	fmt.Println("Hello World")
}

依次对上面的代码解释:

  • package main 包,表示当前程序在哪个包下,入口程序一定要在main包下,入口程序也即程序最开始运行的地方.main方法就是程序的入口.
  • import “fmt” 导入外部依赖(库),这里导入了一个fmt库(format),这个库提供了多种数据格式化的方法.
  • func main() 定义main函数,main函数就是程序执行和入口函数,main函数一定要在main包下.
  • fmt.Println(“Hello World”) 程序向控制台输出Hello World

3 基础

3.1 包

包程序由包构成,程序从main程序开始执行,mian程序在mian包下。

3.1.1 主程序

新建一个index.go文件,并键入以下代码:

package main
func main() {
	println("hello world!")
}

主程序必须是func main并且在main包下。

3.1.2 导包

包中可以提供方法供其他程序使用,其他程序要使用包就需要导包,导包通过import关键字。导包有要注意的地方是导包的路径是从GOPATHGOROOT开始查找包的,其中GOROOT是在安装GO环境是配置的,GOPATH默认是用户家目录/go。我们更改默认的GOPATH配置:

GOPATH=//home/deepin/Documents/workspace/go
export PATH=$PATH:$JAVA_HOME/bin:$CLASS_PATH:$MAVEN_HOME/bin:$NODE_HOME/bin:$GOROOT/bin/:$GOPATH

配置好后,新建测试程序:

  • 新建项目(文件夹demo)并在项目目录下新建两个文件夹mainutil
  • main包下新建一个main.go文件并键入以下代码:
package main

import "../util"

func main() {
	util.PrintHello()

  • util文件夹下新建一个hello.go文件,并键入以下内容:
package util

func PrintHello() {
	hello()
}


func hello() {
	println("hello")
}

运行go run main/main.go
注意:
相对路径导包:

import "../util"

如果写成绝对路径导包的话要走GOPATH,也就是从$GOPATH/src/开始查找包,应该是import 项目名/util(项目在src下)。项目如果不在src/下的话,就要把util包发到src下
注:import导包的时候写的是目录名使用的时候使用的是包名

3.1.3 导包可以同时导入多个,例如:
import (
	"demo00/utils"
	"fmt"
)
3.1.4 给包起别名
import (
	hello "demo00/utils"
	"fmt"
)
// 或
import hello "demo00/utils"
3.1.5 导出约定

要导出的内容(方法/变量等)必须以大小字母开头。

3.2 函数

使用func关键字定义函数,格式为

func 函数名(参数名1 参数类型, 参数名2 参数类型, ....) 返回值类型 {
}

例如定义一个两数相加返回结果的方法:

func add(x int, y int) int {
	return x + y
}
3.2.1 参数类型缩写

如上add函数,xy是同一个类型的,所以参数列表也可以用下缩写写法:

func add(x, y int) int {
	return x + y
}
3.2.2 多值返回

函数不能返回多值,比如下面这个方法计算计算两个数的和和差并分别返回

func calc(x, y int) (int, int) {
	return x + y, x - y;
}
3.2.3 命令返回值

可以为返回值定义变量名,在函数末尾使用return就直接返回:

func calc(x, y int) (add, minus int) {
	add = x + y
	minux = x - y
	return
}

3.3 变量

3.3.1 变量声明及初始化
// 声明变量
var a, b int
// 声明变量并初始化
var x, y int = 2, 3
3.3.2 变量声明及初始化简写

简写变量的形式只能在函数内使用:

// 声明变量初始化简写,类型自动推导
a, b := 3, 4
3.3.3 定义多组变量

就像导入多组包一样,可以定义多级变量:

var (
	a,b int = 1, 2
	c, d bool
)
3.3.4 默认值

在定义了变量且没有显示赋值时,变量会有一个默认值:

数值类型为 0,
布尔类型为 false,
字符串为 ""(空字符串)。
3.3.5 类型转换

go中类型必须显示转换:

var a int = 1
// 显示转换
var b float64 = float64(a)
println(b)
// 显示转换
var c int = int(b)
println(c)
3.3.6 常量

go中常量通过const关键字定义,常量不允许被修改:

	const Pi = 3.14
	println(Pi)
	// 试图修改一个常量,编译不通过
	Pi = 3.15
	println(Pi)

3.4 流程控制语句

3.4.1 for

在go中只有一种循环语句,就是for循环,如下:

for 前置语句; 条件语句; 后置语句 {

}

前置语句最先执行且执行一次,每次执行循环前执行条件语句,每次循环结束时执行后置语句。{}不能省略。

  • 死循环
for {
	// 代码块...
}
  • 只判断条件
for ; i < 10;  {
	//  代码块...
	
}
3.4.2 if

if语句不需要小括号,必须要大括号,如下

if 条件 {
	// do something...
}
  • 可以在if中执行一条语句
    类似for语句一样,在执行if语句的时候也可以执行一条语句:
func main() {
	var i int = 1
	
	if a := 2; i < a {
		i = a + i
	}
	println(i)
}

如果在if中定义了一个变量,这个变量在整个if中共享:

func main() {
	var i int = 1
	
	if a := 2; i < 0 {
		i = a + i
	} else if i < 2 {
		i = a + 2
	}
	println(i)
}

3.4.3 switch

下面例子是所有switch语句相关的用法:

package main

func main() {
	var c string = "abc"

	// switch 也可以执行一条语句
	switch i := "bc"; c {
	case "a" + i:
		println("a")
		// 默认有break语句的作用 不会继续向下执行
	case "ab": 
		println("ab")
		// break语句作用无效 也就是会继续向下执行
		fallthrough
		// switch 的case 可以是一个函数
	case fun():
		println("abc")
	case "d":
		println("d")
	default: // 条件都不匹配时执行
		println("default")
	}
}

func fun() string {
	println("fun执行...")
	return "abc"
}

如果switch语句没有条件,相当于swith true:


func main() {
	switch  {
	case 1 < 10:
		println("true")
	case 1 > 10:
		println("false")
	}	
}
4.4 延迟执行defer

defer后面跟着一个函数defer关键字会使该函数延迟执行,也即在defer所在函数执行完后(return后)执行该函数。

func main() {
	j := test()
	println(j) // 4
	defer println("main", j) // 5
}

func test() int{
	defer println("test exec ...") // 3
	i := 1
	println("test", i) // 1
	defer println("test defer", i) // 2
	return 1
}

执行结果是:

test 1
test defer 1
test exec ...
1
main 1

defer 栈:推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

3.5 更多类型

指针指向(保存)了值的内存地址。

3.5.1 指针
a 定义指针

通过var 变量名 *类型可以定义一个指向对应类型的指针变量,如下:

var p *int 

变量都有默认值,这里p的默认值是nil

b 取址

通过&变量就可以取到对应变量的地址了,如下:

i :=  10
// 取变量 i的地址
var p *int = &i
c 取值

通过*指针变量就可以取到指针变量对应的地址的值:

var p *int 
i := 3
p = &i
j := *p
// j 的值就是3

下面两种是不同的操作(地址就是内存地址):

func main() {
	var p *int 
	i := 3
	p = &i
	// 取p中的值,p保存的是i的地址,所以是地址值
	println(p)
	// 取p中的地址中的值, 也就是对就的i的值
	println(*p)
}

看下面 这个程序:

package main

func main() {
	// 定义一个变量 值是21  根据类型推导  i的类型是int型
	i := 21
	// 定义一个变量  值是&i  根据类型推导 p的类型是 *int型
	p := &i
	// *p 的操作就是对 p中保存的地址上的内存的操作 *p =  42 就是改变了内存地址上的值为42
	*p = 42
	// 所以在这里打印i 就是42
	println(i)
}

3.5.2 结构体
a 定义结构体

定义一个结构体通过type 结构体名 struct

type Student struct {
	
}
b 为结构体添加字段

结构体可以看做是一个对象,字段可以看做是这个对象的属性:

type Student struct {
	X int
	Y int
}
c 定义一个结构体
// X = 1 , Y = 3
var s Student = Student{1, 3}
// X = 1, Y = 0
var s1 = Student{X: 1}
// X = 1 ,  Y = 2
var s2 = Student{X: 1, Y: 2}
// X = 0, Y = 0
var s3 = Student{}
d 获取结构体字段值
var s Student = Student{1, 3}
// 获取s中的X的值保存在变量i中
var i int = s.X
e 结构体指针

可以通过(*指针).字段通过指针访问属性值,也可以直接访问指针.字段

package main
import "fmt"

func main() {
	var s Student = Student{1, 3}
	p := &s
	fmt.Println((*p).X)
	fmt.Println(p.X)
}
type Student struct {
	X int
	Y int
}
3.5.3 数组
a 定义数组

定义一个10个长度的数组i,数组中每个元数都被会初始化(这里每个元素的类型是int,所以被始化为0)

var i [10]int

如果用:形式定义数组就必需显示初始化数组:

i := [4]int{1, 3, 4, 5}
3.5.4 切片

切片类似于数组,数组长度固定,切片长度是动态的。

a 定义一个切片

定义一个切片可以看做是定义一个没有长度的数组,如下定义了一个接收int类型的切片:

var i []int
b 从数组中初始化

从一个数组中初始化切片,如下j[1:3]即取j中下角标1和2中的数据(包含1不包含3):

var i []int
// 定义一个数组
j := [4]int{1, 2, 3, 4}
// 初始化切片
i = j[1:3]

注:j[1:3]其左右两边的数值都可以省略,如果左边省略表示从0开始,右边省略表示到数组最后的下角标.

c 切片中不保存数据

切下中不保存任何数据,只是描述了数组中的一段,如下从数组中扩展的切片改变数组上对应的数据也会发生变化:

j := [4]int{1, 2, 3, 4}
i := j[1:3]
i[1] = 2 // j[2]也变成了2
c 直接初始化切片

可以不从数组来初始化切片:

s := []int{1, 2, 3, 4, 3}
d 切片长度

可以通过leng(切片)获取切片长度,cap(切片)来获取切片的容量:

s := []int{1, 2, 3, 4, 3}
fmt.Println(len(s)) // 5
fmt.Println(cap(s)) // 5

*长度和容量:长度可以理解为当前切片中元素的个数,容量可以理解为当前切片可以保存元素的个数,容量可以扩充,当前当前元素个数>容量时,容量扩充一倍。

e 重新切片

重新切片,改变切片容量:

s := []int{1, 2, 3, 4, 3} // [1 2 3 4 3] len = 5 cap = 5
s = s[3:] // [4 3] len = 2 , cap = 2
d 通过make创建切片

如下,创建了一个长度为0,容量为5的切片:

s := make([]int, 0, 5) // []

如下,创建一个长度为2(切片中有两个元素,默认值为0),容量为3的切片:

s := make([]int, 2, 3) // [0 0] 
e 向切片中添加元素

可以通过append(切片, 元素1, 元素2, ..., 元素n)向切片中添加元素,当元素长度超过了容量时,容量扩容一倍:

s := make([]int, 0, 2) // []  len = 0 cap = 2
s = append(s, 1) // [1] len = 1 cap = 2
s = append(s, 2) // [1 2] len = 2 cap = 2
s = append(s, 3, 4) // [1 2 3 4] len = 4 cap = 4
f 遍历切片

通过forrange可以遍历切片:

func main() {
	s := []int{1, 2, 3, 4, 5}
	// i 是下角标 v是对应下角标的元素
	for i, v := range s {
		fmt.Print("i = ", i, "  ")
		fmt.Println("v = ", v)
	}
}

可以用_代替iv中的其中一个,也就是说如果只要获取下角标或元素可以像下面这样写:

for	i, _ := range s {
	fmt.Print("i = ", i, "  ")
}
// 或
for	_, v := range s {
		fmt.Print("i = ", i, "  ")
}

如果只要下角标也可以这样写:

for i := range s {
	// do something...,
}
3.5.5 映射 map
a 定义映射

新定义 的映射是nil,不能向其添加数据:

var m map[string]string 
b make初始化

映射通过make初始化后可以向其中添加数据:

var m map[string]string
m = make(map[string]string])
c 初始化数据

在创建时初始化数据:

var m = map[string]string {
		"a": "xiaoming",
		"b": "xiaohong",
	}
fmt.Println(m) // map[a:xiaoming b:xiaohong]
d 向映射中添加或删除数据
// 如果没有添加数据,如果存在就修改数据
m["c"] = "xiaogang"
e 获取元素

直接通过key来获取元素:

func main() {
	var m = map[string]string {
		"a": "xiaoming",
		"d": "xiaohong",
	}
	fmt.Println(m["a"])
}
f 删除元素

可以通过delete(映射, key)删除元素:

func main() {
	var m = map[string]string {
		"a": "xiaoming",
		"d": "xiaohong",
	}
	// 删除元素
	delete(m, "a")
	fmt.Println(m)
}
```下单接口json
##### g 检测值是否存在
通过下面语法检测值是否存在,如果存在 `elem`就是对应的值,`ok`为`true`,如果不存在,`elem`是对应类型的默认值,`ok`为`false`:
```go
elem, ok := m["c"]
3.5.6 函数作为值传递

下面这个例子,函数作为另一个函数的参数和返回值传递:

func main() {
	i := func(a, b int) int {
		return a + b
	}
	fn := test(i)
	r := fn(1, 2)
	fmt.Println(r)
}

func test(fn func(a, b int) int ) func(a, b int) int {
	return fn
}
3.5.7 函数的闭包

函数的闭包也就是函数的返回值是一个函数,如果函数中有变量,那么返回的函数在执行时会共享同一个变量:

func main() {
	f1 := test()
	fmt.Println(f1(1)) // 1
	fmt.Println(f1(1)) // 2
	f2 := test()
	fmt.Println(f2(1)) // 1
	fmt.Println(f2(1)) // 2
}

func test() func(b int) int {
	sum := 0 // 这个就是共享变量
	return func(b int) int{
		sum += b
		return sum
	}
}

4 方法和接口

4.1 方法

go中没有类。不过你可以为结构体定义方法。方法也就是带接收者的函数,用func定义方法,func后是接收者类似:func 接收者 方法名(参数) 返回值.

4.1.1 定义方法

为结构体定义一个方法:


func main() {
	s := Student{"Tom", 18}
	s.say("hello")
}
type Student struct {
	name string
	age int
}
// 为结构体定义方法
func (s Student) say(word string) {
	fmt.Println(s.name, ":", word);Scale
}
4.1.2 扩展类型添加方法

我们可以扩展一个类型,type 类型名 类型,如type MyInt int,也可以为其添加方法,如下:

package main
import "fmt"

func main() {
	var m MyInt = MyInt(1)
	sum := m.add(11)
	fmt.Println(sum)
}
type MyInt int
func (m MyInt) add(param int) (sum int){
	sum = int(m) + param
	return 
}
4.1.3 方法的接收者

方法的接收者可以是一个‘类型’,也可以是一个指针,如果是一个类型的话,接收者只是这个‘类型’的副本,如果是指针,接收者指向对应的‘类型’,也就是说如果修改值,一个会修改副本一个修改值本身:

package main
import "fmt"

func main() {
	var m My = My{1, 2}
	m.print() // x 1   y 2
	m.change(2) // x 2   y 4
	m.print() // x 1   y 2
	m.changeP(3)
	m.print() //  x 3   y 6
}
type My struct {
	X int
	Y int
}

// 打印结果
func (m My) print() {
	fmt.Println("m.X -> ", m.X)
	fmt.Println("m.Y -> ", m.Y)
}

// 改变值
func (m My) change(param int) {
	m.X = param
	m.Y = 2 * param
	println("---------------------")
	m.print()
	println("---------------------")
}

// 使用指针改变值
func (m *My) changeP(param int) {
	m.X = param
	m.Y = 2 * param
}

如果方法的接收者是值本身,如果使用指针调用这个方法,会被解释为(*指针).方法(),如果方法的接收者是值本身,使用值本身调用方法时,会被解释为(&值本身).方法名()

4.2 接口

接口是定义了一组方法签名的集合,如下就是一个接口:

type MyInterface interface {
	say(word string)
	eat(food string)
}
4.1 实现接口

实现接口只需要实现接口中定义的方法即可!

a 使用指针作用接收者
// 结构体
type Student struct {
	name string
	age int
}
// 为Student实现接口MyInterface中的say方法
func (s *Student) say(word string) {
	fmt.Println(s.name, " say -> ", word)
}

// 为Student实现接口MyInterface中的eat方法
func (s *Student) eat(food string) {
	fmt.Println(s.name, " eat -> ", food)
}

使用指针接收的方法,接口接收的类型也应该是指针类型:

func main() {
	var mi MyInterface
	s := Student{"Tom", 18}
	// 这里是指针类型
	mi = &s
	mi.say("hello")
	mi.eat("orange")
}
b 不使用指针接收
// 结构体
type Student struct {
	name string
	age int
}
// 为Student实现接口MyInterface中的say方法
func (s Student) say(word string) {
	fmt.Println(s.name, " say -> ", word)
}

// 为Student实现接口MyInterface中的eat方法
func (s Student) eat(food string) {
	fmt.Println(s.name, " eat -> ", food)
}

不使用指针接收方法,接口接收的类型可以是指针或不是指针

func main() {
	var mi MyInterface
	s := Student{"Tom", 18}
	mi = &s
	mi = s
	mi.say("hello")
	mi.eat("orange")
}
4.2 接口作为值传递

下面是接口作为值传递的例子:

package main
import "fmt"

func main() {
	s := MyStruct1{"Tom"}
	var ms MyStruct1 = s
	exec(ms, "hello")
}

type MyInterface interface {
	show(param string)
}

type MyStruct1 struct {
	name string
}

func (m MyStruct1) show(param string) {
	fmt.Println(param)
}

//  传递接口
func exec(m MyStruct1, param string) {
	m.show(param)
}
4.3 空接口

没有任何方法定义的接口就是空接口,空接口可以接收任何参数类型interface{}

func main() {
	print(Student{"Tom"})
}



type Student struct {
	name string
}

func print(i interface{}) {
	fmt.Println(i)
}

4.4 空接口断言

变量1 , 变量二 : 空接口.(类型),变量1保存了空接口中保存的变量值,变量保存了空接口中是否是保存的对应变量的布尔值,具体看下面这个例子:

package main
import "fmt"

func main() {
	var i interface{} = Student{"Tom"}
	// 判断i保存的变量 是不是int类型的 
	// 如果是就返回 该变量 并保存到 s1中,且ok为true, 
	// 如果不是s1为int类型默认值 ok为false
	s1, ok := i.(int)
	fmt.Println(s1, ok)
	s2, ok := i.(Student)
	fmt.Println(s2, ok)
	// 如果只有一个接收的变量,这个值就是口中保存的值
	s3 := i.(Student)
}



type Student struct {
	name string
}
4.5 类型选择

可以使用switch i.(type)作类型选择,i.(type)会返回i中保存的变量,并且只能用于switch:

package main
import "fmt"

func main() {
	var i interface{} = Student{"Tom"}
	switch v := i.(type) {
	case string:
		fmt.Println("string")
	case int:
		fmt.Println("int")
	case Student:
		fmt.Println("student", v)
	}
}



type Student struct {
	name string
}
4.6 Stringer

fmt 包中定义的 Stringer 是最普遍的接口之一。

type Stringer interface {
    String() string
}

该接口的方法有点类似于toString()方法,可以返回用fmt.Sprintf()格式化好的字符串,在调用fmt.Print()等方法时会被输出:

package main
import "fmt"

func main() {
	s := Student{"Tom", 18}
	fmt.Println(s)
}



type Student struct {
	name string
	age int
}

func (s Student) String() string {
	return fmt.Sprintf("Student[name = %v, age = %v]", s.name, s.age)
}
4.7错误

在go中有一个内置接口error,定义如下:

type error interface {
	Error() string
}

自定义异常时,只需要实现这个接口即可 :

type MyError struct {
	Name string
	Desc string
}

func (m *MyError) Error() string {
	return fmt.Sprintf("错误:(%v),%v", m.Name, m.Desc)
}

使用自定义异常时,在要抛出异常的地方返回该异常:

func testError(i int) error {
	if i == 0 {
		return &MyError{"零值", "接收的参数不能为零"}
	}
	fmt.Println("接收的参数是:", i)
	return nil
}

要捕获异常时,判断返回的异常的值:

func main() {
	err := testError(0)
	if err != nil {
		fmt.Println(err)
	}
}


4.8 Reader
package main
import (
	"fmt"
	"strings"
	"io"
)

func main() {
	// 被读取的数据
	s := strings.NewReader("hello world!")
	
	// 切片,容量长度为8
	b := make([]byte, 8)
	for {
		n, err := s.Read(b)
		// n是读取了多少 长度 err在读取到文件末尾时返回eof错误
		fmt.Printf("%q", b[:n])
		println()
		if err == io.EOF {
			break
		}
	}
}

5 并发

5.1 goroutine

使用go关键字,方法会启用新的线程来执行:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 使用了go关键字 开启新的线程运行
	go say("hello")
	// 当前线程执行下面的方法 
	say("hi")
}

func say(s string) {
	for i := 0; i < 5; i++ {
		fmt.Println(s)
		time.Sleep(100 * time.Millisecond)
	}
}

5.2 信道

5.2.1 创建信道
package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建信道
	c := make(chan int)
	go in(1, c)
	// <- 从信道中拿出数据 是阻塞的操作,会等待数据写入
	fmt.Println(<- c)	
}

func in(i int, c chan int) {
	time.Sleep(1000 * time.Millisecond)
	c <- i
}

5.2.2 写入和取出数据

通过<-进行写入和读取数据,如上所示c <- i就是把i写入信道c中,<- c就是把信道中的值从信道中读出。

5.2.3 带缓冲的信道

在创建信道是指定缓冲大小,缓冲区满了就无法再写入数据。

package main

import (
	"fmt"
)

func main() {
	c := make(chan int, 2)
	c <- 1
	c <- 2
	// c <- 3
	// fmt.Println(<-c)
	fmt.Println(<-c)
	fmt.Println(<-c)
}
5.2.3 range和close

range可以从信道中读取值,直到遇到close:

package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan int, 2)
	go in(c)
	for i := range c {
		fmt.Println(i)
	}
}

func in(c chan int) {
	for i := 0; i <= 10; i++ {
		time.Sleep(1000 * time.Millisecond)
		c <- i
	}
	// 不写入数据后close
	close(c)
}

5.2.4 select

从信道中读取数据时,如果信道中没有数据且没关闭,会出现死锁的情况,如果使用select就不会出现类似情况:

package main

import (
	"fmt"
	"time"
)

func main() {
   i := make(chan int, 10)
   s := make(chan string, 5)
   for a := 0; a < 10; a++ {
	   i <- a
   }
   for a := 0; a < 5; a++ {
	   s <- "h"
   }

   for {
	   select {
	   // 从 i 中读取数据,如果读取到了就执行
	   case v := <- i:
		fmt.Printf("从i中读取的数据:%d \n", v)
		time.Sleep(100 * time.Millisecond)
		// 从s中读取数据,如果读取到了就执行
	   case <- s:
		fmt.Println("从s中读取了数据")
		time.Sleep(100 * time.Millisecond)
	   default:
		fmt.Println("默认...")
		return;
	   }

   }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值