【面试题】Golang (第一篇)

目录

1.Golang有哪些优势?

2.Golang数据类型有哪些?

3.Golang中的包如何使用?

4.Go支持什么形式的类型转换?

5.什么是Goroutine?你如何停止它?

协程/线程

如何创建goroutine?

如何停止goroutine?

1.使用channel

2.使用context

3. 使用Mutex和WaitGroup

4. 使用runtime包

6.Context

1.什么是context

2.context的接口方法

3.context树

3.1context.WithCancel 取消多个 goroutine

3.2 context.WithValue 传值

3.3 context.WithTimeout 超时取消

3.4Context 使用原则

3.5Context底层原理

7.如何在运行时检查变量类型?

8.Go两个接口之间可以存在什么关系?

9.Go的同步锁有什么特点?作用是什么?


1.Golang有哪些优势?

高效性:Go语言是一种编译型语言,能够生成高效的机器码。同时,Go语言的垃圾回收机制和协程支持使其在处理大规模并发任务时非常高效。

并发性:Go语言内置支持协程和通道,能够方便地编写并发程序。协程可以轻松实现高并发,通道可以方便地进行通信和同步,这使得Go语言在网络编程、分布式系统和大数据处理等领域具有优势。

简单性:Go语言语法简洁,容易学习和理解。Go语言没有继承和多态等复杂的语言特性,使得程序设计更加直观和简单。

可读性:Go语言具有良好的代码风格和格式,使得代码易于阅读和维护。Go语言的代码组织方式和注释规范使得代码的可读性和可维护性得到保证。

安全性:Go语言具有内置的安全特性,如内存安全、类型安全和并发安全等。Go语言的垃圾回收机制可以避免内存泄露,类型安全可以防止代码中出现类型错误,而并发安全可以避免数据竞争问题。

跨平台性:Go语言的编译器可以将源代码编译为本地机器码,使得程序可以在各种操作系统上运行。同时,Go语言标准库中提供了许多与平台无关的包,如网络、文件操作等,可以方便地编写跨平台的程序。

总之、Go语言具有高效性、并发性、简单性、可读性、安全性和跨平台性等优势,使得它在云计算、网络编程、分布式系统、大数据处理等领域得到了广泛应用。

2.Golang数据类型有哪些?

1、布尔型,值只可以是常量true或false;bool

2、数字类型,支持整型和浮点型数字,并且支持复数;int、int8、int16、int32、int64;uint、uint8、uint16、uint32、uint64;float32、float64;complex64、complex128

3、字符串类型,是一串固定长度的字符连接起来的字符序列;string

4、指针类型;var 变量名 *基础类型 如:var p *int

5、数组类型; var 数组名 [数组大小]数据类型 如:

var arr1 [5]int =[5]int{1,2,3,4,5}

var arr2 = [5]int{1,2,3,4,5}

var arr3 = [...]int{1,2,3,4,5}

var arr4=[...]int{2:66,0:22,3:88}

6、结构化类型; type 结构体名称 struct {} 如:

type Teacher struct{

Name string

Age int

}

var t1 Teacher = Teacher{"测试",20}

var t2 *Teacher = new(Teacher)

7、Channel类型;var 变量名 chan int 如:

var intchan chan int

intchan = make(chan int,3)

intchan<-10

8、函数类型;func 函数名(形参列表)(返回值类型列表){执行语句... return +返回值列表} 如:

func main() int{

...

return 0

}

9、切片类型;var 切片名 []类型 = 数组中的一个片段引用 如:

var intattr [6]int = [6]int{1,2,3,4,5,6}

slice:=intattr[1:3] //切片

slice1:=make([]int,4,20) //make函数的三个参数:1.切片类型 2.切片长度 3.切片容量

10、接口类型;type 接口名称 interface{ 方法 }如:

type Sayhello interface {

sayHello()

}

11、Map类型。var 变量名 map[keytype]valuetype 如:

var a map[int] string

a = make(map[int]string,10)

a[1000] = "测试1"

a[1001] ="测试2"

3.Golang中的包如何使用?

