go学习记录

楼主是于3月开始转go语言,希望能在这里记录自己到秋招的学习过程,以此来督促自己每天能够在实习工作结束后进行相关的学习以及复习工作,为秋招乃至后面的职业生涯技术提升做准备。

一、接口interface()

1.什么是接口?

简单来说,interface()是一组method的组合,通过interface来定义对象的一组行为

2.interface类型

interface 类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实 现了此接口。详细的语法参考下面这个例子

type Human struct {
	name string 
	age int 
	phone string
}
type Student struct { 
	Human //匿名字段 Human 
	school string
	loan float32
}
type Employee struct { Human 
	//匿名字段 
	Human 
	company string
	money float32
}
//Human 对象实现 Sayhi 方法 
func (h *Human) SayHi() {
	fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) 
}
// Human 对象实现 Sing 方法
func (h *Human) Sing(lyrics string) {
	fmt.Println("La la, la la la, la la la la la...", lyrics) 
}
//Human 对象实现 Guzzle 方法
func (h *Human) Guzzle(beerStein string) {
	fmt.Println("Guzzle Guzzle Guzzle...", beerStein) 
}
// Employee 重载 Human 的 Sayhi 方法 
func (e *Employee) SayHi() {
	fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) 
	//Yes you can split into 2 lines here.
}
//Student 实现 BorrowMoney 方法
func (s *Student) BorrowMoney(amount float32) {
	s.loan += amount // (again and again and...)
}
//Employee 实现 SpendSalary 方法
func (e *Employee) SpendSalary(amount float32) {
	e.money -= amount 
	// More vodka please!!! Get me through the day! 
}
// 定义 interface
type Men interface {
  SayHi()
  Sing(lyrics string)
  Guzzle(beerStein string) 
 }
type YoungChap interface { 
  SayHi()
  Sing(song string)
  BorrowMoney(amount float32) 
 }
type ElderlyGent interface { 
  SayHi()
  Sing(song string) 
  SpendSalary(amount float32)
}
  1. interface可以被任意的对象实现。

  2. 一个对象可以实现任意多个interface

  3. 任意的类型都实现的空interface(即interface{},包含了0个method的interface)

3.interface值

1.interface中到底能存什么值?

假如我们定义了一个interface的变量,那么这个变量可以存实现这个interface的任意类型的对象

例如上面例子中,我们定义了一个 Men interface 类型的变量 m,那么 m 里面可以存 Human、Student 或者 Employee 值。

因为 m 能够持有这三种类型的对象,所以我们可以定义一个包含 Men 类型元素的 slice

这个 slice 可以被赋予实现了 Men 接口的任意结构的对象,这个和我们传统意义上面的 slice 有所不同。

type Human struct { 
  name string
  age int
  phone string
}
type Student struct { 
  Human //匿名字段 
  school string
  loan float32
}
type Employee struct { 
  Human //匿名字段 
  company string 
  money float32
}
//Human 实现 Sayhi 方法 
func (h Human) SayHi() {
	fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) 
}
//Human 实现 Sing 方法
func (h Human) Sing(lyrics string) {
	fmt.Println("La la la la...", lyrics) 
}
//Employee 重载 Human 的 SayHi 方法 
func (e Employee) SayHi() {
  fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) 
//Yes you can split into 2 lines here.
}
// Interface Men 被 Human,Student 和 Employee 实现 // 因为这三个类型都实现了这两个方法
type Men interface {
	SayHi() 
	Sing(lyrics string)
}

func main() {
  mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
  paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
  sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000} 	Tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000}
  //定义 Men 类型的变量 i 
  var i Men
  //i 能存储 Student
  i = mike
  fmt.Println("This is Mike, a Student:") 
  i.SayHi()
  i.Sing("November rain")
  
  //i 也能存储 Employee
  i = Tom
  fmt.Println("This is Tom, an Employee:") 
  i.SayHi()
  i.Sing("Born to be wild")
  
  //定义了 slice Men
  fmt.Println("Let's use a slice of Men and see what happens")
  x := make([]Men, 3)
  
  //T 这三个都是不同类型的元素,但是他们实现了 interface 同一个接口 x[0], x[1], 		x[2] = paul, sam, mike
  
  for _, value := range x{ 
  	value.SayHi()
  }
  
}

Interface 就是一组抽象方法的集合。 ---其必须由其他非interface类型实现 ,而不能自我实现

4.空 interface

空 interface(interface{})不包含任何的 method,正因为如此,所有的类型都实现了空 interface。

