golang学习笔记

//每个 go 程序都是由包构成
//程序从 main 包开始执行
//本程序通过导入路径 "fmt" 和 "dll/stringutil" 来使用这两个包
//按照约定,包名与导入路径的最后一个元素一致

//在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。 例如, Pizza 就是个已导出名, Pi 也同样,它导出自 math 包

//函数可以返回任意数量的返回值

//Go 的返回值可被命名,它们会被视作定义在函数顶部的变量;返回值的名称应当具有一定的意义,它可以作为文档使用

//var 语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后

//在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明;函数外的每个语句都必须以关键字开始( var 、 func 等等),
//因此 := 结构不能在函数外使用

//Go的基本类型:bool、string; int  int8  int16  int32  int64; uint uint8 uint16 uint32 uint64 uintptr
//byte (uint8 的别名)、 rune (int32 的别名, 表示一个 Unicode 码点);float32 float64; complex64 complex128

//类型转换:表达式 T(v) 将值 v 转换为类型 T

//常量:常量的声明与变量类似,只不过是使用 const 关键字
package main

import ("fmt"
        "dll/stringutil")

func main() {
    fmt.Printf(stringutil.Reverse("!oG ,olleH"))
}


//Go 只有一种循环结构:for循环
package main

import "fmt"

func main()  {
    sum := 0;
    for i := 0; i < 10; i++ {
        sum += i;
    }

    fmt.Println(sum);
}


//defer 语句会将函数推迟到外层函数返回之后执行
//推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用
//推迟的函数调用会被压入一个栈中。 当外层函数返回时,被推迟的函数会按照后进先出的顺序调用
package main

import "fmt"

//打印结果为:hello world 9876543210
func main() {
    for i := 0; i < 10; i++ {
        defer fmt.Print(i)
    }

    s := " world "
    defer fmt.Printf(s)

    s = " world! "
    fmt.Printf("hello")
}


//Go 具有指针。 指针保存了变量的内存地址;与 C 不同,Go 没有指针运算
package main

import "fmt"

//42 21 73
func main() {
    i, j := 42, 2701

    p := &i
    fmt.Println(*p)
    *p = 21
    fmt.Println(i)

    p = &j
    *p = *p / 37
    fmt.Println(j)
}


//一个结构体( struct )就是一个字段的集合,而 type 声明就是定义类型的;结构体字段使用点号来访问
package main

import "fmt"

type Vertex struct {
    X int
    Y string
}

//{1 string}
func main() {
    fmt.Println(Vertex{1, "string"})

    v := Vertex{3, "4"}
    fmt.Printf(v.Y)
}


//切片就像数组的引用。切片并不存储任何数据, 它只是描述了底层数组中的一段;更改切片的元素会修改其底层数组中对应的元素;
//与它共享底层数组的切片都会观测到这些修改
//切片的长度与容量:切片的长度就是它所包含的元素个数;切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数
//切片下界的默认值为 0 ,上界则是该切片的长度(不是容量)
//nil 切片的长度和容量为 0 且没有底层数组
//切片可以用内建函数 make 来创建,这也是你创建动态数组的方式
//为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append 函数,append 的结果是一个包含原切片所有元素加上新添加元素的切片
//当切片的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。 返回的切片会指向这个新分配的数组

package main

import "fmt"

//len=6 cap=6 [2 3 5 7 11 13]
//len=0 cap=6 []
//len=4 cap=6 [2 3 5 7]
//len=2 cap=4 [5 7]
//len=2 cap=3 [7 11]
func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    s = s[:0]
    printSlice(s)

    s = s[:4]
    printSlice(s)

    s = s[2:]
    printSlice(s)

    s = s[1:3]
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}


//for 循环的 range 形式可遍历切片或映射
//当使用 for 循环遍历切片时,每次迭代都会返回两个值。 第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本
//可以将下标或值赋予 _ 来忽略它;若你只需要索引,去掉 value 的部分即可
package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
    for i, v := range pow {
        fmt.Printf("2^%d = %d\n", i, v)
    }

    pow1 := make([]int, 10)
    for i := range pow1 {
        pow1[i] = 1 << uint(i) // == 2^i
    }
    for _, value := range pow1 {
        fmt.Printf("%d\n", value)
    }
}