什么是Golang包?

包是一组相关函数、类型和变量的集合,在代码中起到一种组织结构的作用。与其他语言中的库或者模块类似,包可以实现代码的复用和模块化。所有Golang程序都要使用包,包的导入可以在源代码的头部进行声明

如何使用Golang包?

使用Golang包需要三个步骤:导入包、访问包中的函数或者变量以及调用这些函数或者使用这些变量。如:

package main
import (
       "fmt"
        "math/rand"
)
func main(){
    fmt.Println("My favoriate number is",rand.Intn(10))
}

这个例子中使用了fmt 和math/rand包。该程序打印了从0到9之间的随机整数。其中rand.Intn函数用于获取随机数。在导入包后,我们可以通过包的名称来访问其函数和变量。例如,我们使用rand.Intn调用Intn函数。

如何创建自己的包?

我们也可以创建自己的golang包。以下是创建自己的包的步骤:

1.创建一个包目录

我们可以在任何目录下创建一个golang包,但是最好在我们的工作区目录树中创建它。我们的工作区目录树必须包含src目录,其中包含我们的所有go源文件。

例如,我们可以在工作区目录树/src/myutils下创建一个新的包目录。

2.将功能代码放入包中

我们将我们的代码放到包目录下的一个或多个.go文件中。这些文件必须在包声明的同一目录下。

例如,在myutils目录下创建一个myutils.go文件,并在其中添加以下代码:

package myutils
import "fmt"
//函数名必须是大写其他包中才可以使用
func SayHello(){
    fmt.Println("hello,world")
}

3.在其他代码中使用自己的包

我们可以像使用任何其他包一样使用我们的包。我们可以使用import语句导入它,然后使用它的函数和变量。

例如,在我们的主程序中,我们可以使用以下代码:

package main
​
import "myutils"
​
func main(){
    myutils.SayHello()
}
​

4.Go支持什么形式的类型转换?

  1. 基本类型转换

  2. 自定义类型转换

一、基本类型转换

在golang中,包括int、float、string等等在内的基本类型都支持类型转换,开发人员可以使用内置的转换函数将一个基本类型转换成另一个基本类型。

1.整型类型转换

在golang中,整型类型包括int8、int16、int32、int64、uint8、uint16、uint32、uint64等等。当我们需要将一个整型变量从一个类型转换成另一个类型时,可以使用内置的转换函数。

下面以将int32类型转换为int64类型为例:

var a int64 = 100
var b int32
b = int32(a)
fmt.Println(a, b)

2.浮点型类型转换

在golang中,浮点型类型包括float32和float64,将一个浮点型变量从一个类型转换成另一个类型时,同样可以使用内置的转换函数。

下面以将float32类型转换为float64类型为例:

var a float32= 100.00 var b float64 b = float32(a) fmt.Println(a, b)

3.字符串类型转换

将一个字符串转换为其它类型的变量也比较常见,golang提供了内置的转换函数,如下:

a := "1234"
b, err := strconv.Atoi(a)   //将字符串转换成int类型的变量
c, err := strconv.ParseFloat(a,64)  //将字符串转换成float64类型的变量
d := strconv.Itoa(1234)   //将int类型的变量转换成字符串
e := strconv.FormatFloat(3.14159, 'E', -1, 64)  //将float64类型的变量转换成字符串`

二、自定义类型转换

在golang中,自定义类型也可以进行类型转换,只需要实现相应的类型转换函数即可。

在自定义类型上进行类型转换需要满足一定的条件,具体如下:

1.自定义类型T必须是一个类型

2.类型T有一个底层类型,且底层类型不能是指针或接口类型

3.底层类型与T之间可以进行相互转换

5.什么是Goroutine?你如何停止它?

协程/线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

线程:一个线程上可以跑多个协程,协程是轻量级的线程。

如何创建goroutine?

go 函数名( 参数列表 )

如何停止goroutine?

1.使用channel

使用channel是最简单、最安全的停止goroutine的方法之一。可以创建一个bool类型的channel,当我们需要停止goroutine时,给这个channel发送一个信号,goroutine收到信号后就可以正常退出了。

