Go语言基础(6)-- 复合数据类型

本文深入探讨Go语言中的复合数据类型,包括数组的定义、初始化、操作以及注意事项;切片的概念、操作、追加、拷贝和删除;结构体的创建、赋值、比较、数组与切片、作为函数参数及与JSON的交互;指针的定义、创建内存空间以及与new和make的区别。通过实例展示了Go语言在数据结构和内存管理上的灵活性。
摘要由CSDN通过智能技术生成

六、复合数据类型

复合类型主要包括了数组指针切片结构体map等。

6.1、数组

数组: 是指一系列同一类型数据的集合。是在内存中连续存储的多个相同类型的数据集合。

● 数组是值类型

● 数组支持==与!=

● 在数组中找具体元素使用下标,下标是从0始的,最大为数组个数减一。

● 定义格式:

var arr [10]int 

● 初始化 ①:(初始化部分元素值,其他值均为0)

var arr [10]int = [10]int{1,2,3,4}

● 初始化 ②:

arr := [10]int{1,2,3,4}

● 初始化 ③:(6:8 表示下标为6的赋值为8)

arr := [10]int{1,2,3,4,6:8}

● 初始化 ④:(使用自动类型推导可以根据元素个数创建数组)

arr := [...]int{1,2}

fmt.Println(arr)//[1 2]

fmt.Printf("%T",arr)//[2]int

6.1.1、len(参 数)作用

● 获取字符串有效长度

● 获取不定参函数参数个数

● 计算数组元素个数

6.1.2、注意

● 数组在定义后元素个数就已经固定不能添加。

● 小于0个或者大于定义个数的数组下标,都会引发数组越界

● 数组是一个常量,不允许赋值(可以给数组中的每一个元素赋值),数组名代表整个数组,只能允许相同类型进行赋值(即相同数组长度)。

● 数组名也可以表示数组的首地址,即 &arr == &arr[0]。

6.1.3、示例

求最大值
arr := [10]int{9, 1, 5, 6, 7, 3, 10, 2, 4, 8}
max := arr[0]
for i := 1; i < len(arr); i++ {
  if max < arr[i] {
   max = arr[i]
  }
}
fmt.Println("最大值是:", max)
数组置换
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
//定义起始和结束下标
start := 0
end := len(arr) - 1
for {
  if start > end {
   break
  }
  arr[start], arr[end] = arr[end], arr[start]
  start++
  end--
}
fmt.Println(arr)
③ 冒泡排序
arr := [10]int{9, 1, 5, 6, 7, 3, 10, 2, 4, 8}
//外层控制行
for i := 0; i < len(arr)-1; i++ {
  //内层控制列
  for j := 0; j < len(arr)-1-i; j++ {
   //比较两个相邻元素满足条件交换数据
   //升序排序
   if arr[j] > arr[j+1] {
     arr[j], arr[j+1] = arr[j+1], arr[j]
   }
   //fmt.Println(arr)
  }
}
fmt.Println(arr)

6.1.4、数组作为函数参数

注意: 数组作为函数参数是 值传递 的(不是 地址 传递)

① 作为参数

func testArr(arr [10] int)  {
  fmt.Println(arr)
  }

 

func main() {
  arr := [10]int{9, 1, 5, 6, 7, 3, 10, 2, 4, 8}
  testArr(arr)
  }

② 作为参数和返回值

func testArr(arr [10] int) [10] int {
  fmt.Println(arr)
  return arr
  }

6.1.5、二维数组

定义格式

var 数组名 [行个数][列个数]数据类型

var a [3][2]int
a = [3][2]int{[2]int{1, 2}, [2]int{3, 4}, [2]int{5, 6}}
fmt.Println(a)
//多维数组的遍历
for _, v1 := range a {
  fmt.Print(v1)
  fmt.Printf("\t")
  for _, v2 := range v1 {
   fmt.Printf("%d ",v2)
  }
  fmt.Println()
}

6.2、随机数

6.2.1、导头文件

在这里插入图片描述

