Go interface详解

什么是interface?

  Go 语言是一门强大而简洁的编程语言,其接口(interface)机制是其特色之一。接口在 Go 中被广泛应用,不仅可以用于实现多态,还能帮助开发者编写更加灵活和可复用的代码。在本文中,我们将深入探讨 Go 语言接口的各个方面,包括定义、实现和使用。

interface的使用

 type myInterface interface{
     方法名1(参数列表1) 返回值列表1
     方法名2() 返回值列表2
     }
 // 嵌套接口
 type 接口名 interface{
     myInterface 
     方法名1(参数列表1) 返回值列表1
 }

  下面我将详细的讲解interface的使用,例如我现在定义一个接口的名字叫 studyInterface, 方法名为 Name返回参数为string

 type studyInterfaceinterface interface{
     Name() string
     }

  我们可以定义一个 student 的结构体,注意如果student小写就只能同个包的文件读取,相当于java中的private,如果是大写相当于public,可以在包的上一级读取到这个结构体。下面我们来实现studyInterfaceinterface的接口:

 type Student struct {
 }
 
func (s Student) Name() string {
	return "my name is zhangsan"
}

  在上面的代码中,我们定义了Student结构体,实现了studyInterfaceinterface 接口中的 Name方法,要实现接口,必须把结构体中的方法全部实现。现在我们创建一个名为student的接口,并且将Student的值赋值给student接口

var student studyInterfaceinterface

student = Student{}
fmt.Println(student.Name())

  打印的结果就是"my name is zhangsan",在上面的代码中,将一个 Student 类型的值赋给变量 student。尽管 Student 类型和 studyInterface 接口是两种不同的类型,但由于 Student 类型实现了 studyInterface 接口中定义的所有方法,因此可以将 Student 类型的值赋给 studyInterface 类型的变量。

interface 的使用技巧

在Go语言中,接口(interface{})是一项非常强大的类型,。以下是一些使用接口的技巧

1. 使用空接口的技巧

1.1. 通用函数
使用空接口作为函数参数,可以创建通用的函数,能够接受任意类型的参数

func printValue(val interface{}) {
    fmt.Println("Value:", val)
}

func main() {
    printValue(42)
    printValue("Hello, Go!")
    printValue(3.14)
}

1.2. 存储不同类型的数据
通过使用空接口,可以创建能够存储不同类型数据的切片、数组或映射。

Copy code
var data []interface{}

data = append(data, 42, "Hello, Go!", 3.14)

for _, val := range data {
    fmt.Println("Value:", val)
}

1.3. 实现泛型容器
使用空接口可以创建类似于泛型容器的结构,使容器能够存储任意类型的元素。

Copy code
type GenericContainer struct {
    data []interface{}
}

func (gc *GenericContainer) Add(value interface{}) {
    gc.data = append(gc.data, value)
}

func main() {
    container := GenericContainer{}
    container.Add(42)
    container.Add("Hello, Go!")
    container.Add(3.14)
}

使用空接口带来了很大的灵活性,但也需要小心使用。在运行时,可能需要进行类型断言,因此在使用时应注意进行适当的错误处理

2 使用类型断言的技巧

类型断言: 用于在运行时检查接口值的底层类型并将其转换为具体的类型。以下是一些使用类型断言的技巧:

2.1. 类型断言的基本语法

使用类型断言来获取接口值的底层类型。这会判断interface的类型,如果是你预期的类型,那么他就会返回true,反之是false

var val interface{} = 42

if v, ok := val.(int); ok {
    fmt.Println("Value is an integer:", v)
} else {
    fmt.Println("Value is not an integer")
}
// 借用上面的例子
var student studyInterfaceinterface

student = Student{}
name ,ok :=  student.(Student)
if ok{
fmt.Println(student.Name())
}
2.2. 多重类型断言

通过使用switch语句进行多重类型断言,处理多种可能的类型。注意这样使用将会降低性能,时间复杂度为O(n)

func processValue(val interface{}) {
    switch v := val.(type) {
    case int:
        fmt.Println("Processing an integer:", v)
    case string:
        fmt.Println("Processing a string:", v)
    default:
        fmt.Println("Unknown type")
    }
}