package main
​
import (
    "fmt"
    "sync"
    "time"
)
​
var wg sync.WaitGroup
​
func worker(stopchan chan bool) {
    defer wg.Done()
    for {
        select {
        case <-stopchan:
            fmt.Println("worker is stopped")
            return
        default:
            fmt.Println("worker is running")
            time.Sleep(time.Second * 1)
        }
​
    }
}
func main() {
    wg.Add(1)
    stopchan := make(chan bool)
    go worker(stopchan)
    time.Sleep(time.Second * 3)
    stopchan <- true
    wg.Wait()
}
​

2.使用context

在Go1.7中,标准库中加入了context包,提供了一种新的挂起、取消goroutine的方法。我们可以在每个goroutine中传入一个Context参数,然后在需要停止goroutine时,对这个Context变量执行cancel操作。

package main
​
import (
    "context"
    "fmt"
    "sync"
    "time"
)
​
​
func worker1(ctx context.Context) {
    
    for {
        select {
        case <-ctx.Done():
            fmt.Println("worker is stopped")
            return
        default:
            fmt.Println("worker is running")
            time.Sleep(time.Second * 1)
        }
​
    }
}
func main() {
    
    ctx, cancel := context.WithCancel(context.Background())
    go worker1(ctx)
    time.Sleep(3 * time.Second)
    cancel()
    time.Sleep(time.Second)
}
​

3. 使用Mutex和WaitGroup

通过共享变量的方式来控制goroutine的停止,这种方法需要使用sync包中的Mutex和WaitGroup。Mutex用于保护共享变量的读写,WaitGroup用于等待所有goroutine完成。

4. 使用runtime包

使用runtime包的方法则要稍微麻烦一些,在goroutine运行的代码中,需要定期地检查全局变量,然后在需要停止goroutine时,使用runtime包中的函数强制将其终止。

6.Context

1.什么是context

context是一个接口,它具备手动、定时、超时发出取消信号、传值等功能,主要用于控制多个goroutine之间的协作,尤其是取消操作。一旦取消指令下达,那么被context跟踪的这些goroutine都会收到取消信号,就可以清理和退出操作。

goroutine的应用更多的需要配合context来做并发任务的处理:

传递数据、主动取消、超时取消

2.context的接口方法

type Context interface{

Deadline()(deadline time.Time,ok bool)

Done()<-chan struct{}

Err() error

Value(key interface{}) interface{}

}

Deadline 方法可以获取设置的截至日期,第一个返回值deadline是截止日期,到这个时间,Context会自动发送取消请求,第二个返回值ok 代表是否设置了截止时间

Done 方法返回是一个只读的channel,类型为struct{},在goroutine中,如果该方法返回的chan可读,则意味着context已经发起了取消信号,通过Done方法收到这个信号后,就可以做清理操作,然后退出goroutine,释放资源,多次调用Done方法会返回相同的通道。最常用的方法

Err方法返回取消的错误原因,即因为什么原因Context被取消。

Value方法取该Context上绑定的值,返回指定key对应的value,这是Context携带的值。key必须是可比较的,一般的用法key是一个全局变量,通过context.WithValue将key存储到Context中,并通过Context.Value方法取出。一个键值对,所以要通过一个key才可以获取对应的值。

3.context树

Go 语言提供了函数可以帮助我们生成不同的 Context,通过这些函数可以生成一颗 Context 树,这样 Context 才可以关联起来,父 Context 发出取消信号的时候,子 Context 也会发出,这样就可以控制不同层级的 goroutine 退出。

从使用功能上分,有四种实现好的 Context。

空 Context:不可取消,没有截止时间,主要用于 Context 树的根节点。

可取消的 Context:用于发出取消信号,当取消的时候,它的子 Context 也会取消。

可定时取消的 Context:多了一个定时的功能。

值 Context:用于存储一个 key-value 键值对。

在 Go 语言中,可以通过 context.Background() 获取一个根节点 Context。有了根节点 Context 后,这颗 Context 树要怎么生成呢?需要使用 Go 语言提供的四个函数。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
​