import (
  "math/rand"
  "time"
)

6.2.2、添加随机数种子

rand.Seed(time.Now().UnixNano())

6.2.3、使用随机数

for i:=0;i<10;i++{
  fmt.Println(rand.Intn(123))
}

6.3、切片

6.3.1、概念

切片: 长度是不固定的,可以追加元素,在追加时可能使切片的容量增大,所以可以将切片理解“动态数组”,但它不是数组,切片存在堆区。切片不保存具体的值,像一个框,底层是数组。

注意: 切片之间是不能比较的,不能使用 == 操作符来判断两个切片是否含有全部相等元素。切片唯一合法的比较操作是和nil比较。一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是不能说一个长度和容量都是0的切片一定是nil。

定义:var 切片名 []数据类型

var s []int

定义指定长度的切片(make()函数创建)

make([]T,size,cap)

T:切片的元素类型,size:切片中元素的数量,cap:切片的容量

a := make([]int, 8,10)

6.3.2、赋值

简单赋值:

a := make([]int, 8)
a[0] = 1

6.3.3、切片截取

在这里插入图片描述

同数组截取

注意:

切片截取后值失窃片的一部分,其值改变后,原切片的值也会改变。两 个操作的是同一地址内的数据。

格式:

切片名[起始下标 结束下标(不包含) 容量]

容量的值必须小于原始容量,大于等于截取的个数,默认等于原始容量

s:=[]int{1,2,3,4,5}
  slice0:=s[2:]
  slice:=s[:2]
  fmt.Println(slice0)
  fmt.Println(slice)
}

6.3.4、切片的追加、拷贝、删除注意事项

追加:

● 切片的每一次追加数据,可能会导致切片的首地址发生变化。(如果容量扩充导致输出存储溢出切片会自动找寻新的空间存储数据。)

● 调用append函数必须用原来的切片变量接收返回值append追加元素,原来的底层数组放不下的时候,Go底层就会把底层数组换一个。所以需要接收返回值。

● 切片中,把一个切片中的内容append都另一个切片,用 … 拆分

s1 := []string{"北京", "上海", "深圳"}
	//s1[3] = "广州" //错误的写法会导致编译错误:索引越界
    ss := []string{"武汉","西安","苏州"}
    s1 = append(s1, ss...)//...表示拆开

拷贝:

● 拷贝后的元素是新的,修改后不会改变原来切片的值

删除:

Go语言中没有专门的删除操作,可以利用切片特性,自己封装。

// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
//要删除索引为3的元素
a = append(a[:3], a[4:]...)
fmt.Println(a) //[30 31 32 34 35 36 37]

6.3.5、切片做为函数参数和返回值

作为参数

形参和实参使用的是同一内存地址空间,传递时是地址传递

func test22(s []int) {
  fmt.Printf("%p\n", s)
 }
func main() {
  s := []int{1, 2, 3, 4, 5}
  test22(s)//0xc00000e390
  fmt.Printf("%p\n", s)//0xc00000e390
}

引用计数

在函数内部append切片

注意:如果使用append操作切片可能改变切片的地址,需要使用返回值给实参赋值。

func test111(s []int){
  s=append(s, 4,5,6)
  fmt .Printf( "%p\n",s)//0xc0000c2060
  fmt .Println(s)//[1 2 3 4 5 6]
}
func main(){
  s:=[]int{1,2,3}
  fmt.Printf("%p\n",s)//0xc0000ae090
  test111(s)
  fmt.Println(s)//[1 2 3]
}

6.3.6、常用操作

① append()添加元素(在长度后添加):

s = append(s, 2,3,4)
s := make([]int, 5)
s = append(s, 1, 2, 3)
fmt.Println(s)//[0 0 0 0 0 1 2 3]

② len()查询切片使用的长度

b := len(a)

③ cap()查询切片整体容量(容量大于等于使用长度)

容量扩展规则:

★ 如果新容量大于原来的2倍,则现容量就是新容量

