Go基础学习笔记

基础

tips:  1.Go语言中如果标识符(变量名,函数名,类型名,方法名)首字母是大写的,就表示对外部包可见(公共的)
	   2.当标识符大写后会有黄色波浪线提示,给予注释后可消除(注释格式:Dog 这是一个狗的结构体)

变量

// Go语言中声明了全局变量,可以不使用。
// Go语言在构建时为了节省内存,创建非全局变量后必须要使用(全局变量可以不使用,因为全局变量可能在其他包中被使用)
// Go语言中声明了局部变量必须要使用,不使用不能编译
1.批量变量声明
var (
	age  int
	name string
	isOK bool
)

2.声明变量同时赋值
var s1 string ="adq"

3.短变量声明(根据值自动判断类型,只能在函数中用)
s3 := "abc"

4.匿名变量 匿名变量不占用命令空间,也不占用内存,所以匿名变量之间不存在重复声明
func foo() (int, string) {
    return 10 , "abc"
}
func main() {
    x, _ := foo()
    fmt.Println("x=", x)
}

注意事项:
	1.函数外的每个语句都必须以关键字开始
	2.:=不能使用在函数外
	3._多用于占位,表示忽略值

常量

// Go中使用const表示常量
const pi = 3.1415

func main() {
	fmt.Print(pi)
}
//批量声明常量,如果后面没有值,默认和上一行一致
const (
	n1 = 100
	n2 
	n3
)
// iota (面试经常会问)
iotago语言的常量计数器,只能在常量的表达式中使用
iotaconst关键字出现将被重置为0const中每新增一行常量声明将使iota计数一次(可以理解为行索引)
const (
	a1 = iota //0
	a2		  //1
	a3  	  //2
	a4        //3
)

字符串

Go语言中字符串是用双引号包裹的
Go语言中单引号是用来表示字符

s1 := "hello world!你好"
s2 := 'a'
// 声明过个字符串,使用·· (esc下面的按键)反引号
s := `
	jack
	tom
	level `
// 字符串长度
len(s)
// 字符串拼接
s3 := s1 + s2
s3 := fmt.Sprintf("%s%s", s1, s2)
// 字符串分隔
strings.Split(s, ",")
// 判断是否包含
strings.Contains(s, "hello")
// 判断前后缀
strings.HasPrefix(s, "hello")
strings.HasSufix(s, "hello")
// 字符串所在的位置
strings.Index(s, "a")
// 连接
Strings.Join([]string, "+")

Go中字符有两种类型:

	1. `uint8`类型,或者叫byte型,代表了ASCII码的一个字符(英文一个字符)
	2. `rune`类型,代表一个`UTF-8`字符(中文一个字符)

修改字符串

要修改字符串,需要先转换成[]rune[]byte

rune是int32的别名(-231~231-1),对于byte(-128~127),可表示的字符更多。由于rune可表示的范围更大,所以能处理一切字符,当然也包括中文字符。在平时计算中文字符,可用rune。

s1 := "字符串"
s2 := []rune(s1) //将s1转换成rune数组
s2[0] = '英'
fmt.Printf(string(s2)) //将rune切片强制转换成字符串

类型转换

// 将10强制转换成float64类型
n = 10
var f float64
f = float64(n)

作业

统计字符串“hello沙河小王子”中汉子的数量

import (
	"fmt"
	"unicode"
)

func Chinesecount(s string) (count int) {
	for _, char := range s {      //for range循环
		if unicode.Is(unicode.Han, char) {
			count++
		}
	}
	return count
}
func main() {
	str := "hello沙河小王子"
	result := Chinesecount(str)
	fmt.Println(result)
}

判断 循环

if 判断

age := 19
if age > 18 {
   fmt.Println("成年")
}else {
   fmt.Println("未成年")
}

if ages := 18; ages>18{ //这里的ages只作用于这个循环中 
	fmt.Println("成年")
}else {
	fmt.Println("未成年")
}
switch
var n = 3
switch n {
case 1:
	fmt.Println("A")
case 2:
	fmt.Println("B")
case 3:
	fmt.Println("C")
	fallthrough			//执行完后,向下穿透一行(不用写)
case 4:
	fmt.Println("D")
}