空 interface 对于描述起不到任何的作用(因为它不包含任何的 method),

但是空interface 在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数 值。它有点类似于 C 语言的 void*类型。

// 定义 a 为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a 可以存储任意类型的数值 a=i
a=s

一个函数把 interface{}作为参数,那么他可以接受任意类型的值作为参数

如果一个函数 返回 interface{},那么也就可以返回任意类型的值。

5.interface函数参数

interface的变量,可以持有任意实现该interface类型的对象

---我们可以通过定义interface参数,让函数接受各种类型的参数。

6.interface变量存储的类型

我们知道interface的变量里面可以存储任意类型的数值(该类型实现了interface)

6.1那么如何反向知道这个变量里实际保存了是哪个类型的对象? Value,ok = element.(T)

1.Comma-ok断言

Go 语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),

这里 value 就是变量的值,ok 是一个 bool 类型,element 是 interface 变量,T 是断言的类 型。

type Element interface{}
type List [] Element

type Person struct { 
  name string
  age int
}
//定义了 String 方法,实现了 fmt.Stringer 
func (p Person) String() string {
	return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
func main() {
  list := make(List, 3)
  list[0] = 1 // an int
  list[1] = "Hello" // a string list[2] = Person{"Dennis", 70}
  for index, element := range list {
  	if value, ok := element.(int); ok {
  		fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
  	} else if value, ok := element.(string); ok { 
  		fmt.Printf("list[%d] is a string and its value is %s\n", index,
  value)
		} else if value, ok := element.(Person); ok {
			fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
		} else {
			fmt.Println("list[%d] is of a different type", index)
} }
}

我们断言的类型越多,那么 ifelse 也就越多,所以才引出了下面要介绍 的 switch。

2.switch 测试

type Element interface{} 
type List [] Element
type Person struct { 
  name string
  age int
}
//打印
func (p Person) String() string {
	return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" 
}
func main() {
	list := make(List, 3)
  list[0] = 1 //an int
  list[1] = "Hello" //a string list[2] = Person{"Dennis", 70}
  for index, element := range list{ 
    switch value := element.(type) {
    case int:
    fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
    case string:
    fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
    case Person:
    fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
    default:
    fmt.Println("list[%d] is of a different type", index)
} }
}

7.嵌入interface

如果一个interface1 作为 interface2 的一个嵌入字段,那么interface2隐式的包含了interface1里面的method

比如:io 包下面的 io.ReadWriter ,他包含了 io 包下面的 Reader 和 Writer 两个 interface。

// io.ReadWriter
type ReadWriter interface { 
	Reader
	Writer
}

8-- 接口型函数

8.1接口型函数的实现:

//A Getter loads data for a key.
type Getter interface {
		Get(key string) ([]byte,error)
}

//A GetterFunc implements Getter with a function
type GetterFunc func(key string) {[]byte ,error}

//Get implements Getter interface funtion
func (f GetterFunc) Get(key string) ([]byte,error) {
	return f(key)
}

8.2逻辑梳理

首先是定义了一个Getter接口,其中,包含了一个Get方法,参数为key string,

返回值为 []byte ,error

接着是定义了一个函数类型GetterFunc

该函数类型的参数和返回值与 Getter 中的 Get方法是一致的。

同时GetterFunc还定义了Get方法,

并且在Get方法中调用了自己(return f(key))

从而实现了Getter接口

因此 GetterFunc 是 一个实现了接口的函数类型,称为接口型函数。

接口型函数只能用于 接口内部 只定义了一个方法的情况

既然只有一个方法,那为什么还要多此一举,封装为一个接口呢?

定义参数的时候,直接用GetterFunc这个函数类型,然后让用户直接传入一个函数作为参数;

8.3作用

定义一个函数GetFromSource,其作用是从某数据源获取结果,

其接口类型 Getter 是其中一个参数

type Getter interface{
		Get(key string) ([]byte,error)
}

//A GetterFunc implements Getter with a function
type GetterFunc func(key string) {[]byte ,error}

//Get implements Getter interface funtion
func (f GetterFunc) Get(key string) ([]byte,error) {
	return f(key)
}

func GetFromSource(getter Getter,key string) []byte {
	buf,err := getter.Get(key)
	if err == nil {
			return buf
	}
	return nil
}

假设我现在需要调用GetFromSource 来从某数据源获取结果

有以下几种方式:

1.GetterFunc 类型的函数作为参数


GetFromSource(GetterFunc(func(key string) ([]byte,error) {
	return []byte(key),nil
}),"hello")

func main() {
		GetFromSource(GetterFunc(func(key string) ([]byte,error) {
			return []byte(key),nil
	}),"hello")
}

GetterFunc实现了Getter接口,故而能作为GetFromSource的参数

2.实现了Getter接口的结构体作为参数

type DB struct{
	url string
}
func (db *DB) Query(sql string,arg ...string) string {
		// ...处理逻辑
		return "hello"
}

func (db *DB) Get(key string) ([]byte,error) {
		// ...处理逻辑
		v := db.Query("select name from table where name = ? ",key)
		return []byte(v),nil
}

func main() {
		GetFromSource(new(DB),"hello")
}

首先结构体DB定义了方法 Query(sql string, arg ...string) string{}

其次结构体DB也实现了接口Getter ,并在其中调用了Query方法,

最后,因为DB实现了接口Getter,因此在调用GetFromSource()时,DB也是一个合法参数。

这种方式适用于逻辑较为复杂的场景,如果对数据库的操作需要很多信息,地址、用户名、密码,还有很多中间状态需要保持,比如超时、重连、加锁等等。这种情况下,更适合封装为一个结构体作为参数。

3.总结

这样,既能够将普通的函数类型(需类型转换)作为参数,也可以将结构体作为参数,使用更为灵活,可读性也更好,这就是接口型函数的价值。

8.4使用场景

net/httpHandlerHandlerFunc 就是一个典型。

Handler的定义:
type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
​
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  f(w, r)
}