package main

import "golang.org/x/tour/pic"

func Pic(dx, dy int) [][]uint8 {
    a := make([][]uint8, dy*dx)

    for i := 0; i < dy; i++ {
        b := make([]uint8, dx)
        for j := 0; j < dx; j++ {
            c := j % 8;
            b[j] = uint8(1<<uint8(c))
        }
        a[i] = b
    }
    return a
}

func main() {
    pic.Show(Pic)
}


//映射
//映射的零值为 nil 。`nil` 映射既没有键,也不能添加键;make 函数会返回给定类型的映射,并将其初始化备用

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m map[string]Vertex

func main() {
    m = make(map[string]Vertex)
    m["Bell Labs"] = Vertex{
        40.68433, -74.39967,
    }
    fmt.Println(m["Bell Labs"])
}


//实现 WordCount 。它应当返回一个映射,其中包含每个字符串 s 中“单词”的个数。
//函数 wc.Test 会对此函数执行一系列测试用例,并输出成功还是失败

package main

import (
    "strings"
    "golang.org/x/tour/wc"
)

func WordCount(s string) map[string]int {
    field := strings.Fields(s)

    m := make(map[string]int)
    for i := 0; i < len(field); i++ {
        if m[field[i]] > 0 {
            m[field[i]] = m[field[i]] + 1
        } else {
            m[field[i]] = 1
        }
    }
    return m
}

func main() {
    wc.Test(WordCount)
}


//函数也是值。它们可以像其它值一样传递
//函数值可以用作函数的参数或返回值

package main

import (
    "fmt"
    "math"
)

func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

func main() {
    hypot := func(x, y float64) float64 {
        return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(5, 12))

    fmt.Println(compute(hypot))
    fmt.Println(compute(math.Pow))
}


//闭包
//其实理解闭包的最方便的方法就是将闭包函数看成一个类,一个闭包函数调用就是实例化一个类
//然后就可以根据类的角度看出哪些是“全局变量”,哪些是“局部变量”了
//比如本例中的adder函数返回func(int) int 的函数
//pos和neg分别实例化了两个“闭包类”,在这个“闭包类”中有个“闭包全局变量”sum。所以这样就很好理解返回的结果了

package main

import "fmt"

func adder() (func(int) int) {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {

    pos, neg := adder(), adder()

    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2 * i),
        )
    }
}

//实现一个 fibonacci 函数,它返回一个函数(闭包), 该闭包返回一个斐波纳契数列 `(0, 1, 1, 2, 3, 5, 8, 13, 21, ...)`
package main

import "fmt"

func fibonacci() func() int {
    fibItemPre1 := -1
    fibItemPre2 := 0
    fibItem := 0

    return func() int {
        if fibItemPre1  == -1 {
            fibItemPre1  = 0;
            return 0
        } else if fibItemPre1 == 0 {
            fibItemPre1  = 1
            return 1
        }  else {
            fibItem = fibItemPre1 + fibItemPre2
            fibItemPre2 = fibItemPre1
            fibItemPre1 = fibItem
            return fibItem
        }
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 15; i++ {
        fmt.Println(f())
    }
}


//方法
//Go没有类,不过你可以为结构体类型定义方法;方法就是一类带特殊的 接收者 参数的函数;
//方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间
//记住:方法只是个带接收者参数的函数

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
}

//指针接收者
//而以值为接收者的方法被调用时,接收者既能为值又能为指针:
//var v Vertex
//fmt.Println(v.Abs()) // OK
//p := &v
//fmt.Println(p.Abs()) // OK
//这种情况下,方法调用 p.Abs() 会被解释为 (*p).Abs()

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    v.Scale(10)
    fmt.Println(v.Abs())
}