fmt.Println("---------------------------------")
	
switch a := 3; a {
case 1:
	fmt.Println("A")
case 2:
	fmt.Println("B")
case 3:
	fmt.Println("C")
case 4:
	fmt.Println("D")
}
goto

做一个标志位,跳转到指定位置

for 循环

for

Go语言中的break可以跳出循环,continue可以跳过此次循环

// 基本格式
for i := 0; i < 5; i++ {
	fmt.Print(i)
}

// 变种1
var i = 0
for ; i < 5 ; i++{
		fmt.Print(i)
}
for range

Go语言中可以使用for range遍历数组,切片,字符串,map以及通道(channel):

  1. 数组,切片,字符串返回索引和值
  2. map返回键和值
  3. 通道(channel)只返回通道内的值

https://blog.csdn.net/fzeyu/article/details/88557065

s := "hello world"
for i, v := range s {
	fmt.Printf("%d %c\n", i, v)
}

数组

存放元素的容器

必须制定存放的元素的类型和长度

Go语言中数组的长度是类型的一部分,长度不同,类型也不同

​ var a1 [3]bool 与var a1 [4]bool 不能作比较

数组初始化

如果不初始化,默认元素都是零值

// 方式一
a1 = [3]bool{true, true, true}
// 方式二
a100 := [...]int{1,2,3,65,5,4,2,5,4}  //[...]表示根据初始值推断数组长度
// 方式三
a3 := [5]int{0: 5, 4: 2}  //根据索引初始化,将第一个数赋值5,第五个数赋值2

数组遍历

citys := [...]string{"北京", "上海", "苏州"}
// 1.根据索引遍历
for i := 0; i < len(citys); i++ {
	fmt.Println(citys[i])
}
// 2.使用for range遍历
for key, value := range citys {
	fmt.Println(key, value)
}

多维数组

// 1.声明多维数组
var a11 [3][2]int	//[[1,2][2,3][3,4]]
// 2.初始化多维数组
a11 = [3][2]int{
    [2]int{1, 2},
	[2]int{3, 4},
    [2]int{5, 6}  
}

切片(slice)

切片是一个动态数组,与数组相比切片的长度是不固定的,可以追加元素,再追加可能使切片的容量增大

切片是一个引用类型,它的内部结构包含地址,长度和容量。真正的数据是保存在底层的数组中。

切片就是一个框,框住了一块连续的内存

var s []int  //定义一个存放int类型的切片

切片初始化

s = []int{1, 2, 3}

由数组得到切片

切片指向了一个底层数组

切片的长度就是它的元素个数

切片的容量是底层数组从切片的第一个元素到最后一个元素

a1 := [...]int{1,2,3,4,5,6}
s1 := a1[0:4]   //基于数组切割,左包又不包
s2 := a1[:4]
s3 := a1[3:]
s4 := a1[:]
fmt.Println(s1)  
fmt.Println(s2)
fmt.Println(s3)
fmt.Println(s4)

// 底层数组从切片的第一个元素到最后的元素数量
fmt.Println(s2, len(s2), cap(s2)) //运行结果为 [1 2 3 4] 4 6   
fmt.Println(s3, len(s3), cap(s3)) //运行结果为 [4 5 6] 3 3

使用make创建切片

s1 := make([]int,5,10)   // 创建一个int类型的长度为5,容量为10的切片,容量不写默认和长度一样

append

s1 := []string{"北京", "上海"}
// 调用append函数必须要用原来的切片变量接收返回值
// append追加元素,原来的底层数组放不下的时候,go语言就会把底层数组换一个
s1 = append(s1, "广州")
fmt.Println(s1)

在这里插入图片描述