func main() {
    processValue(42)
    processValue("Hello, Go!")
    processValue(3.14)
}
2.3. 类型断言的安全使用

为了安全使用类型断言,可以使用comma, ok形式来获取断言的结果,避免在断言失败时导致程序崩溃。

var val interface{} = "Hello, Go!"

if str, ok := val.(string); ok {
    fmt.Println("Value is a string:", str)
} else {
    fmt.Println("Value is not a string")
}
2.4. 空接口的类型判断

通过类型断言检查空接口的类型。

var data interface{} = 42

if _, ok := data.(int); ok {
    fmt.Println("Data is of type int")
} else if _, ok := data.(string); ok {
    fmt.Println("Data is of type string")
} else {
    fmt.Println("Unknown type")
}

使用类型断言可以使代码更具灵活性,但需要注意在进行断言时进行适当的错误处理,以确保程序的稳定性。

3. 使用interface实现多态

多态是面向对象编程中的一个概念,它允许不同类型的对象对相同的接口进行调用,产生不同的行为。以下是在Go中使用接口实现多态的示例:

package main
import "fmt"
// 定义一个接口
type Shape interface {
    Area() float64
}
// 定义圆形结构体
type Circle struct {
    Radius float64
}
// 实现 Shape 接口的 Area 方法
func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

// 定义矩形结构体
type Rectangle struct {
    Width  float64
    Height float64
}
// 实现 Shape 接口的 Area 方法
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}
// 函数接受 Shape 接口作为参数
func PrintArea(s Shape) {
    fmt.Println("Area:", s.Area())
}
func main() {
    // 创建圆形和矩形对象
    circle := Circle{Radius: 5}
    rectangle := Rectangle{Width: 4, Height: 6}

    // 使用多态调用 PrintArea 函数
    PrintArea(circle)
    PrintArea(rectangle)
}

4. 使用接口组合的技巧

在 Go 中,接口组合是一种强大的技巧,允许将多个接口组合在一起,形成一个新的接口。这种方式提供了更灵活的设计和代码组织方式。以下是一些使用接口组合的技巧:

4.1. 接口组合基础

通过将多个接口组合成一个新接口,可以在新接口中包含所有原始接口的方法。

// 定义两个接口
type Reader interface {
    Read() string
}

type Writer interface {
    Write(data string)
}

// 将两个接口组合成一个新接口
type ReadWriter interface {
    Reader
    Writer
}
4.2. 实现接口组合

实现接口组合时,只需要实现组合中的所有接口的方法即可。

// 实现 Reader 接口
type MyReader struct{}

func (mr MyReader) Read() string {
    return "Reading..."
}

// 实现 Writer 接口
type MyWriter struct{}

func (mw MyWriter) Write(data string) {
    fmt.Println("Writing:", data)
}

// 实现 ReadWriter 接口
type MyReadWriter struct {
    Reader
    Writer
}
4.3. 使用接口组合

通过接口组合,可以在代码中更灵活地使用多个接口。

func processData(rw ReadWriter) {
    data := rw.Read()
    rw.Write(data)
}

func main() {
    myRW := MyReadWriter{}
    processData(myRW)
}

在上述例子中,MyReadWriter 结构体同时实现了 ReaderWriter 接口。通过接口组合,我们可以将 MyReadWriter 传递给 processData 函数,该函数接受一个 ReadWriter 接口类型的参数。

使用匿名接口嵌套

在 Golang 中,使用匿名接口嵌套是一种强大的组合接口的方式。以下是使用匿名接口嵌套的示例,以组合 io.Readerio.Writerio.Closer 接口为例:

package main

import (
	"fmt"
	"io"
)
// 定义一个接口组合
type ReadWriteCloser interface {
	io.Reader
	io.Writer
	io.Closer
}

// 实现接口组合的结构体
type MyFile struct {
	// 使用匿名接口嵌套
	ReadWriteCloser
	FileName string
}

// 实现 io.Closer 接口的 Close 方法
func (file *MyFile) Close() error {
	fmt.Println("Closing file:", file.FileName)
	return nil
}