//接口
//接口类型 是由一组方法签名定义的集合
//接口类型的值可以保存任何实现了这些方法的值

package main

import (
    "fmt"
    "math"
)

type Abser interface {
    Abs() float64
}

func main() {
    var a Abser
    f := MyFloat(-math.Sqrt2)
    v := Vertex{3, 4}

    a = f  // a MyFloat 实现了 Abser
    a = &v // a *Vertex 实现了 Abser

    // 下面一行,v 是一个 Vertex(而不是 *Vertex)
    // 所以没有实现 Abser。
    // a = v

    fmt.Println(a.Abs())
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

//接口值
//在内部,接口值可以看做包含值和具体类型的元组;接口值保存了一个具体底层类型的具体值;接口值调用方法时会执行其底层类型的同名方法
//空接口
//指定了零个方法的接口值被称为 空接口:
//interface{}
//空接口可保存任何类型的值。 (因为每个类型都至少实现了零个方法。)
//空接口被用来处理未知类型的值。 例如,`fmt.Print` 可接受类型为 interface{} 的任意数量的参数

package main

import "fmt"

func main() {
    var i interface{}
    describe(i)

    i = 42
    describe(i)

    i = "hello"
    describe(i)
}

func describe(i interface{}) {
    fmt.Printf("(%v, %T)\n", i, i)
}


//类型断言
//类型断言 提供了访问接口值底层具体值的方式
//t := i.(T) 该语句断言接口值 i 保存了具体类型 T ,并将其底层类型为 T 的值赋予变量 t
//若 i 并未保存 T 类型的值,该语句就会触发一个panic

//为了 判断 一个接口值是否保存了一个特定的类型, 类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值
//t, ok := i.(T)  若 i 保存了一个 T ,那么 t 将会是其底层值,而 ok 为 true
//否则, ok 将为 false 而 t 将为 T 类型的零值,程序并不会产生恐慌

package main

import "fmt"

func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s)

    s, ok := i.(string)
    fmt.Println(s, ok)

    f, ok := i.(float64)
    fmt.Println(f, ok)

    f = i.(float64) // panic
    fmt.Println(f)
}

//通过让 IPAddr 类型实现 fmt.Stringer 来打印点号分隔的地址
//例如,`IPAddr{1,`2,`3,`4}` 应当打印为 "1.2.3.4"

package main

import (
    "fmt"
    "strconv"
    "strings"
)

type IPAddr [4]byte

func appendString(bs []string, b byte) []string {
    var a byte
    var s int
    for i := 0; i < 8; i++ {
        a = b
        b <<= 1
        b >>= 1
        switch a {
        case b:
            s += 0
        default:
            temp := 1
            for j := 0; j < 7 - i; j++ {
                temp = temp*2
            }
            s += temp
        }

        b <<= 1
    }

    bs = append(bs, strconv.Itoa(s))
    return bs
}

func BytesToString(bs []byte) string {
    l := len(bs)
    buf := make([]string, 0, l)
    for i := 0; i < l; i++ {
        buf = appendString(buf, bs[i])
    }
    return strings.Join(buf, ".")
}

func (p IPAddr) String() string {
    return BytesToString(p[:])
}

func main() {
    hosts := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for name, ip := range hosts {
        fmt.Printf("%v: %v\n", name, ip)
    }
}


//Channels
//默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步
package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // send sum to c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c

    fmt.Println(x, y, x+y)
}

package main

var a string
var c = make(chan int)

func f() {
    a = "hello, world"
    <-c
}

func main() {
    go f()
    //c <- 0
    print(a)

}

//信道可以是 带缓冲的 。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道
//仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞
//有缓冲的channel要注意“放”先于“取”
package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2

    //fatal error: all goroutines are asleep - deadlock!
    //goroutine 1 [chan send]: ... exit status 2
    //ch <- 3

    fmt.Println(<-ch)
    fmt.Println(<-ch)
}