扩容策略:

  1. 首先判断,如果新申请(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。

  2. 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍(newcap = doublecap)

  3. 否则判断,如果旧切片长度大于等于1024,则最终容量 newcap从旧容量 oldcap开始循环增加到原来的1/4

    newcap=old.cap,for {newcap += newcap/4}直到最终容量 newcap大于等于

    \新申请的容量newcap >= cap

  4. 如果最终容量计算值溢出,则最终容量就是新申请容量

Tips:切片扩容还会根据切片中元素的类型不同做不同的处理,如:int,string类型的处理方式就不一样

copy

在这里插入图片描述

删除元素

x1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8}
s1 := x1[:]
s1 = append(s1[:2], s1[4:]...)
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(x1)

指针

Go语言不存在指针操作,只要记住两个符号

  1. & 取地址
  2. * 根据地址取值

new创建指针

new 申请一个内存地址

var a1 *int
fmt.Println(a1)  //<nil>

var a2 = new(int)
fmt.Println(a2)  //0xc0000140d0
fmt.Println(*a2) //0

*a2 = 100
fmt.Println(*a2)  //100

make也是用于内存分配的,它只用来slice,map,chan的内存创建

make和new的区别:

  1. make和new都是用来申请内存的
  2. new很少用,一般用来给基本数据类型申请内存, 返回对应类型的指针
  3. make是用来给slice,map,chan申请内存的,make函数返回的是对应的这三个类型的本身

Map集合

map是一种无序的基于key-value的数据结构(底层基于hash实现),Go语言中的map是引用类型,必须初始化才能使用

定义:

map[KeyType]ValueType

判断

value, ok := m1["fgv"] //判断map中是否有对应的key   约定成俗用ok接收布尔值
if !ok {
	fmt.Println("not exist")
} else {
	fmt.Println(value)
}

遍历

for k, v := range m1 {
	fmt.Println(k, v)
}

删除

delete(m1,"bcd")
fmt.Println(m1)

顺序输出

rand.Seed(time.Now().UnixNano()) //生成随机数

var scoreMap = make(map[string]int, 200)

for i := 0; i < 100; i++ {
	key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
	value := rand.Intn(100)        //生成0-99的随机整数
	scoreMap[key] = value
}
fmt.Println(scoreMap)

var keys = make([]string, 0, 200) //将key取出来,放入切片中排序
for key := range scoreMap {
	keys = append(keys, key)
}

sort.Strings(keys)
// 根据排序后的key,取值
for _, key := range keys {
	fmt.Println(key, scoreMap[key])
}

slice_map

import "fmt"

func main() {
	// map类型的slice
	// 先对slice初始化
	var s1 = make([]map[int]string, 10, 10)
	// 在对map初始化
	s1[0] = make(map[int]string, 1)
	s1[0][10] = "A"
	fmt.Println(s1)

	// slice类型的map
	var m1 = make(map[string][]int, 10)
	m1["上海"] = []int{10, 20, 30}
	fmt.Println(m1)
}

函数

返回值可以命名也可以不命名

命名的返回值就相当于在函数中声明了一个变量

func f1(x int, y int) (ret int) {
	ret = x + y
	return   // 声明函数时指定返回值,return可以省略返回值变量
}
func f2(x int, y int) int {
	ret := x + y
	return ret
}

// 多返回值
func f3() (int, int) {
	return 1, 2
}

// 参数类型简写
// 多个参数类型相同时,可以shenglue
func f4(x, y int, m, n string, i, j bool) int {
	return x + y
}
// 可变长参数
func f5 (x string,y ...int){ // y可以看做是一个int类型的切片

}
func main() {
	m, n := f3()
	fmt.Println(m, n)

	f5("a",1,2,3,4)
}

defer

Go语言中的defer语句会将其后面跟随的语句进行延迟处理,在defer归属的函数即将返回时,将延迟的处理的语句按defer定义的逆顺序进行执行。先defer的语句最后被执行,最后被defer的语句,最先执行

多用于释放资源(文件,数据库,socket),可以有多个

在这里插入图片描述

函数进阶

函数中查找变量的顺序

  1. 现在函数内部查找
  2. 函数内部找不到,就在函数传入的参数中查找
  3. 找不到,就在函数外面查找,一直找到全局变量

作用域

在函数内部定义的变量只能在该函数内部使用

参数和返回值

func f1() {
	fmt.Println("hello")
}