// 实现 ReadWriteCloser 接口的结构体
type ReadWriteImpl struct{}

// 实现 Read 方法
func (rw *ReadWriteImpl) Read(p []byte) (n int, err error) {
	fmt.Println("Reading:", string(p))
	return len(p), nil
}

// 实现 Write 方法
func (rw *ReadWriteImpl) Write(p []byte) (n int, err error) {
	fmt.Println("Writing:", string(p))
	return len(p), nil
}

func main() {
	// 创建 MyFile 结构体实例
	file := MyFile{
		// 使用匿名接口嵌套,可以直接初始化这些接口
		ReadWriteCloser: &ReadWriteImpl{},
		FileName:        "example.txt",
	}

	// 使用组合接口的方法
	file.Read([]byte("Hello, "))
	file.Write([]byte("Golang!"))
	file.Close()
}

在这个示例中,ReadWriteCloser 是一个接口组合,它包含了 io.Readerio.Writerio.Closer 接口。然后,我们通过一个结构体 MyFile 来实现这个接口组合,同时还嵌套了一个匿名的实现了 ReadWriteCloser 接口的结构体 ReadWriteImpl。这样,MyFile 结构体既可以调用 io.Readerio.Writerio.Closer 接口的方法,也可以自定义其他方法,实现更丰富的功能。

这种方式使得我们可以更方便地组合和扩展接口,提高代码的灵活性。

Interface 使用场景

通过上面我们已经了解到了interface的使用技巧,下面我来讲解一下interface的使用场景

1. 依赖注入

依赖注入是一种设计模式,它可以减少类之间的耦合,提高代码的可维护性和可测试性。依赖注入的基本思想是,不让类自己创建它所依赖的对象,而是通过外部的方式(如构造函数、参数、属性等)将依赖的对象传递(或注入)给类。这样,类就不需要知道依赖对象的具体实现,只需要调用它们的接口方法。依赖注入有以下好处:

降低了类与类之间的耦合,使得代码更容易修改和扩展。
提高了代码的可重用性,因为依赖的对象可以在不同的类中复用。
增强了代码的可测试性,因为依赖的对象可以用模拟对象或桩对象替换,从而方便进行单元测试

1.1. 依赖注入基础

在 Golang 中,接口经常用于依赖注入,这是一种通过接口实现解耦的方法,使得代码更加灵活和可测试。以下是在依赖注入中常见的 Interface 使用场景。

package main

import "fmt"

// 定义一个接口
type Logger interface {
    Log(message string)
}

// 依赖注入的函数
func performLogging(log Logger, message string) {
    log.Log(message)
}

// 实现接口的结构体
type ConsoleLogger struct{}

// 实现 Log 方法
func (cl ConsoleLogger) Log(message string) {
    fmt.Println("Logging:", message)
}

func main() {
    // 创建一个 ConsoleLogger 实例
    consoleLogger := ConsoleLogger{}

    // 通过依赖注入调用 performLogging 函数
    performLogging(consoleLogger, "Hello, Dependency Injection!")
}
2. 更灵活的替换实现

通过依赖注入,可以轻松替换接口的实现,从而实现更灵活的代码结构。这对于单元测试、模拟和切换不同的实现非常有用。

// 另一个实现接口的结构体
type FileLogger struct {
    FilePath string
}

// 实现 Log 方法
func (fl FileLogger) Log(message string) {
    // 实现日志写入文件的逻辑
    fmt.Println("Logging to file:", fl.FilePath, "Message:", message)
}

func main() {
    // 创建一个 FileLogger 实例
    fileLogger := FileLogger{FilePath: "/path/to/logfile.txt"}

    // 通过依赖注入调用 performLogging 函数
    performLogging(fileLogger, "Hello, Dependency Injection!")
}

通过这些方式,我们可以在不修改调用代码的情况下,更改或替换接口的实现,从而实现更好的代码可维护性和可测试性。

2 测试驱动开发(TDD)中的 Interface 使用场景

在测试驱动开发(TDD)中,接口在很多情况下扮演着关键的角色,帮助开发者编写可测试、可维护的代码。以下是测试驱动开发中常见的 Interface 使用场景:

1. 定义接口来抽象行为

在 TDD 中,首先会定义接口来抽象待实现的行为。接口定义了期望的功能,但并不提供具体的实现。

// 定义一个计算器接口
type Calculator interface {
    Add(a, b int) int
    Subtract(a, b int) int
}
2. 编写测试用例

在定义接口后,首先编写针对接口的测试用例。这些测试用例会检查接口方法的预期行为。

func TestCalculator_Add(t *testing.T) {
    // 编写测试代码,测试 Add 方法
}

func TestCalculator_Subtract(t *testing.T) {
    // 编写测试代码,测试 Subtract 方法
}
3. 编写接口的实现

根据测试用例,编写接口的实现。确保实现的方法满足接口定义,并通过测试用例。

// 实现 Calculator 接口
type BasicCalculator struct{}

func (bc BasicCalculator) Add(a, b int) int {
    return a + b
}

func (bc BasicCalculator) Subtract(a, b int) int {
    return a - b
}
4. 扩展接口和实现

在开发过程中,如果需要添加新的功能,可以通过扩展接口和实现来满足新的需求。

// 扩展 Calculator 接口
type AdvancedCalculator interface {
    Calculator
    Multiply(a, b int) int
}

// 实现 AdvancedCalculator 接口
type ScientificCalculator struct{}

func (sc ScientificCalculator) Add(a, b int) int {
    return a + b
}

func (sc ScientificCalculator) Subtract(a, b int) int {
    return a - b
}

func (sc ScientificCalculator) Multiply(a, b int) int {
    return a * b
}
5. 保持接口的稳定性

TDD 的一个目标是确保代码的稳定性。通过定义和使用接口,可以使得代码更加灵活、可测试,并且在修改实现时不影响接口的使用者。

通过这种方式,测试驱动开发可以帮助确保代码的正确性,同时提供了一种清晰的结构,使得代码更容易维护和扩展。

3. 接口在框架中的使用

当涉及框架设计时,具体的实现和结构会根据应用程序的需求而变化,以下是一个更具体的示例,展示如何设计一个简单的 Web 框架,其中包含路由处理和中间件。

package main

import (
	"fmt"
	"net/http"
)

// Handler 接口定义了处理 HTTP 请求的方法
type Handler interface {
	Handle(w http.ResponseWriter, r *http.Request)
}

// Middleware 接口定义了中间件的方法
type Middleware interface {
	Apply(next Handler) Handler
}

// Router 结构体实现了 Handler 接口,用于处理路由
type Router struct {
	routes map[string]Handler
}

// NewRouter 函数用于创建一个新的 Router 实例
func NewRouter() *Router {
	return &Router{
		routes: make(map[string]Handler),
	}
}

// Handle 方法用于注册路由
func (r *Router) Handle(path string, handler Handler) {
	r.routes[path] = handler
}

// Handle 方法实现了 Handler 接口,用于处理 HTTP 请求
func (r *Router) Handle(w http.ResponseWriter, req *http.Request) {
	path := req.URL.Path
	if handler, ok := r.routes[path]; ok {
		handler.Handle(w, req)
	} else {
		http.NotFound(w, req)
	}
}

// LoggingMiddleware 结构体实现了 Middleware 接口,用于添加日志记录功能
type LoggingMiddleware struct{}

// Apply 方法实现了 Middleware 接口,用于将日志记录功能应用到下一个处理程序
func (lm LoggingMiddleware) Apply(next Handler) Handler {
	return loggingHandler{
		next: next,
	}
}

// loggingHandler 结构体实现了 Handler 接口,用于添加日志记录功能
type loggingHandler struct {
	next Handler
}

// Handle 方法实现了 Handler 接口,用于处理 HTTP 请求,并添加了日志记录
func (lh loggingHandler) Handle(w http.ResponseWriter, req *http.Request) {
	fmt.Println("Log: Handling request")
	lh.next.Handle(w, req)
}