//range 和 close
//send to channel, receive from channel
//sender可通过 close 关闭一个信道来表示没有需要发送的值了。receiver可以通过为接收表达式分配第二个参数来测试信道是否被关闭:
//若没有值可以接收且信道已被关闭,那么在执行完 v, ok := <-ch 之后 ok 会被设置为 false
//只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发 panic
//信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有值需要发送的时候才有必要关闭,例如终止一个 range 循环
//若在信道关闭后从中接收数据,接收者就会收到该信道返回的零值

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

//select 语句
package main

import (
    "fmt"
)

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            fmt.Println("send...", x)
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        default:
            fmt.Println("wait......")
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    //使用 go 在一个新的 goroutine 中执行代码,此函数中的for循环不一定在主线程调用fibonacci(c, quit)之前就会执行;
    //因此,可能会首先打印wait......,等待sender和receiver准备好
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println("receive...", <-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

//sync.Mutex
package main

import (
    "fmt"
    "sync"
    //"time"
)

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
    v   map[string]int
    mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    c.v[key]++
    c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    defer c.mux.Unlock()
    return c.v[key]
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        go c.Inc("somekey")
    }

    //time.Sleep(time.Second)
    fmt.Println(c.Value("somekey"))
}

//默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步
//goroutine阻塞后,在函数操作执行之前main线程就已完成并退出
//goroutine背后的系统知识: http://www.sizeofvoid.net/goroutine-under-the-hood/
//Concurrency is not Parallelism: https://talks.golang.org/2012/waza.slide#1

//http://www.cnblogs.com/shenguanpu/archive/2013/05/05/3060616.html
//http://blog.zhaojie.me/2013/04/why-channel-and-goroutine-in-golang-are-buildin-libraries-for-other-platforms.html
//http://www.yankay.com/go-clear-concurreny/
//作者Rob Pike 说:一个Goroutine是一个与其他goroutines 并发运行在同一地址空间的Go函数或方法。
//一个运行的程序由一个或更多个goroutine组成。它与线程、协程、进程等不同。它是一个goroutine。
//goroutine的并发问题:goroutine在共享内存中运行,通信网络可能死锁,多线程问题的调试糟糕透顶等等。
//一个比较好的建议规则:不要通过共享内存通信,相反,通过通信共享内存
//goroutine是Go语言运行库的功能,不是操作系统提供的功能,goroutine不是用线程实现的
//goroutine是一个比线程更小的代码执行单位,假如说“线程”是用来计算的“物理”资源,那么goroutine就可以认为是计算的“逻辑”资源了,
//我们可以创建大量此类单元而不用担心占用过多资源,自有调度器来使用一个或多个线程来执行它们的逻辑

//线程和协程的区别:
//一旦创建完线程,你就无法决定他什么时候获得时间片,什么时候让出时间片了,你把它交给了内核。
//而协程编写者可以有一是可控的切换时机,二是很小的切换代价。从操作系统有没有调度权上看,协程就是因为不需要进行内核态的切换,
//所以会使用它,会有这么个东西。赖永浩和dccmx 这个定义我觉得相对准确  协程-用户态的轻量级的线程

//goroutine就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈。所以它非常廉价,我们可以很轻松的创建上万个goroutine,
//但它们并不是被操作系统所调度执行
//和所有其他并发框架里的协程一样,goroutine里所谓“无锁”的优点只在单线程下有效,如果$GOMAXPROCS > 1并且协程间需要通信,
//Go运行库会负责加锁保护数据,这也是为什么sieve.go这样的例子在多CPU多线程时反而更慢的原因
//Go语言运行库封装了异步IO,所以可以写出貌似并发数很多的服务端,可即使我们通过调整$GOMAXPROCS来充分利用多核CPU并行处理,
//其效率也不如我们利用IO事件驱动设计的、按照事务类型划分好合适比例的线程池。在响应时间上,协作式调度是硬伤
//goroutine最大的价值是其实现了并发协程和实际并行执行的线程的映射以及动态扩展,随着其运行库的不断发展和完善,其性能一定会越来越好,
//尤其是在CPU核数越来越多的未来,终有一天我们会为了代码的简洁和可维护性而放弃那一点点性能的差别
package main