★ 如果不超过1024字节,每次扩展都是上次的2倍

★ 如果超过,每次扩展是上次的四分之一

Go源码

在这里插入图片描述

fmt.Println(cap(s))

④ copy(切片1,切片2)复制切片:将s1切片中的数据拷贝到s2中,s2中要有足够的容量

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

s2:=make([]int,5)

copy(s2,s1)

copy(s2,s1[1:3])//如果想拷贝切片中的片段,需要使用截取

⑤ for遍历

for i, v := range s {
     fmt.Println(i, v)
  }

6.3.7、切片的注意事项

var a = make([]int, 5, 10) //创建切片,长度为5,容量为10
fmt.Println(a)//[0 0 0 0 0]
for i := 0; i < 10; i++ {
  a = append(a, i)
}
fmt.Println(a) //[0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]

6.4、Map

翻译:字典结构(建议读map类型)

由键值对构成无序的,内部由hash实现的数据结构,是引用类型。

6.4.1、定义格式

● 定义字典结构使用map关键字,[]中指定的是键(key)的类型后面紧跟着的是值的类型。

的类型,必须是支持==和!=操作符的类型,切片、函数以及包含切片的结构类型不能作为字典的键,使用这些类型会造成编译错。

● 值的类型,任意都可以。

● map类型的长度是自动扩容的,长度和容量始终保持相等。

● map中的数据存储是无序的,所以遍历用rang遍历。

● map 中的键必须是唯一的,在定义时不唯一会报错。

● map 中的键没有的时候,取值会得到对应类型的默认值

map[keyType]valueTye
var  ml map[int]string

m :=make(map[int]string,50)//尽量预估数量,避免动态扩容

n := map[string]int{"赵四": 10}

6.4.2、对map元素的操作

ok(自己定义的,约定用Ok接收返回bool值)判断值存在与否
m := map[int]string{1: "hell", 2: "o", 3: "word"}
value, ok := m[1]
if ok == true {
  fmt.Println("m[1] = ", value)
} else {
  fmt.Println("key不存在")
}
rang遍历map
m := map[int]string{1: "hell", 2: "o", 3: "word"}
for k,v:=range m{
  fmt.Println(k,v)
}
delete删除元素
m := map[int]string{1: "hell", 2: "o", 3: "word"}
delete(m,1)

注意,delete删除map中的元素的时候,没有相应键是不会报错的

6.4.3、map作为函数参数

Map做为函数参数是引用传递(地址传递)。

Map在函数中添加数据是允许的,实参也会被影响。

func demo(m map[int]string) {
  m[102] = "李四"
  m[103] = "王五"
  delete(m, 101)
}
func main() {
  m := make(map[int]string, 1)
  m[101] = "张三"
  demo(m)
  fmt.Println(m)//map[102:李四 103:王五]
}

6.4.4、练习

统计一串字符(20个)的每个的单词数目

var arr [20]byte
for i := 0; i < len(arr); i++ {
  fmt.Scanf("%c", &arr[i])
}
m := make(map[byte]int)
for i := 0; i < len(arr); i++ {
  m[arr[i]] ++
}
for k, v := range m {
  if v > 0 {
   fmt.Printf("%c %d\n", k, v)
  }
}

将map里面的值按照键的前后顺序排序

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)
//取出map中的所有key存入切片keys
var keys = make([]string, 0, 280)
for key := range scoreMap {
  keys = append(keys, key)
}
//对切片进行排序
sort.Strings(keys)
//按照排序后的key遍历map
for _,key := range keys {
  fmt.Println(key, scoreMap[key])
}

6.5、结构体

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct 。也就是我们可以通过struct 来定义自己的类型了。

Go语言中通过struct 来实现面向对象。

结构体是值类型。

6.5.1、定义结构体

type Student struct{
id int
name string
}

type后面跟着的是结构体的名字Student, struct表示定义的是-一个结构体。大括号中是结构体的成员,注意在定义结构体成员时,不要加var。