func main() {
	// 创建一个新的 Router 实例
	router := NewRouter()

	// 创建一个处理器并注册到路由
	helloHandler := HelloHandler{}
	router.Handle("/hello", helloHandler)

	// 应用 LoggingMiddleware 中间件到路由
	router.Handle = LoggingMiddleware{}.Apply(router.Handle)

	// 启动服务器
	http.ListenAndServe(":8080", router)
}

// HelloHandler 结构体实现了 Handler 接口,用于处理 "/hello" 路由
type HelloHandler struct{}

// Handle 方法实现了 Handler 接口,用于处理 HTTP 请求
func (hh HelloHandler) Handle(w http.ResponseWriter, req *http.Request) {
	fmt.Fprintln(w, "Hello, World!")
}

在这个示例中,我们定义了一个简单的 Web 框架,包含了 Handler 接口、Middleware 接口、Router 结构体,以及实现了这些接口的结构体和函数。Router 负责管理路由,LoggingMiddleware 负责添加日志记录功能。最后,我们创建了一个 HelloHandler 处理器,注册到路由中,并应用了日志记录中间件。这个示例展示了如何通过接口和结构体设计一个简单而灵活的框架,其中可以轻松添加新的处理器和中间件。

interface 原理剖析

  interface底层实现分两种:iface和eface,都用struct来标识。 eface表示不含方法的interface结构,即empty interface. iface表示non-empty inteface

  GO interface 是一种抽象的类型,它定义了一组方法的集合,但不指定具体的实现。任何类型,只要实现了 interface 的所有方法,就可以赋值给 interface 类型的变量。

  GO interface 的底层原理是通过两个指针来实现的:

一个指向 interface 的类型信息,一个指向 interface 的具体值。interface 的类型信息包含了 interface 的方法集合和具体值的类型信息。interface 的具体值可以是任意类型的值,包括指针。interface 的转换和断言都是通过比较类型信息和方法集合来实现的,这些操作会有一定的性能损耗

+-------------------+
| Type Information  |   --> 指向具体类型的信息
+-------------------+
|       Value       |   --> 存储具体类型的值
+-------------------+

iface 和 eface 的区别

我们先看看他们两者结构体的区别

// iface 表示有方法的接口类型
type iface struct {
    tab  *itab          // 指向接口的类型信息和方法集合
    data unsafe.Pointer // 指向接口的具体值
}

// eface 表示没有方法的空接口类型
type eface struct {
    _type *_type        // 指向接口的类型信息
    data  unsafe.Pointer // 指向接口的具体值
}

  iface 和 eface 的主要区别是,iface 有一个 tab 字段,而 eface 有一个 _type 字段。这两个字段都是指向接口的类型信息的指针,但是 tab 字段还包含了接口的方法集合,而 _type 字段只包含了接口的类型信息。这是因为,有方法的接口类型需要知道具体类型实现了哪些方法,以及如何调用这些方法,而没有方法的空接口类型只需要知道具体类型的信息就可以了

//unsafe.Pointer 的结构
type Pointer *ArbitraryType
type ArbitraryType int

  unsafe.Pointer类似C语言中的void类型指针,它可以包含任意类型的地址。和普通指针一样,unsafe.Pointer指针也是可以比较的,且支持和nil常量比较判断是否为空指针。

  虽然iface只有两个字段构成,但也容易猜想到任何用interface包装的类型,都会被取其地址。这么做其实编译器很容易让变量逃逸到堆。

1.data

  data 用来保存实际变量的地址。

  data 中的内容会根据实际情况变化,因为 golang 在函数传参和赋值时是 值传递 的,所以:

  1. 如果实际类型是一个值,那么 interface 会保存这个值的一份拷贝。interface 会在堆上为这个值分配一块内存,然后 data 指向它。

  2. 如果实际类型是一个指针,那么 interface 会保存这个指针的一份拷贝。由于 data 的长度恰好能保存这个指针的内容,所以 data 中存储的就是指针的值。它和实际数据指向的是同一个变量

-- ,将 A 的值赋值给 i1
则 i1 中的 data 中的内容是一块新内存的地址 (0x666666)

i1 = A                                             A的copy
+-------------------+   0x666666     +-------------------+
|      _type        |   -------->    |                   |
+-------------------+                |                   |
|     0x666666      |                |        A          |
+-------------------+                |                   |
                                     +-------------------+
                                     |    i1 Interface   |
                                     +-------------------+
                                                