WithCancel 函数会返回一个子 Context 和 cancel 方法。子 Context 会在两种情况下触发退出:一种情况是调用者主动调用了返回的 cancel 方法;另一种情况是当参数中的父 Context 退出时,子 Context 将级联退出。

WithDeadline:生成一个可定时取消的 Context,参数 d 为定时取消的具体时间。

WithTimeout:生成一个可超时取消的 Context,参数 timeout 用于设置多久后取消。当超时发生后,子 Context 将退出。因此,子 Context 的退出有三种时机,一种是父 Context 退出;一种是超时退出;最后一种是主动调用 cancel 函数退出。

WithValue:生成一个可携带 key-value 键值对的 Context。

以上四个生成 Context 的函数中,前三个都属于可取消的 Context,它们是一类函数,最后一个是值 Context,用于存储一个 key-value 键值对。

3.1context.WithCancel 取消多个 goroutine

package main
​
import (
    "context"
    "fmt"
    "sync"
    "time"
)
​
func main() {
    var wg sync.WaitGroup
    wg.Add(3)
    ctx, stop := context.WithCancel(context.Background())
    go func() {
        defer wg.Done()
        watchDog(ctx, "watchdog_1")
    }()
​
    go func() {
        defer wg.Done()
        watchDog(ctx, "watchdog_2")
    }()
​
    go func() {
        defer wg.Done()
        watchDog(ctx, "watchdog_3")
    }()
​
    time.Sleep(5 * time.Second)
    stop() //发停止指令
    wg.Wait()
}
​
func watchDog(ctx context.Context, name string) {
    //开启for select循环,一直后台监控
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name, "receive stop cmd, will stop")
            return
        default:
            fmt.Println(name, "is running ……")
        }
        time.Sleep(1 * time.Second)
    }
}
​
​

使用 context.WithCancel(context.Background()) 函数生成一个可以取消的 Context,用于发送停止指令。这里的 context.Background() 用于生成一个空 Context,一般作为整个 Context 树的根节点。

3.2 context.WithValue 传值

Context 不仅可以取消,还可以传值,通过这个能力,可以把 Context 存储的值供其他 goroutine 使用。我通过下面的代码来说明:

package main
​
import (
    "context"
    "fmt"
    "sync"
    "time"
)
​
func main() {
    var wg sync.WaitGroup
    wg.Add(4)
    ctx, stop := context.WithCancel(context.Background())
    go func() {
        defer wg.Done()
        watchDog(ctx, "watchdog_1")
    }()
​
​
go func() {
    defer wg.Done()
    watchDog(ctx, "watchdog_2")
}()
​
go func() {
    defer wg.Done()
    watchDog(ctx, "watchdog_3")
}()
​
valCtx := context.WithValue(ctx, "userId", 2)
go func() {
    defer wg.Done()
    getUser(valCtx)
}()
​
time.Sleep(5 * time.Second)
stop() //发停止指令
wg.Wait()
​
}
​
func watchDog(ctx context.Context, name string) {
    //开启for select循环,一直后台监控
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name, "receive stop cmd, will stop")
            return
        default:
            fmt.Println(name, "is running ……")
        }
        time.Sleep(1 * time.Second)
    }
}
​
func getUser(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("user exit")
            return
        default:
            userId := ctx.Value("userId")
            fmt.Println("userId is", userId)
            time.Sleep(1 * time.Second)
        }
    }
}

其中,通过 context.WithValue 函数存储一个 userId 为 2 的键值对,就可以在 getUser 函数中通过 ctx.Value("userId") 方法把对应的值取出来,达到传值的目的。

3.3 context.WithTimeout 超时取消

在实际开发过程,当我们需要开一个 Goroutine 来做一些耗时的操作的时候,我们可能需要控制超时时间,超时后,让 Goroutine 退出。

比如,在我实际开发 IM 消息系统的架构中,我需要从 DB 中拉取消息,因此我要控制好超时,怎么控制呢? 通过 context.WithTimeout 设置超时时间,然后把返回的新的上下文传递到 Goroutine 任务中,这样,超时后自动退出并清理,示例如下:

package main
​
import (
    "fmt"
    "sync"
    "time"
​
    "golang.org/x/net/context"
)
​
var (
    wg sync.WaitGroup
)
​
func startTask(ctx context.Context) error {
    defer wg.Done()
​
    for i := 0; i < 30; i++ {
        select {
        case <-time.After(2 * time.Second):
            fmt.Printf("in goroutine do task %v\n", i)
​
        // we received the signal of cancelation in this channel
        case <-ctx.Done():
            fmt.Printf("cancel goroutine task %v\n", i)
            return ctx.Err()
        }
    }
    return nil
}
​
func main() {
    timeoutCtx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
    defer cancel()
​
    fmt.Println("startTask")
​
    wg.Add(1)
    go startTask(timeoutCtx)
    wg.Wait()
​
    fmt.Println("endTask")
}   
​

3.4Context 使用原则

Context 是一种非常好的工具,使用它可以很方便地控制取消多个 goroutine 。在 Go 语言标准库中也使用了它们,比如 net/http 中使用 Context 取消网络的请求。

要更好地使用 Context,有一些使用原则需要尽可能地遵守。

Context 不要放在结构体中,要以参数的方式传递。

Context 作为函数的参数时,要放在第一位,也就是第一个参数。

要使用 context.Background 函数生成根节点的 Context,也就是最顶层的 Context。

Context 传值要传递必须的值,而且要尽可能地少,不要什么都传。

Context 多 goroutine 安全,可以在多个 goroutine 中放心使用。

可以把一个 Context 对象传递给任意个数的 Gorotuine,对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。

Context 一般是作为函数的参数进行传递,并且最优的做法是把 Context 作为第一个参数放到每个关键函数的参数中,并且变量名都建议统一命名,名为 ctx。

一般而言,把 context.Background() 作为第一个 parent Context

Context 的 Value 中应该传递必须的核心元数据,不要什么数据都使用 Context 传递。

永远记住,只要传递 Context,就不要把 Context 设置为 nil 来传递。

以上原则是规范类的,Go 语言的编译器并不会做这些检查,要靠自己遵守。

3.5Context底层原理

Context 在很大程度上利用了通道的一个特性:通道在 close 时,会通知所有监听它的协程。

每个派生出的子 Context 都会创建一个新的退出通道,这样,只要组织好 Context 之间的关系,就可以实现继承链上退出信号的传递。如图所示的三个协程中,关闭通道 A 会连带关闭调用链上的通道 B,通道 B 会关闭通道 C。 前面我们说,Context.Background 函数和 Context.TODO 函数会生成一个根 Context。要使用 context 的退出功能,需要调用 WithCancel 或 WithTimeout,派生出一个新的结构 Context。 WithCancel 底层对应的结构为 cancelCtx,WithTimeout 底层对应的结构为 timerCtx,timerCtx 包装了 cancelCtx,并存储了超时时间。代码如下所示。

​
type cancelCtx struct {
  Context
​
  mu       sync.Mutex   
  done     atomic.Value  
  children map[canceler]struct{} 
  err      error
}
​
type timerCtx struct {
  cancelCtx
  timer *time.Timer 
​
  deadline time.Time
}
​

cancelCtx 第一个字段保留了父 Context 的信息。children 字段则保存了当前 Context 派生的子 Context 的信息,每个 Context 都会有一个单独的 done 通道。

而 WithDeadline 函数会先判断父 Context 设置的超时时间是否比当前 Context 的超时时间短。如果是,那么子协程会随着父 Context 的退出而退出,没有必要再设置定时器。

当我们使用了标准库中默认的 Context 实现时,propagateCancel 函数会将子 Context 加入父协程的 children 哈希表中,并开启一个定时器。当定时器到期时,会调用 cancel 方法关闭通道,级联关闭当前 Context 派生的子 Context,并取消与父 Context 的绑定关系。这种特性就产生了调用链上连锁的退出反应。