import (
    "time"
    "fmt"
)

var c chan string
func ready(w string, sec time.Duration) {
    time.Sleep(sec * time.Second)
    fmt.Println(w, "is ready!")
    c <- w
}

func main()  {
    c = make(chan string)
    go ready("Tee", 2)
    go ready("Coffee", 1)
    fmt.Println("I am waiting!")
    //time.Sleep(5 * time.Second)
    //<- c
    //<- c
}


//Go 内存模型(https://go-zh.org/ref/mem)
//Go内存模型阐明了一个 goroutine 对某变量的写入,如何才能确保被另一个读取该变量的 goroutine 监测到
//go 语句会在当前goroutine开始执行前启动新的Go程
//对于任何 sync.Mutex 或 sync.RWMutex 类型的变量 l 以及n < m,对l.Unlock()的第 n 次调用在对l.Lock()的第 m 次调用返回前发生
//对于任何 sync.RWMutex 类型的变量 l 对 l.RLock 的调用,存在一个这样的 n,使得 l.RLock 在对 l.Unlock
//的第 n 次调用之后发生(返回),且与其相匹配的 l.RUnlock 在对 l.Lock的第 n+1 次调用之前发生

package main

import "sync"

var l sync.Mutex
var a string

func f() {
    a = "hello, world"
    l.Unlock()
}

func main() {
    l.Lock()
    go f()
    l.Lock()
    print(a)
}


//Once 类型
//有些像单例但又不全像(函数作用域上的单例?)
//sync 包通过 Once 类型为存在多个Go程的初始化提供了安全的机制。 多个线程可为特定的 f 执行 once.Do(f),
//但只有一个会运行 f(),而其它调用会一直阻塞,直到 f() 返回
package main

import (
    "sync"
    "time"
)

var a string
var count int = 0
var once sync.Once

func setup() {
    a = "hello, world! "
    println("go in setup")
    count++
}

func doprint() {
    once.Do(setup)
    println(a)
    println(count)
}

func twoprint() {
    go doprint()
    go doprint()
}

func main() {
    twoprint()
    time.Sleep(1 * time.Second)
    twoprint()
    time.Sleep(1 * time.Second)
}


//错误的同步
//这里并不保证在 doprint 中对 done 的写入进行监测蕴含对 a 的写入进行监测。可能会(错误地)打印出一个空字符串而非 "hello, world"
package main

import (
    "sync"
    "time"
)

var a string
var done bool
var once sync.Once

func setup() {
    a = "hello, world!"
    done = true
}

func doprint() {
    if !done {
        once.Do(setup)
    }
    println(a)
}

func twoprint() {
    go doprint()
    go doprint()
}

func main() {
    twoprint()
    twoprint()
    time.Sleep(1 * time.Second)
}


//计算素数
//该示例从小到大针对每一个因子启动一个代码片段,相当于并发;但它无法被并行,因为每个片段都依赖于前一个片段的处理结果和输出

package main

import "fmt"

// Send the sequence 2, 3, 4, ... to channel 'ch'.
func Generate(ch chan<- int) {
    for i := 2; ; i++ {
        ch <- i // Send 'i' to channel 'ch'.
        print("---", i, "---")
    }
}

// Copy the values from channel 'in' to channel 'out',
// removing those divisible by 'prime'.
func Filter(in <-chan int, out chan<- int, prime int) {
    for {
        i := <-in // Receive value from 'in'.
        if i%prime != 0 {
            print("+++", i, "+++")
            out <- i // Send 'i' to 'out'.
        }
    }
}

// The prime sieve: Daisy-chain Filter processes.
func main() {
    ch := make(chan int) // Create a new channel.
    go Generate(ch)      // Launch Generate goroutine.
    for i := 0; i < 10; i++ {
        prime := <-ch
        fmt.Println(prime)
        ch1 := make(chan int)
        go Filter(ch, ch1, prime)
        ch = ch1
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值