1.Handle --第二个参数类型是接口类型Handler

我们可以 http.Handle 来映射请求路径和处理函数。

Handel的定义
func Handle(pattern string,handler Handler)

第二个参数 是 接口类型Handler。

func home(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_,_ = w.Write([]byte("hello,index page"))
}

func main() {
		//HandlerFunc实现了Handler接口,故而可以作为参数
		http.Handle("/home",http.HandlerFunc(home))
		_ = http.ListenAndServe("localhost:8000",nil)
}

2.HandleFunc --

通常我们还会使用另外一个函数 http.HandleFunc


func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

其中第二个参数handler fun(ResponseWriter, *Request)是一个普通的函数类型(其参数类型与Hanlder一致), 可以直接将home 传递给 HandleFunc:

func main() {
	http.HandleFunc("/home", home)
	_ = http.ListenAndServe("localhost:8000", nil)
}

由HandleFunc的内部实现得知,这两种写法是完全等价的,内部将第二种写法转换为了第一种写法

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}

3.总结

如果你仔细观察,会发现 http.ListenAndServe 的第二个参数也是接口类型 Handler,我们使用了标准库 net/http 内置的路由,因此呢,传入的值是 nil。

那如果这个地方我们传入的是一个实现了 Handler 接口的结构体呢就可以完全托管所有的 HTTP 请求,后续怎么路由,怎么处理,请求前后增加什么功能,都可以自定义了。

golang也是为了向接口参数里传函数,所以取了个巧,让函数继承了接口,从而接口型函数可以接受(转换自)匿名函数,从而名正言顺地往接口里传函数,否则其实是类型不匹配的。

二、错误处理

1.错误处理

Go定义了一个叫做error的类型,来显式表达错误。在使 用时,通过把返回的 error 变量与 nil 的比较,来判定操作是否成功。例如 os.Open 函数在打 开文件失败时将返回一个不为 nil 的 error 变量

func Open(name string) (file *File, err error)

类似于 函数,标准包中所有可能出错的 API 都会返回一个 error 变量,以方便错误 处理,这个小节将详细地介绍 error 类型的设计,和讨论开发 Web 应用中如何更好地处理 error。

2.Error类型

error 类型是一个接口类型,这是它的定义:

type error interface {
	Error() string
}

error 是一个内置的接口类型,我们可以在/builtin/包下面找到相应的定义。而我们在很多内 部包里面用到的 error 是 errors 包下面的实现的私有结构 errorString

type errorString struct { 
 	s string
}
func (e *errorString) Error() string { 
	return e.s
}

五、切片(slice)性能以及陷阱

3.1数组

Go 的切片(slice)是在数组(array)之上的抽象数据类型,数组类型定义了长度和元素类型。

例如, [3]int 类型表示由 3 个 int 整型组成的数组,数组以索引方式访问,例如表达式 s[n] 访问数组的第 n 个元素。

数组的长度是固定的,长度是数组类型的一部分。

长度不同的 2 个数组是不可以相互赋值的,因为这 2 个数组属于不同的类型。例如下面的代码是不合法的:

a := [3]int{1, 2, 3}
b := [4]int{2, 4, 5, 6}
a = b // cannot use b (type [4]int) as type [3]int in assignment

在 C 语言中,数组变量是指向第一个元素的指针,但是 Go 语言中并不是。

Go 语言中,数组变量属于值类型(value type),因此当一个数组变量被赋值或者传递时,实际上会复制整个数组。

例如,将 a 赋值给 b,修改 a 中的元素并不会改变 b 中的元素:

a := [...]int{1, 2, 3} // ... 会自动计算数组长度
b := a
a[0] = 100
fmt.Println(a, b) // [100 2 3] [1 2 3]

为了避免复制数组,一般会传递指向数组的指针。例如:

func square(arr *[3]int) {
	for i, num := range *arr {
		(*arr)[i] = num * num
	}
}

func TestArrayPointer(t *testing.T) {
	a := [...]int{1, 2, 3}
	square(&a)
	fmt.Println(a) // [1 4 9]
	if a[1] != 4 && a[2] != 9 {
		t.Fatal("failed")
	}
}

总结:go语言当中的值传递和地址传递(引用传递)如何运用?有什么区别?

  1. 值传递只会把参数的值复制一份放进对应的函数,两个变量的地址不同,不可相互修改。

  2. 地址传递(引用传递)会讲变量本身传入对应的函数,在函数中,可以对该变量进行值内容的修改。

3.2切片(slice)

数组固定长度,缺少灵活性

切片使用字面量初始化时和数组很像,但是不需要指定长度:

languages := []string{"GO","Python","c"}

或者使用 内置函数make进行初始化

func make([]T,len,cap) []T

第一个参数是 []T 即元素类型, 第二个参数len是长度 即初始化的切片拥有多少个元素

第三个参数是 容量 cap,容量是可选参数,默认等于长度。

使用内置函数len 和 cap 可以得到切片的长度和容量

1.容量(如何实现切片扩容?)

容量是当前切片已经预分配的内存 能够容纳的元素个数,

如果往切片中不断的增加新元素,超过了当前切片的容量,就需要分配新的内存并将当前切片所有元素拷贝到新的内存块上

因此为了减少内存的拷贝次数,容量在比较小的时候,一般是以 2 的倍数扩大的,例如 2 4 8 16 …,当达到 2048 时,会采取新的策略(每次都是扩容了四分之一左右),避免申请内存过大,导致浪费。

总结:

  1. 首先判断,如果新申请容量大于两倍的旧容量,最终容量就是新申请的容量

  2. 若不是,则进行判断旧切片的长度是否小于1024,若小于,则最终容量就是旧容量的两倍。

  3. 若旧切片长度大于等于1024,则最终容量从旧容量开始循环增加原来的1/4,直到最终容量大于新申请的容量。

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

2.slice的底层实现

切片是基于数组实现的,它的底层是数组,它本身很小,可以理解为对底层数组的抽象。

因为是基于数组实现的,因此其底层的内存是连续分配的,效率较高,可以 通过索引获得数据。

---截止 5.23

3.操作

1.按照下标进行索引 --- 切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量。

struct {
    ptr *[]T
    len int
    cap int
}

2.切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组,因此切片操作十分高效。

 

nums := make([]int, 0, 8)
nums = append(nums, 1, 2, 3, 4, 5)
nums2 := nums[2:4]
printLenCap(nums)  // len: 5, cap: 8 [1 2 3 4 5]
printLenCap(nums2) // len: 2, cap: 6 [3 4]

nums2 = append(nums2, 50, 60)
printLenCap(nums)  // len: 5, cap: 8 [1 2 3 4 50]
printLenCap(nums2) // len: 4, cap: 6 [3 4 50 60]
  • nums2 执行了一个切片操作 [2, 4)此时 nums 和 nums2 指向的是同一个数组。

  • nums2 增加 2 个元素 50 和 60 后,将底层数组下标 [4] 的值改为了 50,下标[5] 的值置为 60。

  • 因为 nums 和 nums2 指向的是同一个数组,因此 nums 被修改为 [1, 2, 3, 4, 50]

1.copy

 2.append

切片有三个属性,指针(ptr)、长度(len) 和容量(cap)。append 时有两种场景:

  • 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间。

  • 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组

 3.delete

 切片的底层是数组,因此删除意味着后面的元素需要逐个向前移位。每次删除的复杂度为 O(N),因此切片不合适大量随机删除的场景,这种场景下适合使用链表。