func f2() int {
	return 10
}

// 函数作为参数
func f3(x func() int) {
	ret := x()
	fmt.Println(ret)
}

//函数作为返回值
func f5(x func() int) func(int, int) int {
	ret := func(a, b int) int {
		return 5
	}
	return ret
}
func main() {
	a := f1
	fmt.Printf("%T\n", a)

	b := f2
	fmt.Printf("%T\n", b)

	f3(f2)
	fmt.Printf("%T\n", f5(f2))
}

匿名函数

//匿名函数,一般在函数内部使用
func main() {
	f1 := func(x, y int) {
		fmt.Println(x + y)
	}
	f1(10, 20)

	// 如果函数只是调用一次,还可以简写成立即执行函数
	func(x, y int) {
		fmt.Println("aaaa")
	}(10, 20)
}

闭包

作用:缩小变量作用域,减少对全局变量的污染

闭包详解

闭包是一个函数,这个函数包含了外部作用域的一个变量(闭包 = 函数 + 外部变量的引用)

底层原理:

  1. 函数可以作为返回值
  2. 函数内部查找变量的顺序,现在自己内部找,找不到往外层找

结构体

Go语言中没有“类”的概念,也不支持“类”的继承等面对对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性

Go语言结构体作为函数参数,采用的是值传递。所以对于大型结构体传参,考虑到值传递的性能损耗,最好能采用指针传递。

结构体占用的一块连续的内存空间

// 自定义类型和类型别名
type myInt int		//自定义类型
type yourInt = int  //类型别名    rune类型就是int32的类型别名
//结构体是值类型
type person struct {
	name   string
	gender string
}

// go 语言中函数参数永远是拷贝的
func f(x person) {
	x.gender = "女" //修改的是副本,并不是p.gender本身的值
}
func f2(x *person) {
	(*x).gender = "女" //根据内存地址找到那个原变量,修改的就是原来的变量 //可以写成: x.gender = "女"
}

func main() {
	var p person
	p.name = "dzq"
	p.gender = "男"
	fmt.Println(p.gender)
	f2(&p)
	fmt.Println(p.gender)
}
//结构体是值类型
type person struct {
	name   string
	gender string
}

// go 语言中函数参数永远是拷贝的
func f(x person) {
	x.gender = "女" //修改的是副本,并不是p.gender本身的值
}
func f2(x *person) {
	(*x).gender = "女" //根据内存地址找到那个原变量,修改的就是原来的变量 //可以写成: x.gender = "女"
}

func main() {
	var p person
	p.name = "dzq"
	p.gender = "男"
	fmt.Println(p.gender)
	f2(&p)
	fmt.Println(p.gender)
	fmt.Printf("%p\n", &p)
    
	// 1.结构体指针1
	var p2 = new(person)
	p2.name = "ddd"
	p2.gender = "qwe"
	fmt.Printf("%p\n", p2)  // p2的值就是一个内存地址
	fmt.Printf("%p\n", &p2) // 求p2的内存地址

	// 2.结构体指针
	// 2.1key-value初始化
	var p3 = person{
		name : "q",
		gender: "girl",
	}
	fmt.Printf("%#v\n", p3)
	// 2.2 使用值列表的形式初始化,值得顺序要和结构体定义时的字段的顺序一致
	var p4 = person{
		"q",
		"girl",
	}
	fmt.Printf("%#v\n", p4)
}

嵌套结构体

// 结构体嵌套,申明,调用
type address struct {
	city string
}

type person struct {
	name string
	age  int
	addr address
}

// 匿名嵌套结构体
type company struct {
	name string
	address
}

func main() {
	p1 := person{
		name: "dzq",
		age:  18,
		addr: address{
			city: "xuancheng",
		},
	}
	c1 := company{
		name: "dzq01",
		address: address{
			city: "wuhu",
		},
	}
	fmt.Println(p1.name, p1.addr.city)
	fmt.Println(c1.city) //可以直接访问city--先在自己的结构体中找这个字段,找不到就去匿名嵌套结构体中查找该字段
}

结构体模拟实现继承