​
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
  
  ...
   // 关闭当前通道
  close(d)
  // 级联关闭当前context派生的子context
  for child := range c.children {
    child.cancel(false, err)
  }
  c.children = nil
  c.mu.Unlock()
  // 从父context中能够删除当前context关联
  if removeFromParent {
    removeChild(c.Context, c)
  }
}
​

7.如何在运行时检查变量类型?

反射

示例1:

package main
​
import (
    "fmt"
    "reflect"
)
​
type Person struct {
    Name string
    Age  int
}
​
func typeofObject(variable interface{}) string {
    switch variable.(type) {
    case int:
        return "int"
    case float32:
        return "float32"
    case bool:
        return "boolean"
    case string:
        return "string"
    default:
        // 如果是结构体类型,返回结构体名称
        v := reflect.ValueOf(variable)
        if v.Kind() == reflect.Struct {
            return v.Type().Name() + " struct{}"
        }
        return "unknown"
    }
}
​
func main() {
    var a int = 42
    var b float32 = 3.14
    var c bool = true
    var d string = "hello"
    var e Person = Person{"John Doe", 30}
​
    fmt.Println(typeofObject(a)) // Output: int
    fmt.Println(typeofObject(b)) // Output: float32
    fmt.Println(typeofObject(c)) // Output: boolean
    fmt.Println(typeofObject(d)) // Output: string
    fmt.Println(typeofObject(e)) // Output: Person
}
​

示例2:

package main
​
import "fmt"
​
type Data struct {
}
​
func demo2(d Data) {
    fmt.Println(typeofObject(d))
}
​
func demo1(v interface{}) {
    //断言v为Data类型
    demo2(v.(Data))
}
​
func main() {
    //Data类型隐式转换成interface{}类型
    demo1(Data{})
}
​
func typeofObject(variable interface{}) string {
    switch variable.(type) {
    case int:
        return "int"
    case float32:
        return "float32"
    case bool:
        return "boolean"
    case string:
        return "string"
    case struct{}:
        return "struct{}"
    case interface{}:
        return "interface{}"
    default:
        return "unknown"
    }
}
​
​

示例3:

package main
​
import (
    "fmt"
    "reflect"
)
​
func testReflact(i interface{}) {
    reType := reflect.TypeOf(i)
    
    fmt.Println("reType的类型是", reType)
    fmt.Printf("reType的具体类型是:%T\n", reType)
    reValue := reflect.ValueOf(i)
    fmt.Println(reValue)
    fmt.Printf("reValue的具体类型:%T", reValue)
}
​
func main() {
​
    //
    var num int = 100
    testReflact(num)
​
}
​

8.Go两个接口之间可以存在什么关系?

如果两个接口有相同的方法列表,那么他们等价,可以相互赋值

接口A可以嵌套接口B里面

9.Go的同步锁有什么特点?作用是什么?

在 Go 中,同步锁是一种用于保护临界区(被多个 goroutine 访问的共享资源)的机制,以确保在某个时刻只有一个 goroutine 可以访问共享资源,从而避免并发访问的竞态条件。

同步锁的特点包括:

  1. 互斥性:同步锁一次只能被一个 goroutine 持有,其他 goroutine 必须等待锁的释放才能进入临界区。

  2. 阻塞性:当一个 goroutine 尝试获取锁时,如果锁已经被其他 goroutine 持有,则该 goroutine 将被阻塞,直到锁被释放。

  3. 公平性:Go 的同步锁是非公平锁,即不能保证等待时间最长的 goroutine 能优先获得锁。

同步锁的作用包括:

  1. 数据竞争避免:同步锁可用于避免多个 goroutine 并发访问共享资源时的数据竞争问题,确保对共享资源的访问是安全的。

  2. 同步控制:同步锁可以用于对多个 goroutine 的执行顺序进行控制,确保它们按照一定的顺序来访问共享资源。

  3. 代码保护:同步锁可以将一段代码标记为临界区,保证同一时刻只有一个 goroutine 执行该段代码,从而避免并发访问时的问题。

  • 54
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱编程的小猴

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

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

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

打赏作者

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

抵扣说明:

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

余额充值