type Student struct{
  id int
  name,sex string
}
func main(){
	var s Student
	s.name="张三"
	s.id = 1
	s.sex="男"
	var s1 Student = Student{1,"李四"}
	s2:=Student{id:10,name:"李四"}
}

6.5.2、结构体初始化

 

//类构造函数初始化:约定成俗,用new开头
type person struct {
  name string
  age  int
}
//类似构造函数 返回值是结构体 
func newPerson(name string, age int) person {
  return person{
   name: name,
   age:  age,
  }
}

//类似构造函数 返回值是指针
func newPerson2(name string, age int) *person {
  return &person{
   name: name,
   age:  age,
  }
}

 
p1 := newPerson("张三", 18)
p2 := newPerson2("李四", 9000)
fmt.Println(p1, p2)//{张三 18} &{李四 9000}

对于函数返回值是结构体还是结构体指针看具体的结构体内的成员:

当结构体内变量少时,选哪个都无所谓。

当结构体内变量多时,尽量选结构体指针。

为的是减少程序运行时候的内存开销。

6.5.3、赋值与比较

赋值(值赋值,两个不会相互影响)

s2:=Student{id:10,name:"李四"}
s3 :=s2

比较,结构体可以使用 == 或 != 来进行成员的值的比较操作

也可以用 > < 来比较成员(int 或 bool)

s3 :=s2
fmt.Println(s2==s3)//true

6.5.4、结构体数组/切片

var 结构体数组名 [元素个数]结构体类型

var ss [8]Student
ss[0].id=1
ss[0].name = "王五"
var ss []Student//切片

● 结构体数组可以进行排序(根据结构体成员)。

● 结构体数组中的元素允许相互赋值交换位置。

● 结构体切片添加信息

var s5 []Student
s5 = append(s5,Student{10,"赵四"})

6.5.5、结构体作为map中的值

m := make(map[int]Student)
m[101] = Student{101, "张三"}
m[102] = Student{102, "李四"}

Map和结构体切片

m := make(map[int][]Student)
m[101] = append(m[101],Student{101, "张"}
				  ,Student{102, "王五"})
m[101] = append(m[101],Student{103, "李四"})
fmt.Println(len(m[101]))//3
fmt.Println(m[101])//[{101 张三} {102 王五} {103 李四}]

6.5.6、结构体作为函数参数

结构体作为参数传递属于值传递

func test123( student Student){
  student.name= "李逵"
}
func main() {
  stu:=Student{101,"宋江"}
  test123(stu)
  fmt.Println(stu)//{101 宋江}
}

6.5.7、创建结构体类型的指针

p1 := new(Student)
p1.name = "张三"  //go语言语法糖,会转化成下面那种格式
(*p1).id = 123
fmt.Println(p1)//&{123 张三}
var p2 = &Student{
  name: "李四",
  id:  111,
}
fmt.Println(p2)//&{111 李四}
var p3 = &Student{
  1234,
  "王五",
}
fmt.Println(p3)//&{1234 王五}

6.5.8、匿名结构体

//匿名结构体
var s struct {
  name string
  age  int
}

6.5.9、结构体内存布局

结构体内的字段是连续的

type x struct {
  a int8 // 8bit 一》1byte
  b int8
  c int8
}

func main() {
  m := x{
   a: int8(18),
   b: int8(20),
   c: int8(30),
  }
  fmt.Printf("%p\n", &(m.a))
  fmt.Printf("%p\n", &(m.b))
  fmt.Printf("%p\n", &(m.c))
}

在这里插入图片描述

6.5.10、结构体匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。

type people struct {
  string
  int
}
func main() {
  var p = people{"张三", 11}

fmt.Println(p.string)//张三

fmt.Println(p.int)//11
  fmt.Println(p)//{张三 11}

}

6.5.11、结构体嵌套

type people struct {
  name string
  age  int
  addr address
}
type address struct {
  province string
  city   string
}

func main() {
  var p = people{
   "张三",
   11,
   address{
     "湖北",
     "武汉",
   }}
  fmt.Println(p) //{张三 11 {湖北 武汉}}

}