4.insert

 insert 和 append 类似。即在某个位置添加一个元素后,将该位置后面的元素再 append 回去。复杂度为 O(N)。因此,不适合大量随机插入的场景。

3.3new的作用是什么?

new 创建一个该类型的实例,并且返回指向该实例的指针。new 函数是内建函数,函数定义:

func new(Type) *Type
  1. 使用new函数来分配空间

  2. 传递给new函数的是一个类型,而不是一个数值

  3. 返回值是指向这个新分配的地址的指针

3.4make的作用是什么?

make 的作用是为 slice, map or chan 的初始化 然后返回引用 make 函数是内 建函数,函数定义:

func make(Type, size IntegerType) Type

make(T, args)函数的目的和 new(T)不同 仅仅用于创建 slice, map, channel 而且返回类型是实例

3.5Map

1.Golang Map 底层实现

其底层实现是一个散列表,因此实现map的过程实际上就是实现散表的过程。

2.Map如何扩容

  1. 双倍扩容:扩容采取了一种称为“渐进式”的方式,原有的 key 并不会一 次性搬迁完毕,每次最多只会搬迁 2 个 bucket。

  2. 等量扩容:重新排列,极端情况下,重新排列也解决不了,map存储就会蜕 变成链表,性能大大降低,此时哈希因子 hash0 的设置,可以降低此类极 端场景的发生。

3.Map查找

Go 语言中 map 采用的是哈希查找表,由一个 key 通过哈希函数得到哈希值,64 位系统中就生成一个 64bit 的哈希值,由这个哈希值将 key 对应存到不同的桶 (bucket)中,当有多个哈希映射到相同的的桶中时,使用链表解决哈希冲 突。

六、struct

1.struct的声明方式

1.直接赋值

type person struct {
	name string
	age int
}
var p person //p 是 person类型的变量
p.name = "zhangkai"
p.age = 21

2.按照顺序提供初始化值

P := person{"Tom","25"}

3.通过field:value方式来初始化--可以任意顺序

P := person{age:24,name:"mayirui"}

2.匿名字段

上面我们介绍了如何定义struct ----字段名与其类型一一对应。

实际上Go支持只提供类型,而不提供写字段名的方式,即匿名字段 ---也称为嵌入字段

当匿名字段是一个struct,那么这个struct所拥有的全部字段都被隐式的引入了当前定义的这个stuct

如:

type Human struct {
	name string
	age int 
	weight int
}
type Student struct {
	Human //匿名字段,那么默认Student就包含了Human的所有字段
	speciality string
}
type Skills []string

 

当我们看到Student访问属性age和name的时候,就像访问自己所有有用的字段一样,

匿名字段 能够实现字段的继承

同时student还能访问Human这个字段作为字段名

mark.Human = Human{"zhangkai",66,220}
mark.Huamn.age-=1

通过匿名访问和修改字段相当的有用,

但是不仅仅是struct字段,所有的内置类型和自定义类型都是可以作为匿名字段的,且可以在响应的字段上进行函数操作(如数组的append)

type Skills []string
​
type Human struct {
  name string
  age int
  weight int
}
type Student struct {
  Human //匿名字段
  Skills //匿名字段,自定义的类型string slice
  int //内置类型作为匿名字段
  speciality string
}
​
​
func main() {
  //初始化学生Jane
  jane := Student{Human:Human{"Jane",35,100},speciality:"Biology"}
  //访问其对应的字段
  fmt.Println("Her name is ",jane.name)
  fmt.Println("her age is",jane.age)
  fmt.Println("Her weight is",jane.weight)
  //修改起skill技能字段
  jane.Skills = []string{"anatomy"}
  jane.Skills = append(jane.Skills,"physics","golang")
  //修改匿名内置类型字段
  jane.int = 3
  
  
}

假如此时 human里面有一个字段phone ,而student中也有一个字段phone,该如何?

GO通过 最外层的优先访问来解决这个问题:

当通过student.phone访问时,是访问student里面的字段,而不是human里面的字段。

这样就允许我们去重载通过匿名字段继承的一些字段

当然,如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段来访问

type Human struct {
  name string
  age int
  phone string //Human类型拥有的字段
}
​
type Employee struct {
  Human //匿名字段Human
  speciality string
  phone string //雇员的phone字段
}
func main(){
  Bob := Employee{Human{"Bob",34,"777-444-XXXX"},"Designer","333-222"}
  fmt.Println("Bob's work phone is",Bob.phone)
  //如果要访问Human的phone字段
  fmt.Println("Bob's personal phone is:",Bob.Human.phone)
}

----截止到5.24

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值