-- i2 = &A,将 A 的地址赋值给 i2
+-------------------+        +-------------------+
|         _type     |   -->  |                   |
+-------------------+        |                   |
|       0xabcdef    |        |      0xabcdef     |
+-------------------+        |                   |
                            +-------------------+
                            |    i2 Interface   |
                            +-------------------+
2. _type

_type是runtime对Go任意类型的内部表示。 path:src/runtime/type.go

// Needs to be in sync with ../cmd/link/internal/ld/decodesym.go:/^func.commonsize,
// ../cmd/compile/internal/gc/reflect.go:/^func.dcommontype and
// ../reflect/type.go:/^type.rtype.
type _type struct {
    size       uintptr // type size
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32  // hash of type; avoids computation in hash tables
    tflag      tflag   // extra type information flags
    align      uint8   // alignment of variable with this type
    fieldalign uint8   // alignment of struct field with this type
    kind       uint8   // enumeration for C
    alg        *typeAlg  // algorithm table
    gcdata    *byte    // garbage collection data
    str       nameOff  // string form
    ptrToThis typeOff  // type for pointer to this type, may be zero
}

  源码中的注释已经标识的比较清楚了。_type描述了类型的各个属性信息:大小、名称、类型地址…,某种程度上一些类型的行为也包含在内了,如哈希、比较等等。

size: 描述类型的大小
hash:数据的hash值
align:指对齐
fieldAlgin:是这个数据嵌入结构体时的对齐
kind:是一个枚举值,每种类型对应了一个编号
alg:是一个函数指针的数组,存储了hash/equal这两个函数操作。
gcdata:存储了垃圾回收的GC类型的数据,精确的垃圾回收中,就是依赖于这里的gcdata
nameOff和typeOff为int32,表示类型名称和类型的指针偏移量,这两个值会在运行期间由链接器加载到runtime.moduledata结构体中,通过以下两个函数可以获取偏移量

3. itab

itab 表示 interface 和 实际类型的转换信息。对于每个 interface 和实际类型,只要在代码中存在引用关系, go 就会在运行时为这一对具体的 <Interface, Type> 生成 itab 信息。

inter 指向对应的 interface 的类型信息。
type 和 eface 中的一样,指向的是实际类型的描述信息 _type
fun 为函数列表,表示对于该特定的实际类型而言,interface 中所有函数的地址。

itab 中函数表(fun) 的生成

  当编译器生成接口表(itab)时,特别是其中的函数表(fun)时,涉及到了函数匹配的过程。如果一个接口有 ni 个方法,而一个结构体有 nt 个方法,传统的匹配过程的时间复杂度为 O(ni*nt)。这是因为需要遍历接口的所有方法,对于每个方法,都需要从结构体的方法列表中找到匹配的函数。

  然而,为了提高性能,编译器对这个过程进行了优化。具体而言,它对接口类型中的方法列表和非普通类型中的方法列表进行了排序,从而实现了一个更高效的算法,其时间复杂度为 O(ni+nt)。

  以下是这个优化过程的核心算法的代码片段,摘自 $GOROOT/src/runtime/iface.go。经过适度修改,只保留了关键逻辑:

Copy code
var j = 0
for k := 0; k < ni; k++ {
    mi := inter.methods[k]
    for ; j < nt; j++ {
        mt := t.methods[j]
        if isOk(mi, mt) {
            itab.fun[k] = mt.f
        }
    }
}

  在这段代码中,对接口的方法(inter.methods)和结构体的方法(t.methods)进行了遍历。通过检查是否匹配(isOk(mi, mt)),如果匹配,就将结构体中的方法添加到接口表的函数表中(itab.fun[k] = mt.f)。这个算法的优势在于排序后的匹配过程更加迅速,使得生成函数表的过程更加高效。

参考链接:

  1. Golang 官方文档:

  2. Golang Interface 相关文章:

  3. Golang Web 框架设计:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Go 的学习之路

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

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

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

打赏作者

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

抵扣说明:

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

余额充值