构造函数

Go语言中以new开头的基本都是构造函数

Go语言强调面向接口编程

当结构体比较大的时候尽量使用结构体指针,减少程序的内存开销。结构体为值传递,字段较多时,由于字段内存比较大,频繁拷贝会影响内存使用率

// 构造函数
type person struct {
	name string
	age  int
}
// 返回的是结构体还是结构体指针
func newPerson(name string, age int) *person {
	return &person{
		name: name,
		age:  age,
	}
}
func main() {
	p1 := newPerson("dzq", 18)
	p2 := newPerson("dd", 18)
	fmt.Println(p1, p2)
}

方法和接收者

方法是作用于特定类型的函数

接受者表示的是调用该方法的具体类型变量,多用类型名首字母小写表示

格式:

func (接受者变量 接受者方法) 方法名(参数列表)(返回参数){
函数体
}

type dog struct {
	name string
	age  int
}

func newDog(name string, age int) dog {
	return dog{
		name: name,
		age:  age,
	}
}

func (d dog) wang() {
	fmt.Printf("%s:aaaa", d.name)
}

//使用值接受者:传值
func (d dog) addage() {
	d.age++
}

//使用指针接受者:传地址
func (d *dog) addage2() {
	d.age++
}

func main() {
	// d1 := newDog("qwe")
	// d1.wang()
	d1 := newDog("qwe", 12)
	d1.addage()
	fmt.Println(d1.age) //12
	d1.addage2()
	fmt.Println(d1.age) //13
}

什么时候应该适应指针类型接受者

  1. 需要修改接受者中的值
  2. 接受者是拷贝代价比较大的大对象
  3. 保证一致性,如果有某个方法使用了指针接受者,那么其他的方法也应该使用指针接受者

接口(interface)

接口是一种类型(引用类型)

接口的定义

type interface_name interface {
    method_name1(parameter 1, parameter 1) 返回值
    method_name2(parameter 1, parameter 1) 返回值
}

用来给变量,参数,返回值等设置类型

接口的实现

一个变量如果实现了接口中规定的所有方法,那么这个变量就实现了这个接口,可以称为这个接口类型的实现

type cat struct{}

type dog struct{}

type speaker interface {
	speak()
}

func (c cat) speak() {
	fmt.Println("喵喵喵")
}

func (d dog) speak() {
	fmt.Println("汪汪汪")
}

func da(x speaker) {
	x.speak()
}

func main() {
	var c1 cat
	var d1 dog
	da(c1)   // 不关心传入的是什么类型
	da(d1)
}

接口保存分为值的类型和值本身,这样不管传入什么类型的值,都可以存入进来

实现接口

使用值接收者实现接口和指针接收者实现接口的区别?

  1. 使用指针接受者实现接口,结构体类型和结构体指针类型的变量都能存
  2. 指针接受者实现接口只能存结构体指针类型的变量
// 使用值接收者和指针接收者的区别
type animal interface {
	move()
	eat(string)
}

type cat struct {
	name string
	feet int
}

// //使用值接受者实现
//func (c cat) move() {
//	fmt.Println("猫移动")
//}
//func (c cat) eat(str string) {
//	fmt.Println("吃" + str)
//}

//使用指针接收者实现
func (c *cat) move() {
	fmt.Println("猫移动")
}
func (c *cat) eat(str string) {
	fmt.Println("吃" + str)
}

func main() {
	var a1 animal
	c1 := &cat{"tom", 4}
	c2 := &cat{"aaa", 4}

	a1 = c1
	fmt.Println(a1)
	a1 = c2
	fmt.Println(a1)
}

接口和类型的关系

  1. 多个类型可以实现一个接口

  2. 多个接口可以实现一个类型

  3. 接口可以嵌套

空接口

没有必要起名字,通常定义成下面的格式

interface{}    //空接口

空接口类型的变量可以存储任意类型的变量(例:一个方法需要可以传入任意类型的变量,可使用空接口)