对于嵌套的结构体,Go语言的语法糖

type people struct {
  name string
  age  int
  address//将结构体写成匿名嵌套形式,就可以将结构体内的成员当成本结构体的成员直接访问
}
type address struct {
  province string
  city   string
}

func main() {
  var p = people{
   "张三",
   11,
   address{
     "湖北",
     "武汉",
   }}
  fmt.Println(p.province) //湖北 先在自己结构体找这个字段,找不到就去匿名嵌套的结构体中查找该字段
  fmt.Println(p) //{张三 11 {湖北 武汉}}

}

当匿名嵌套结构体字段冲突时,只能全写,不能省略

6.5.12、结构体模拟“继承”

具体方法的定义,看 “八、方法”

//动物的结构体(模拟动物类)
type animal struct {
  name string
}

//给animal结构体实现一个移动的方法
func (a animal) move() {
  fmt.Printf("%s在动!\n", a.name)
}

//狗的结构体(模拟狗类)
type dog struct {
  id uint8
  animal //anima拥有的方法,dog也有了
}

//给dog结构体实现一个汪汪汪的方法
func (d dog) wang() {
  fmt.Printf("%s叫:汪汪汪\n", d.name)
}
func main() {
  var d = dog{
   1,
   animal{
     "小李子",
   },
  }
  fmt.Println(d.id) //1
  fmt.Println(d.name) //小李子
  fmt.Println(d)    //{1 {小李子}}
  d.move()       //小李子在动!
  d.wang()       //小李子叫:汪汪汪
}

6.5.13、结构体与JSON

Json的在线解析器:https://www.json.cn

序列化:Go语言的结构体转换成JSON

type people1 struct {
  name string
  age  int
  address1//将结构体写成匿名嵌套形式,就可以将结构体内的成员当成本结构体的成员直接访问
}
type address1 struct {
  province string
  city   string
}
func main() {
  var p = people1{
   name: "张单",
   age:  18,
   address1: address1{
     "山东",
     "菏泽",
   },
  }
  fmt.Println(p)//{张单 18 {山东 菏泽}}
  marshal,err := json.Marshal(p)
  fmt.Printf("%#v\n",string(marshal))//"{}"
  fmt.Println(string(marshal))//{}
  fmt.Println(marshal)//[123 125]
  fmt.Println(err)//<nil>
}

必须将其中的字段首字母大写,才能在所有包中被访问到

type people1 struct {
  Name string
  Age  int
  address1//将结构体写成匿名嵌套形式,就可以将结构体内的成员当成本结构体的成员直接访问
}
type address1 struct {
  Province string
  city   string
}
func main() {
  var p = people1{
   Name: "张单",
   Age:  18,
   address1: address1{
     "山东",
     "菏泽",
   },
  }
  fmt.Println(p)//{张单 18 {山东 菏泽}}
  marshal,err := json.Marshal(p)
  fmt.Printf("%#v\n",string(marshal))//"{\"Name\":\"张单\",\"Age\":18,\"Province\":\"山东\"}"
  fmt.Println(string(marshal))//{"Name":"张单","Age":18,"Province":"山东"}
  fmt.Println(marshal)//[123 34 78 97 109 101 34 58 34 229 188 160 229 141 149 34 44 34 65 103 101 34 58 49 56 44 34 80 114 111 118 105 110 99 101 34 58 34 229 177 177 228 184 156 34 125]
  fmt.Println(err)//<nil>
}

当需求是,字段必须是小写的时候:

type people1 struct {
  Name string `json:"name" db:"name" xml:"name" ini:"name"`
  Age  int `json:"age"`
  address1//将结构体写成匿名嵌套形式,就可以将结构体内的成员当成本结构体的成员直接访问
}
type address1 struct {
  Province string `json:"province"`
  City   string `json:"city"`
}
func main() {
  var p = people1{
   Name: "张单",
   Age:  18,
   address1: address1{
     "山东",
     "菏泽",
   },
  }
  fmt.Println(p)//{张单 18 {山东 菏泽}}
  marshal,err := json.Marshal(p)
  fmt.Printf("%#v\n",string(marshal))//"{\"name\":\"张单\",\"age\":18,\"province\":\"山东\",\"city\":\"菏泽\"}"
  fmt.Println(string(marshal))//{"name":"张单","age":18,"province":"山东","city":"菏泽"}
  fmt.Println(marshal)//[123 34 110 97 109 101 34 58 34 229 188 160 229 141 149 34 44 34 97 103 101 34 58 49 56 44 34 112 114 111 118 105 110 99 101 34 58 34 229 177 177 228 184 156 34 44 34 99 105 116 121 34 58 34 232 143 143 230 179 189 34 125]
  fmt.Println(err)//<nil>
}

反序列化:JSON转换成Go语言的结构体

type people1 struct {
  Name   string `json:"name" db:"name" xml:"name" ini:"name"`
  Age    int   `json:"age" `
  address1 `:"address_1"` //将结构体写成匿名嵌套形式,就可以将结构体内的成员当成本结构体的成员直接访问
}
type address1 struct {
  Province string `json:"province"`
  City   string `json:"city"`
}

func main() {
  str :=`{"name":"张单","age":18,"province":"山东","city":"菏泽"}`
  var p people1
  json.Unmarshal([]byte(str),&p)
  fmt.Println(p)//{张单 18 {山东 菏泽}}
  fmt.Printf("%#v\n",p)//main.people1{Name:"张单", Age:18, address1:main.address1{Province:"山东", City:"菏泽"}}
}

6.5.14、结构体练习

定义结构体存储5名学生三门成绩求出每名学生的总成绩和平均成绩

type student struct {
  id    int
  name  string
  score  [3]int
}

func main() {
  arr := []student{student{101, "小明", [3]int{100, 99, 88}},
   student{102, "小红", [3]int{88, 56, 83}},
   student{103, "小刚", [3]int{18, 57, 81}},
   student{104, "小强", [3]int{48, 66, 93}},
   student{105, "小花", [3]int{98, 50, 89}}}
  //五名学生
  for i := 0; i < len(arr); i++ {
   //三门成绩
   sum := 0
   for j := 0; j < len(arr[1].score); j++ {
     sum += arr[i].score[j]
   }
   fmt.Printf("第%d名学生\t总成绩为:%d\t平均成绩:%d\n", i+1, sum, sum/len(arr[i].score))

  }
}

在这里插入图片描述

6.6、指针

* :取值运算符(也可以用于定义指针类型)

& :取地址运算符(与变量一起用时)

● 对变量进行取地址( &)操作,可以获得这个变量的指针变量。

● 指针变量的值是指针地址。

● 对指针变量进行取值 (+ )操作,可以获得指针变星指向的原变量的值。

6.6.1、定义

指针类型就是在基本类型前加*表示

var p *int//声明了一个指针默认值为nil (空指针 值为0)指向了内存地址编号为0的空间
//*p =100 0- 255为系统占用不允许用户进行读写操作 空指针是不能直接赋值的
var a int
p =&a
*p =555 // 可以通过指针间接修改变星对应的内存空间
fmt.Println(*p)

● 空指针:指向了内存地址编号为0的空间

● 野指针:指针变量只想了一个未知的空间

● 注意:Go语言访问野指针空指针对应的内存空间都会报错

6.6.2、创建内存空间

格式 :

指针变量 = new(数据类型)

var p *int
p = new (int)//与定义的类型必须一至
*p =555 // 可以通过指针间接修改变星对应的内存空
fmt.Println(*p)

注意:

在Go语言中只需要开辟空间,不需要管理空间的释放。

6.6.3、make和new的区别

● make和new都是用来申请内存的。

● new很少用,一般用来给基本数据类型申请内存,返回的是对应类型的指针。

● make区别于new,它只用于slice、map、chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。

● make函数是无可替代的,在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值