func show(a interface{})  {
	fmt.Println(a)
}
func main() {
	var m1 map[string]interface{}
	m1 = make(map[string]interface{}, 5)
	m1["name"] = "dzq"
	m1["age"] = 18
	m1["hobby"] = [...]string{"a", "b", "c"}
	fmt.Println(m1)
}

线程

func hello(i int)  {
	fmt.Println(i)
}

func main() {
	for i := 0; i < 1000; i++ {
		go func(i int) {
			fmt.Println(i)
		}(i)
	}
	fmt.Println("mian")
	time.Sleep(time.Second)
}

goroutine结束

goroutine对应的函数结束了,goroutine结束了

main函数执行完了,由main函数创建的goroutine也结束了

waitgroup

var wg sync.WaitGroup

func f() {
	defer wg.Done()
	rand.Seed(time.Now().Unix())
	for i := 0; i < 10; i++ {
		r1 := rand.Int() //int64的随机数
		r2 := rand.Intn(10)
		fmt.Println(r1, r2)
	}
}

func f1(i int) {
	defer wg.Done()
	time.Sleep(time.Millisecond * time.Duration(rand.Intn(300)))
	fmt.Println(i)
}

func main() {
	for i := 0; i < 10; i++ {
		wg.Add(2)  //wg.Add(n)把计数器加n,Done()把计数器-1,wait()会阻塞代码的运行,直到计数器的值减为0
		go f1(i)
		go f()
	}
	wg.Wait()   //等待wg的计数器减为0
}

goroutine 调度模型GMP模型

Go语言基础之并发 | 李文周的博客 (liwenzhou.com)

m:n 把m个goroutien分配给n个操作系统线程去执行

goroutine初始栈的大小是2 kb,可以扩容或缩小

channel

// 启动一个goroutine,生成100个数发送到ch1
// 启动一个goroutine,从ch1中取值,计算其平方放到ch2中
// 在main中从ch2取值打印出来
var wg sync.WaitGroup

func main() {
	var ch1 chan int
	var ch2 chan int
	ch1 = make(chan int, 50)
	ch2 = make(chan int, 50)
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < 100; i++ {
			ch1 <- i
		}
		close(ch1)
	}()

	go func() {
		defer wg.Done()
		for a := range ch1 {
			ch2 <- a * a
		}
		close(ch2)
	}()
	//wg.Wait()
	for b := range ch2 {
		println(b)
	}
	wg.Wait()
}

互斥锁

var x = 0
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
	for i := 0; i < 5000; i++ {
		lock.Lock()
		x = x + 1
		lock.Unlock()
	}
	wg.Done()
}

func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

读写互斥锁

大多数场景下读多写少,当我们并发的去读取一个资源不涉及资源修改的时候是没必要加锁的,这种情况下使用读写锁是一种更好的。

读的次数远远大于写的时候使用

待wg的计数器减为0
}

goroutine 调度模型GMP模型

Go语言基础之并发 | 李文周的博客 (liwenzhou.com)

m:n 把m个goroutien分配给n个操作系统线程去执行

goroutine初始栈的大小是2 kb,可以扩容或缩小

channel

// 启动一个goroutine,生成100个数发送到ch1
// 启动一个goroutine,从ch1中取值,计算其平方放到ch2中
// 在main中从ch2取值打印出来
var wg sync.WaitGroup

func main() {
	var ch1 chan int
	var ch2 chan int
	ch1 = make(chan int, 50)
	ch2 = make(chan int, 50)
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < 100; i++ {
			ch1 <- i
		}
		close(ch1)
	}()

	go func() {
		defer wg.Done()
		for a := range ch1 {
			ch2 <- a * a
		}
		close(ch2)
	}()
	//wg.Wait()
	for b := range ch2 {
		println(b)
	}
	wg.Wait()
}

互斥锁

var x = 0
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
	for i := 0; i < 5000; i++ {
		lock.Lock()
		x = x + 1
		lock.Unlock()
	}
	wg.Done()
}

func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

读写互斥锁

大多数场景下读多写少,当我们并发的去读取一个资源不涉及资源修改的时候是没必要加锁的,这种情况下使用读写锁是一种更好的。

读的次数远远大于写的时候使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值