FaceToFace G 1.21-216

FaceToFace G

1.21(1.22 1.24 1.28)
func main() {
    list := new([]int)
    
    
    
    //这里意味着list是一个 *[]int 类型的指针
    
    
    
    list = append(list, 1)
    
    
    
    
    //不能对指针进行append操作
    fmt.Println(list)
}
func main() {
    s1 := []int{1, 2, 3}
    s2 := []int{4, 5}
    s1 = append(s1, s2)
    
    
    
    
    //error
    //将一个切片追加到另一个切片上:append(s1,s2...)
    fmt.Println(s1)
    
}
var(
    size := 1024
    max_size = size*2
)


//error
//只能在函数内部使用简短模式

func main() {
    fmt.Println(size,max_size)
}
func main() {

     slice := []int{0,1,2,3}
     m := make(map[int]*int)

     for key,val := range slice {
         m[key] = &val
         
         
         
         //val的每次地址都是一样的,只是值不一样
         /*正确写法
         value := val//用一个新的地址保存新的值
         m[key] = &value
         */
     }

    for k,v := range m {
        fmt.Println(k,"->",*v)
    }
}
// 1.
 func main() {
     s := make([]int, 5)
     s = append(s, 1, 2, 3)
     fmt.Println(s)
 }
0 0 0 0 0 1 2 3
// 2.
 func main() {
    s := make([]int,0)
    s = append(s,1,2,3,4)
    fmt.Println(s)
}
1 2 3 4

new(T) 和 make(T,args) 是 Go 语言内建函数,用来分配内存,但适用的类型不同。

new(T) 会为 T 类型的新值分配已置零的内存空间,并返回地址(指针),即类型为 *T的值。换句话说就是,返回一个指针,该指针指向新分配的、类型为 T 的零值。适用于值类型,如数组、结构体等。

make(T,args) 返回初始化之后的 T 类型的值,这个值并不是 T 类型的零值,也不是指针 *T,是经过初始化之后的 T 的引用。make() 只适用于 slice、map 和 channel.

func main() {
    sn1 := struct {
        age  int
        name string
    }{age: 11, name: "qq"}
    sn2 := struct {
        age  int
        name string
    }{age: 11, name: "qq"}

    if sn1 == sn2 {
        fmt.Println("sn1 == sn2")
    }//success

    sm1 := struct {
        age int
        m   map[string]string
    }{age: 11, m: map[string]string{"a": "1"}}
    sm2 := struct {
        age int
        m   map[string]string
    }{age: 11, m: map[string]string{"a": "1"}}

    if sm1 == sm2 {
        fmt.Println("sm1 == sm2")
    }//error
}

结构体比较有几个需要注意的地方:

结构体只能比较是否相等,但是不能比较大小

相同类型的结构体才能够进行比较,结构体是否相同不但与属性类型有关,还与顺序有关

一般来说 普通类型如数组,指针,字符,布尔值可以进行比较

切片,map,channel,函数不可以

1.22(1.23 1.25 1.29)

1.23 已复习

type:

type MyInt int //实现了int的新类型
type MyInt2 = int;//int的别名->MyInt

var i int = 0
var myInt MyInt = i;


/*error
Go 是强类型语言,int类型不能赋给MyInt
correct:
var myInt Myint = (Myint)i
*/

字符串连接

str := "abc"+"123"
str := fmt.Sprinf("%s is %s year old","Jack","eight")
//strings.Join



str1 = []string{"I love you"}
str :=strings.Join(str1," ");



//首先,我们定义了一个名为str1的字符串切片,内容是"I love you"
//然后我们使用了strings.Join,实现了str1和" "的拼接




//buffer.WriteString

str1 := "Hello"
str2 :="HaiCoder"
var lk bytes.Buffer
lk.WriteString(str1)
lk.WriteString(str2)
strLk := lk.String()




/*首先,我们定义了两个字符串变量并赋值,然后我们定义了bytes.Buffer类型的变量
  然后,我们使用了writeString的方法将str1和str2均写入进去
  最后,我们使用String方法将这两者拼接,并把结果赋给strLk
*/

iota

const (
 x = iota
 _      
 y
 z = "zz"
 k
 p = iota
)
func main() {
 fmt.Println(x,y,z,k,p)
 }

/*
iota是golang语言的常量计数器,只能在常量的表达式中使用
iota在其出现的时候,变量x被重置为0,之后的每一行变量声明都会使iota计数一次
当遇到下划线的时候,同样会技术,通过下划线可以去掉不想要的值
//当遇到z= "zz"这样类型的值的时候,iota同样会计数
//但此时如若后面没有再出现变量p=iota 类似的子句
后面的变量会赋值为zz
同样,如果出现u = "hh"后面的也都会变成hh
*/

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

nil

nil 只能赋给指针,chan,func,interface,map或slice类型的变量

error是一种内置接口类型

type error interface{
Error () string
}

init()

在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。
需要注意的是:init()函数没有参数,也没有返回值。
init()函数在程序运行时,自动自动被调用执行,不能在代码中主动调用它。
包初始化执行的顺序如下图所示

img

关于 init() 函数有⼏个需要注意的地⽅:

    1. init() 函数是⽤于程序执⾏前做包的初始化的函数,⽐如初始化包⾥的
    2. ⼀个包可以==出现多个 init() 函数,==⼀个源⽂件也可以包含多个 init() 函数;
    3. 同⼀个包中多个 init() 函数的执⾏顺序没有明确定义,但是不同包的init函数是根据包导⼊的依赖关 系决定的;
    4. init() 函数在代码中不能被显示调⽤、不能被引⽤(赋值给函数变量),否则出现编译错误;
    5. ⼀个包被引⽤多次,如 A import B,C import B,A import C,B 被引⽤多次,但 B 包只会初始化⼀ 次;
    6. 引⼊包,不可出现死循坏。即A import B,B import A,这种情况编译失败;

接口

i.(type),其中 i 是接⼝,type 是固定 关键字,需要注意的是,只有接⼝类型才可以使⽤类型选择

1.23(1.24 1.26 1.30)

channel

Channel 是 Go的一个核心类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯

//它的操作符是箭头<-
ch <- v     //发送值v到Channel ch中
v := <-ch   //从Channel ch 中接收是数据,并将值赋给v

//和map、slice一样,channel必须先创建后使用
ch := make(chan int)


三种类型

chan T                //可以接收和发送类型为T的数据
chan<- float64   //只可以用来发送float64类型的数据
<-chan int          //只可以用来接收 int类型的数据

使用make初始化Channel,并且可以设置容量

make(chan int,100)

capcity: Channel容纳最多元素的数量,代表Channel的缓存的大小

如果没有设置容量,只有sender和receiver都准备好了,通讯才会发生

如果设置了容量,只有buffer(缓存)满了,sender才会阻塞

一个nil channel 不会通信

这里要插入一个goroutine (并发)

让我们来理解一下

goroutine是Go语言中最大的特色,Goroutinue是Go中最基本的执行单元

线程(Thread):轻量级进程,是进程中的一个实体,不拥有系统资源,只有在运行中必不可少的资源,与同属于一个进程的其他线程共享进程所拥有的全部资源

协程(coroutinue) :

与函数一样,是一种程序组件

和线程类似,共享堆,不共享栈,协程的切换一般由程序员在代码中显示控制,避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂

goroutine和协程类似,goroutine可以理解为一种Go语言的协程

go语言有和java类似的多线程共享内存

还有一个特点:通过通信来共享内存(CSP)

Go的CSP并发模型,是通过goroutinechannel来实现的。

  • goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。
  • channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。

数据用channel <- data,取数据用<-channel

在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。

而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。

package main

import "fmt"

func main() {
   
   messages := make(chan string)

   go func() { messages <- "ping" }()

   msg := <-messages
   fmt.Println(msg)
}
/* main()本身也是运行了一个goroutine。

messages:= make(chan int) 这样就声明了一个阻塞式的无缓冲的通道

chan 是关键字 代表我要创建一个通道

回到channel上来

可以通过内建的close方法可以关闭Channel。

你可以在多个goroutine从/往 一个channel 中 receive/send 数据, 不必考虑额外的同步措施。

Channel可以作为一个先入先出(FIFO)的队列,接收的数据和发送的数据的顺序是一致的。

v,ok := <-ch
func hello(num ...int) { 
 num[0] = 18
}
func main() { 
 i := []int{5, 6, 7}
 hello(i...)
 fmt.Println(i[0])
 }
t := a[3:4:4]

截取操作符还可以有第三个参数,形如 [i,j,k],第三个参数 k ⽤来限制新切⽚的容量,但不能超过原数组 (切⽚)的底层数组⼤⼩。截取获得的切⽚的⻓度和容量分别是:j-i、k-i。

所以例⼦中,切⽚ t 为 [4],⻓度和容量都是 1

a := [2]int{5,6}
b:=[3]int{5,6}

a和b可以进行比较?

Go中的数组是值类型,可比较,

但是数组的长度也是数组类型的组成部分,所以a和b是不同的类型

cap

package main

func main() {
	array := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	slice := array[0:5]
	temp := cap(slice)//slice最左边的指针索引是0,到结尾有9个数
	println(temp)
	slice = slice[2:]
	println(cap(slice))//此时最左边的索引为2,所以为7
}

接口

在Golang中一个接口由两个部分组成,分别是动态类型和动态值

在一个时刻,一个接口只能有一个类型和值,这是这个接口该时刻的具体类型和具体值。

因为一个接口可以有多个结构体实现,所以当不同的实现该接口的结构体赋值给一个接口变量的时候,接口的动态类型会变成该结构体类型,接口内部存储的指向结构体的指针会指向当前赋值的结构体,而动态值会变成当前赋值的结构体的值

有且仅当接口的动态值和动态类型都为nil时,接口类型为nil

func mian(){
    s:=make(map[string] int)
    delete(s,"h")//删除map不存在的键值对时,不会报错,相当于没有任何作用
    fmt.Println(s["h"])
    //获取不存在的键值对时,返回值类型对应为零值
}

Go语言有25个预定义的关键字,这些关键字用于各种特定目的,从声明变量和函数,到流程控制和并发编程。

break, default, func, interface, select, case, defer, go, map, struct, chan, else, goto, package, switch, const, fallthrough, if, range, type, continue, for, import, return, var

i:=-5
fmt.Printf("%+d",i)//+标识输出数值的符号,不表示取反
//输出-5

全局变量

在Go中,全局变量指的是在包的最顶层声明的头母大写的导出变量,这样这个变量在整个Go程序的任何角落都可以被访问和修改,比如下面示例代码中foo包的变量Global:

package foo

var Global = "myvalue" // Go全局变量
var Str string//只有这两种形式
//不能出现 str:= " "

package bar

import "foo"

func F1() {
 println(foo.Global)
 foo.Global = "another value"
}

defer

func hello(int i){
fmt.Println(i)
}
func main(){
i:= 5
defer.hello(i)
i += 10
}





//输出为5,因为执行到defer.hello时,我们已经保存了副本i
//defer虽然是在函数结束之后执行,但与副本无关

有关结构体嵌套

type People struct{}
func (p *People) ShowA() {
 fmt.Println("showA")
 p.ShowB()
}
func (p *People) ShowB() {
 fmt.Println("showB")
}
type Teacher struct {
 People
}
func (t *Teacher) ShowB() {
 fmt.Println("teacher showB")
}
func main() {
 t := Teacher{}
    t.ShowA()
}

在这个代码中,People称为内部类型,Teacher为外部类型,外部类型可以将内部类型的属性和方法当作自己的使用,但是如果外部类型出现与内部类型同样的属性和方法的时候,会屏蔽掉内部类型的方法与属性

字符串

package main

import "fmt"

func main() {
	str := "hello"
	str[0] = 'x'
	fmt.Println(str)
}

常量,Go 语⾔中的字符串是只读的。

nil切片和空切片

func main(){
var s1 []int    //nil切片
var s2 = []int{}//空切片
}

可以进行强转 int->string

切片a,b,c的长度和容量分别是多少?

func main(){
s:= [3]int{1,2,3}
a := s[:0]
b := s[:2]
c := s[1:2:cap(s)]
}


假设截取对象的底层数组长度为l

形如[i:j] [i:j:k] 截取得到的切片长度和容量分别是 j-i,l-i

k主要用来限制容量

切片长度和容量分别是 :j - i,k-i

1.24(1.25 1.27 1.31)

1.25 已过

1.31 已过

//规定x已声明,y没有声明
x,_  := f()  // x已经声明,:=只对没有声明的进行定义并赋值
x,_ = f()   //对
x,y :=f()  //对,当多值赋值的时候,:=左边的变量无论声明与否都可以
x,y = f() //错,y没有声明

defer 与返回值的关系

defer 语句与函数返回值一同使用时,需要注意以下几点:

  1. 延迟执行的是函数调用,而不是函数内的具体语句。

  2. defer 语句中的函数调用会使用函数定义时的参数值。

  3. defer 语句的执行时机是在包含它的函数执行完毕后,但在返回之前。

    package main
    
    import "fmt"
    
    func example() (result int) {
        defer func() {
            // 在函数返回前执行,可以修改返回值
            result++
            fmt.Println("Deferred function executed")
        }()
    
        fmt.Println("Executing main function")
        return 42
    }
    
    func main() {
        value := example()
        fmt.Println("Returned value:", value)
    }
    
    
    

    在这个例子中,defer 语句中的匿名函数会在 example 函数返回前执行,可以在其中修改返回值。输出结果如下:

Executing main function
Deferred function executed
Returned value: 43

但是如果我们在函数中定义了一个具体的值就不一样了,他并不会改变具名

func increaseA() int {
 var i int
 defer func() {
 i++
 }()
 return i
}

我们可以发现在这段代码中,i是一个具名,这也就意味这defer中的匿名函数保存的是i的副本,并不会改变i地址的值

func f3() (r int) {
 defer func(r int) {
 r = r + 5
 }(r)//这个传的是r的拷贝
 return 1

闭包

闭包是一个函数值,它引用了函数体之外的变量,这些变量可以是外部函数的参数,也可以是外部函数内部声明的变量

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第一个保存的是age的值,存入栈中,当这条语句被执行的时候,直接将存入的值取出 28

2.第二个保存的是p的地址,所以最后person中地址的值被修改,这里也同样会被修改

3.闭包引用的是地址,所以是一样的

最后一行修改的是对象中age的地址,person并没有改变哈

如果改变的话

person = &Person{29}

声明

通常选用不分配内存的

var a []int

永远不要使用一个指针指向一个接口类型,因为它已经是一个指针了

func g(x *interface{}) //这个是错误的

接收指针类型也可以用interface()

函数参数为interface{}时可以接收任何类型的参数,包括自定义类型

1.25(1.26 1.28 2.1)
var a string = nil

//error golang 的字符串类型不能赋值nil,也不能和nl比较
//string 的零值是“”

defer只有在return前面才会执行

切片是从0开始的

s2 :=s1[1:]//从s1第二个元素开始切,直到后面没有元素
s2 = append(s2,5,6,7)//append会导致底层数组扩容,生成新的数组,此时的s2成为一个新数组
func main() {
 if a := 1; false {
 } else if b := 2; false {
 } else {
 println(a, b)
 }
}

这段代码可以转成

func main() {
    {
        a := 1
        if false {

        } else {
            {
                b := 2
                if false {

                } else {
println(a,b)
}
}
}

map的输出是无序的!!!!

func main(){
    m:map[int]string{0:"zero",1,"one"}
    for k,v:range m{
        rmt.Println(k,v)
    }
}

10 1 2 3 //3

calc(“1”,a,10)

1 1 2 11 //4

20 0 2 2 //1

clac(“2”,1,4)

2 1 4 5 // 2

defer 里面的函数会优先执行(按照正常顺序)

defer calc("1",a,calc("10",a,b))//会优先执行cal("10",a,b)

基于类型创建的方法必须定义在同一个包里

func ( i int) PrintInt(){
fmt.Println()
}//这个方法必须要定义在int包里面
//普通包是运行不了的
func(stu * Student) Speak(think string)(talk string){
}
//这个是指针类型*Student实现了该方法 不是值类型Student

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当一个类型实现了某个接口的方法时,只有该类型的指针类型才会被自动视为实现了该接口

只有实现了接口方法的指针类型才会被自动视为实现了该接口。如果要将值类型赋值给接口,必须确保值类型本身也实现了接口的方法。

假设有字符 ‘a’ 和字符 ‘b’,它们的Unicode码点分别为 9798。如果你执行 'b' - 'a',实际上就是在进行 98 - 97 的整数运算,结果是 1

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.26(1.27 1.29 2.2)

iota是golang语言的常量计数器,只能在常量的表达式中使用。

iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。

package main

import "fmt"

type People interface {
    Show()
}

type Student struct{}

func (stu *Student) Show() {
    fmt.Println("Showing student information")
}

func main() {
    var s *Student

    if s == nil {
        fmt.Println("s is nil")
    } else {
        fmt.Println("s is not nil")
    }

    var p People = s
    if p == nil {
        fmt.Println("p is nil")
    } else {
        fmt.Println("p is not nil")
        p.Show()
    }
}

s is nil

结构体指针:指向Student类型的指针,但它没有被初始化,如果一个指针没有被初始化,就是nil

但如果一个结构体没有被初始化,它的所有字段都会变成零值,但是无法与nil进行比较

nil是引用类型的零值,但是结构体中的字段可以有引用类型,但还可以有字符串类型,int,这些都是具体值的类型,自然无法与nil进行比较

在 Go 语言中,即使接口内部的具体类型是 nil,接口本身也不会等于 nil

接口变量未被初始化或赋值为 nil 如果你声明一个接口变量但没有分配具体的实例给它,它的零值将是 nil。例如:

goCopy codevar myInterface MyInterface
// 此时 myInterface 是 nil

当一个接口变量被分配了一个具体的实例,无论该实例是否为 nil,接口变量本身都不会是 nil。在 Go 语言中,接口变量总是非 nil 的,只有当它未初始化或显式赋值为 nil 时才可能为 nil。p is not nil

切片

type Math struct {
 x, y int
}
var m = map[string]Math{
 "foo": Math{2, 3},
}
func main() {
    m["foo"].x = 4 //error :
    //这段代码是错误的,因为map中的值是不可寻址的,而你要访问结构体字段的话就需要知道它的地址,这是不允许的,除非它的值本身是一个结构体指针
fmt.Println(m["foo"].x)
}
/* go 中的 map 的 value 本身是不可寻址的,因为 map 的扩容的时候,可能要做 key/val pair迁移
value 本身地址是会改变的*/


//那我们只要知道被修改值的地址,就可以修改了
var m = map[string]Math{
    "foo":&Math{2,3}
}

切片是不能比较的

fmt.Println([]int{1} == []int{1})

对于使⽤ := 定义的变量,如果新变 量与同名已定义的变量不在同⼀个作⽤域中,那么 Go 会新定义这个变量

如果在函数内部声明同名的局部变量,局部变量会覆盖全局变量。 - chatgpt

当一个指针被赋值为 nil 时,它表示该指针不指向任何有效的内存地址。因此,你可以说它没有有效的地址。尝试在 nil 指针上进行解引用或取地址的操作通常会导致运行时错误。

数组与切片

var a = [5]int{1,2,3,4,5}//注意这是一个数组
var r[5]int

for i,v:=range a{
if i == 0{
a[1] = 12
a[2] =13
}
r[i] = v
}
fmt.Printf("r = ",r)
//因为我们保存的是a的副本,而更改的是a本身的值
// 输出:[1,2,3,4,5]
var a = []int{1,2,3,4,5}//注意这是一个切片
var r[5]int

for i,v:=range a{//副本的指针依然指向底层数组
if i == 0{  //所以这层是有效的
a[1] = 12
a[2] =13
}
r[i] = v
}
fmt.Printf("r = ",r)

//所以这段代码的输出是[1,12,13,4,5]

go 提供了一种直接操作指针的方式,就是unsafe.Pointer 和 uintptr

uintptr 是一个整型,可理解为是将内存地址转换成了一个整数,既然是一个整数,就可以对其做数值计算,实现指针地址的加减,也就是地址偏移,类似跟 C 语言中一样的效果。

func main() {
	a := 10
	var b *int
	b = &a

	fmt.Printf("a is %T, a=%v\n", a, a)
	fmt.Printf("b is %T, b=%v\n", b, b)

	p := unsafe.Pointer(b)
	fmt.Printf("p is %T, p=%v\n", p, p)
	
	uptr := uintptr(p)
	fmt.Printf("uptr is %T, uptr=%v\n", uptr, uptr)
}
// 输出结果
a is int, a=10
b is *int, b=0x140000ae008
p is unsafe.Pointer, p=0x140000ae008
uptr is uintptr, uptr=1374390247432
[]byte 的底层是一个指向连续内存块的指针和一个长度字段
string 的底层是一个指向 UTF-8 编码的字符串的指针和一个长度字段。
// 熟悉一下基本方法
/*
Contains() 返回是否包含子切片
Count() 子切片非重叠实例的数量
Map() 函数,将byte 转化为Unicode,然后进行替换
Repeat() 将切片复制count此,返回这个新的切片
Replace() 将切片中的一部分 替换为另外的一部分
Runes() 将 S 转化为对应的 UTF-8 编码的字节序列,并且返回对应的Unicode 切片
Join() 函数,将子字节切片连接到一起。
*/
  1. (unsafe.Pointer)(&s):这里将&s转换为unsafe.Pointer类型。这是因为unsafe.Pointer是一个通用指针类型,可以存储任何类型的指针。在Go语言中,将一个具体类型的指针转换为通用指针类型时,需要使用括号来明确转换的范围。
  2. (*byte)(p)(*int64)(p):这里将通用指针p转换为*byte*int64类型的指针。同样,括号用于明确将p视为一个指针类型,而不是其他表达式的一部分。

总的来说,括号在这里的作用是为了确保类型转换的正确性,明确指定要转换的内容,避免歧义。这是Go语言中强制要求的语法规范

import (
	"fmt"
	"unsafe"
)

func main() {
	s := struct {
		a byte
		b byte
		c byte
		d int64
	}{a: 0, b: 0, c: 0, d: 0}

	//将结构体指针转换为通用指针
	p := unsafe.Pointer(&s)
	//保存结构体的地址备用
	up0 := uintptr(p)
	//将通用指针转换为byte类型指针
	pb := (*byte)(p)
	//给转换后的指针赋值
	*pb = 10
	//结构体内容跟着变
	fmt.Println(s)
	//仅仅改变了第一个值(一个指针只能指向一个地址)

	//我们让他偏移到第二个字段
	/*func Offsetof(x任意类型) uintptr
	Offsetof 返回 x 表示的字段在结构体中的偏移量,
	该字段必须采用 structValue.field 形式。换句话说,它返回结构体开头和字段开头之间的字节数。*/
	up := up0 + unsafe.Offsetof(s.b)
	//将片以后的指针转为通用指针
	p = unsafe.Pointer(up)
	pb = (*byte)(p)

	*pb = 20

	fmt.Println(s)

	up = up0 + unsafe.Offsetof(s.c)
	//注意我们的up是个地址值
	p = unsafe.Pointer(up)

	pb = (*byte)(p)
	*pb = 30
	fmt.Println(s)

	up = up0 + unsafe.Offsetof(s.d)
	p = unsafe.Pointer(up)
	pi := (*int64)(p)
	*pi = 40
	fmt.Println(s)

}

一旦数组被声明了,那么它的数据类型跟长度都不能再被改变。
var t *T
t = new(T)

上面简单的管用语句方法t := new(T),变量 t 是一个指向 T 的指针,此时结构体字段的值是它们所属类型的零值。

声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型T。在这两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)。

  • len(Array) 数组的元素个数
  • len(*Array) 数组指针中的元素个数,如果入参为nil则返回0
  • len(Slice) 数组切片中元素个数,如果入参为nil则返回0
  • len(map) 字典中元素个数,如果入参为nil则返回0
  • len(Channel) Channel buffer队列中元素个数
1.27(1.28 1.30 2.3)
func change(s ...int) {
    s = append(s, 3)
}

func main() {
    slice := make([]int, 5, 5)
    slice[0] = 1
    slice[1] = 2
    change(slice...)
    fmt.Println(slice)
    change(slice[0:2]...)
    fmt.Println(slice)
}

对于第一次传入 slice...,实际上是将切片 slice 中的每个元素逐个传递给 change 函数。在 change 函数内部,这些元素被合并成一个新的切片 s。然后,append(s, 3) 操作在这个新的切片上进行,但由于切片是值传递,这个 append 操作并不影响原始的 slice

在第二次传入 slice[0:2]... 的情况下,change 函数内部没有创建新的切片。相反,change 函数直接修改了传入的切片的底层数组。因为传递的是切片的引用,所以对切片底层数组的修改会影响到原始的切片。

type Foo struct {
 bar string
}
func main() {
 s1 := []Foo{
 {"A"},
 {"B"},
 {"C"},
 }
 s2 := make([]*Foo, len(s1))
 for i, value := range s1 {
 s2[i] = &value
 }
 fmt.Println(s1[0], s1[1], s1[2])
 fmt.Println(s2[0], s2[1], s2[2])
 }

//关键是第二个
//我们说过 在for 中使用 := i和value都是重复的,所以他们地址是一样的,最后value被赋值为{"C"} 所以地址指向{C}
//实际的值就是 &{C}
func main() {
 var m = map[string]int{
 "A": 21,
 "B": 22,
 "C": 23,
 }
 counter := 0
 for k, v := range m {
 if counter == 0 {
 delete(m, "A")
 }
 counter++
 fmt.Println(k, v)
 }

在你的代码中,你在循环的第一次迭代时删除了 map 中的键 “A”。这个删除操作导致了迭代器的失效,可能会导致后续迭代的不确定行为。具体来说,range 迭代器可能会因为元素的删除而发生改变,导致遍历过程中跳过某个元素或者遍历到不存在的元素。

这就解释了为什么 counter 的值有时候为 2,有时候为 3。在一些情况下,可能由于删除操作导致迭代器失效,使得第二次迭代时直接跳过了键 “B”,从而 counter 的值为 2。在另一些情况下,可能迭代器的变化不会导致直接跳过某个键,counter 的值为 3。

协程通过channel来进行协程间的通信

go只有for关键字支持循环

Go 的 for 语句提供了一个标签(label)的功能,可以选择中断指定的循环,这是一种更高级的 break

Go 中的 for 循环不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量。

多重赋值

多重赋值分为两个步骤,有先后顺序:

计算等号左边的索引表达式和取址表达式,

接着计算等号右边的表达式; 赋值;

B.
type MyInt int
var i int = 1
var j MyInt = (MyInt)i
C.
type MyInt int
var i int = 1
var j MyInt = MyInt(i)

b合法但不道德(没有明确表明自己要表达的意思)

c合法道德,c明确表示了自己要表达的意思

switch

在go中,'break’语句通常是可选的

当匹配到一个case后,程序会自动退出switch,不需要显式添加’break’

如果要自动跳过某种情况,在case语句前使用’fallthrough‘可强制执行下一个case,这个跳过

方法

方法被 Integer 类型实现,但通过指针 *Integer 类型的实例也可以调用这个方法。Go 语言会在需要时自动进行值和指针之间的转换。

在Go中,布尔类型不支持直接的类型转换,而是通过使用布尔运算或比较运算符来实现。

语句是一段执行任务的代码,而表达式是一个计算值的代码片段。在Go中,大部分语句都包含一个或多个表达式。

自增自减

⾃增⾃减操作。i++ 和 i-- 在 Go 语⾔中是语句,不是表达式,因此不能 赋值给另外的变量。此外没有 ++i 和 --i

1.28(1.29 1.31 2.4)

函数声明

//这个是对的
func f(a,b int)(value int,err error)
//错误类型
func f(a,b int)(value int,error)//这里的error也要有变量名

切片

make([]T, length, capacity)

其中:

  • T 是切片的元素类型。
  • length 是切片的长度,表示切片中元素的数量。
  • capacity 是切片的容量,表示底层数组的大小,即切片可以扩展的最大长度。

map

在函数间传递 map 不是传递 map 的拷贝。所以如果我们在函数中改变了 map,那么所有引用 map 的地方都会改变

“别名类型,可以是中文!”

type (
  byte int8
  rune init32
  文本 string
)
var b 文本
b = "别名类型,可以是中文!"
package main
import "unsafe"
// 常量可以用len(), cap(), unsafe.Sizeof()常量计算表达式的值。
// 常量表达式中,函数必须是内置函数,否则编译不过:
const (
  a = "abc"
  b = len(a)
  c = unsafe.Sizeof(a)
)


如果你希望全局变量在包外部可见,其首字母应该大写;如果你希望全局变量在包外部不可见,其首字母应该小写。

//这种不带声明格式的只能在函数体中出现
//g, h := 123, "hello"

在 if 的便捷语句定义的变量同样可以在任何对应的 else 块中使用。

switch 的执行顺序: 条件从上到下的执行,当匹配成功的时候停止。自动配有break

defer 语句会延迟函数的执行直到上层函数返回。 延迟调用的参数会立刻生成,但是在上层函数返回前函数都不会被调用。

延迟的函数调用被压入一个栈中。当函数返回时, 会按照后进先出的顺序调用被延迟的函数调用。

如果没有重新命名,那么对于编译器来说,这两个fmt它是区分不清楚的。重命名也很简单,在我们导入的时候,在包名的左侧,起一个新的包名就可以了。

package main

import (
	"fmt"
	myfmt "mylib/fmt" //命名导入
)

func main() {
	fmt.Println()
	myfmt.Println()
}

但是有时候,我们需要导入一个包,但是又不使用它,按照规则,这是不行的,为此Go语言给我们提供了一个空白标志符_,只需要我们使用_重命名我们导入的包就可以了

package main

import (
	_ "mylib/fmt"
)

包的init函数

每个包都可以有任意多个init函数,这些init函数都会在main函数之前执行。init函数通常用来做初始化变量、设置包或者其他需要在程序执行前的引导工作。

1.30(1.31 2.2 2.6)

1.31 已过

go的并发是指让某个函数独立于其他函数运行的能力,一个goroutine就是一个独立的工作单元,Go的runtime(运行时)会在逻辑处理器上调度这些goroutine来运行

概念说明
进程一个程序对应一个独立程序空间
线程一个执行空间,一个进程可以有多个线程
逻辑处理器执行创建的goroutine,绑定一个线程
调度器Go运行时中的,分配goroutine给不同的逻辑处理器
全局运行队列所有刚创建的goroutine都会放到这里
本地运行队列逻辑处理器的goroutine队列

并行是做很多事情

并发是同时管理很多事情

而因为 操作系统和硬件的总资源比较少,所以并发的效果要比并行好的多

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(2) // 计数的信号量,main函数等待两个 goroutine执行完成再结束
	go func() {
		defer wg.Done()
		for i := 1; i < 100; i++ {
			fmt.Println("A:", i)
		}

	}()
	go func() {
		defer wg.Done()
		for i := 1; i < 100; i++ {
			fmt.Println("B:", i)
		}
	}()
	wg.Wait()
}

当创建一个goroutine后,会先存放在全局运行队列中,等待Go运行的调度器进行调度,把他们分配给其中的一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中,最终等着被逻辑处理器执行

我们对于同一个资源的读写必须是原子化的,也就是说同一时间只能有一个 goroutine对共享资源进行读写操作

func incCount() {
	defer wg.Done()
	for i := 0; i < 2; i++ {
		value := atomic.LoadInt32(&count)
		//atomic 包里面有很多原子性的操作
		//这个是读取int32类型的值
		runtime.Gosched() //让当前的gour
		value++
		atomic.StoreInt32(&count,value)
	    //这个是修改int32类型的值
	}
}

sync包中提供了互斥锁,他能提供一种互斥型的锁,可以让我们灵活的控制那些代码,同时只能有一个goroutinue访问,被sync互斥锁控制的这u但代码范围,被称为临界区

临界区的代码,同一时间,只能由一个goroutine访问

func incCount() {
	defer wg.Done()
	for i := 0; i < 2; i++ {
		mutex.Lock()
		//
		value := count
		runtime.Gosched()
		value++
		count = value
		//
		//临界区
		mutex.Unlock()
	}
}

Go通道

我们可以使用通道在多个goroutinue发送和接受共享的数据,达到数据同步的目的

ch := make(chan int)
ch :=make(chan int,0)//无缓冲通道

无缓冲通道的代销为0

它要求发送goroutine和接收goroutine同时准备好,才可以完成发送和接收操作。

管道

package main

import "fmt"

func main() {
    one := make(chan int)
    two := make(chan int)

    go func() {
       one <- 100
    }()

    go func() {
       v := <-one
       two <- v
    }()
    fmt.Println(<-two)
}
 var send chan<- int//只能发送
 var receive <-chan  int //只能接收
2.1(2.2 2.5 2.8)

控制某个程序是否终止可以使用complete无缓冲通道(同步)

package Runner

import (
	"errors"
	"os"
	"os/signal"
	"time"
)

type Runner struct {
	tasks     []func(int)      //要执行的任务
	complete  chan error       //用于通知任务全部完成
	timeout   <-chan time.Time //这些任务在多久内完成
	interrupt chan os.Signal   // 可以控制强制终止的信号

}

/*
我们定义一个结构体类型Runner
其中complete用于通知任务全部完成
不过有时间限制,就是timeout,他只能接收
complete定义为错误的类型,是因为当任务没有完成的时候
我们知道为什么错,如果没有错误,返回nil
*/

// 定义一个工厂函数 ,tm是超时时间
// tm会被传递给time.After函数,在tm后,会同伙一个time.Time类型的只能接收的单项通道来告诉我们已经到时间了
// complete是一个同步通道,因为我们要使用它来控制我们的整个程序是否终止
//interrupt 是一个有缓冲的通道,这样我们至少可以接收到一个操作系统的终端信息
//如果是无缓冲的通道就会阻塞

func New(tm time.Duration) *Runner {
	return &Runner{
		complete:  make(chan error),
		timeout:   time.After(tm),
		interrupt: make(chan os.Signal, 1),
	}
}

// 通过方法给执行者添加需要执行的任务
func (r *Runner) Add(tasks ...func(int)) {
	r.tasks = append(r.tasks, tasks...)
}

// 定义两个错误变量
var ErrTimeOut = errors.New("执行者执行超时")
var ErrInterrupt = errors.New("执行者被中断")

//执行任务,执行的过程中接收道中断信息时,返回中断错误
//如果任务全部执行完,还没有接收到中断信号

func (r *Runner) run() error {
	for id, task := range r.tasks {
		if r.isInterrupt() {
			return ErrInterrupt
		}
		task(id)
	}
	return nil
}

//检查是否收到中断信号
/*
select和switch很像,
只不过它的每个case都是一个通信操作
如果没有default,select会阻塞
*/

func (r *Runner) isInterrupt() bool {
	select {
	case <-r.interrupt:
		signal.Stop(r.interrupt)
		return true
	default:
		return false
	}
}

//开始执行我们的任务,并且监视通道事件

func (r *Runner) Start() error {
	//希望接收哪些系统信号
	//如果由系统中断的信号,发给r.Interrupt
	signal.Notify(r.interrupt, os.Interrupt)

	go func() {
		r.complete <- r.run()
	}()

	select { //哪个通道可以操作就返回哪一个
	case err := <-r.complete:
		return err
	case <-r.timeout:

		return ErrTimeOut
	}
}

package main

import (
	common "awesomeProject2/Runner"
	"log"
	"os"
	"time"
)

func main() {
	log.Println("...开始执行任务...")

	timeout := 3 * time.Second
	r := common.New(timeout)

	r.Add(createTask(), createTask(), createTask())

	if err := r.Start(); err != nil {
		switch err {
		case common.ErrTimeOut:
			log.Println(err)
			os.Exit(1)//这个是代码超出了我们规定的时间,返回代码为1
		case common.ErrInterrupt:
			log.Println(err)
			os.Exit(2)//这个时系统退出的代码,我们可以通过键盘发出
		}
	}
	log.Println("...任务执行结束...")
}

func createTask() func(int) {
	return func(id int) {
		log.Printf("正在执行任务%d", id)
		time.Sleep(time.Duration(id) * time.Second)
	}
}

当你关闭一个通道时,所有还在等待接收数据的 goroutine 会收到一个零值和一个表示通道已关闭的标志。这允许接收方在没有额外同步的情况下知道通道已关闭。

package main

import (
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	ready := make(chan struct{})

	for i := 0; i < 3; i++ { //通过循环常见三个并发的运动员
		wg.Add(1)
        //表示每个运动员开始将waitGroup的计数加1
			//表示有一个并发任务需要等待
		go func(id int) {
	   defer wg.Done()        //当运动员完成任务,将wg的计数减去	
            println(id, ":ready.") //输出运动员的准备状态
			<-ready                //运动员在这里阻塞,等待发令的信息
		println(id, "running...")
		}(i)
	}
	time.Sleep(time.Second) //主线程等待一秒
	println("Ready?Go!")    //输出发令口号
	close(ready)            //关闭通道,向所有运动员发送发令信号,接触阻塞状态
	wg.Wait()               //等待所有运动员(goroutine)完成任务
}

向已关闭通道发送数据,引发panic
从已关闭接收数据,返回已缓冲数据或零值 【The Valid】
无论收发,nil通道都会阻塞

package main

/*
向已关闭通道发送数据,引发panic
从已关闭接收数据,返回已缓冲数据或零值 【The Valid】
无论收发,nil通道都会阻塞
*/
func main() {
	c := make(chan int, 3)

	c <- 10
	c <- 20
	close(c)

	for i := 0; i < cap(c)+1; i++ { //cap是通道设定的值,不管关闭与否,这个都不会改变
		x, ok := <-c
		println(i, ":", ok, x)
	}
}

一旦我们设定了通道的方向,就不会再改变了

select语句

随机选择一个可用通道作为手法操作

2.8

反射

反射让哦我们能在运行期探知对象的类型信息和内存结构

反射操作所需的全部信息都源自接口变量。接口变量除了存储自身类型外,还会保存实际对象的类型数据

func TypeOf(i interface{}) Type

将任何传入的对象转化为接口类型

我们可以通过reflect类来获取实际对象获取类型

package main

import (
	"fmt"
	"reflect"
)

/*
面对类型的时候,需要区分kind和type。前者是底层类型,后者是真实类型(静态类型)
*/
type X int
type Y int

func main() {
	var a, b X = 100, 200
	var c Y = 300

	ta, tb, tc := reflect.TypeOf(a), reflect.TypeOf(b), reflect.TypeOf(c)

	fmt.Println(ta == tb, ta == tc)
	fmt.Println(ta.Kind() == tc.Kind())
}

func main() {
	a := reflect.ArrayOf(10, reflect.TypeOf(byte(0)))
	m := reflect.MapOf(reflect.TypeOf(""), reflect.TypeOf(0))

    fmt.Println(a, m)
}
//通过reflect来构建一些基础复合类型

方法Elem返回指针、数组、切片、字典或通道的基类型

2.9
package main

import (
    "fmt"
    "reflect"
)

type user struct {
    name string
    age  int
}

type manager struct {
    user  //这就是我们的匿名字段
    title string
}

/*
只有获取结构体指针的基类型后,才能遍历它的字段
基类型:reflect.Elem()
*/
func main() {
    var m manager
    t := reflect.TypeOf(&m) //使用反射获取m变量的类型信息,并将其存储在变量t
    if t.Kind() == reflect.Ptr {
       //获取指针的基类型
       t = t.Elem()
    }
    for i := 0; i < t.NumField(); i++ {
       f := t.Field(i)                       //获得结构体的第i个 字段信息
       fmt.Println(f.Name, f.Type, f.Offset) //打印字段的名称,类型和偏移量

       if f.Anonymous {
          //如果访问的是匿名字段 --user
          for x := 0; x < f.Type.NumField(); x++ {
             af := f.Type.Field(x)
             fmt.Println(" ", af.Name, af.Type)
          }
       }
    }
}
package main

import (
	"fmt"
	"reflect"
)

type user struct {
	name string
	age  int
}

type manager struct {
	user  //这就是我们的匿名字段
	title string
}

/*
只有获取结构体指针的基类型后,才能遍历它的字段
基类型:reflect.Elem()
*/
func main() {
	var m manager
	t := reflect.TypeOf(m) //使用反射获取m变量的类型信息,并将其存储在变量t
	name, _ := t.FieldByName("name")
	//按名称寻找
	fmt.Println(name.Name, name.Type)
	age := t.FieldByIndex([]int{0, 1})
	//按照多级索引查找
	fmt.Println(age.Name, age.Type)
}

说一下互斥

如果一个数据永远也不会被修改,那么其实不存在资源竞争的问题.

所以互斥应该是读取和修改,修改和修改之间

2.16
package main

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

var wg sync.WaitGroup

// 使全部goroutine执行完的,类似于计数器的东西

func worker(ch <-chan bool) {
	defer wg.Done()
LABLE:
	for {
		select {
		case <-ch:
			break LABLE
		default:
			fmt.Println("work...")
			time.Sleep(time.Second)
		}

	} //从外部程序使这个gouroutine终止
	/*
	      LABLE: 是一个标签(Label),它通常用于循环或 switch 语句中,允许在多层嵌套的控制流结构中引用特定的位置。

	      在你提供的代码中,LABLE: 标签用于 for 循环外部。
	   这个标签被命名为 LABLE,但它可以是任何有效的标识符。
	   当 select 语句中的某个 case 条件满足时,通过 break LABLE,循环将被中断,
	   并且执行会跳转到 LABLE 标签之后的代码。
	*/
}
func main() {
	exitChan := make(chan bool)
	wg.Add(1)
	exitChan <- true
	go worker(exitChan)
	//如果缓冲区没有,就一定需要另一边的管道来接收,否则阻塞之后,worker不能再接收信息,这段代码有错误
	wg.Wait()
	fmt.Println("over")
}

func worker(ch chan <-struct{}) {
	defer wg.Done()
	for {
		fmt.Println("work...")
		time.Sleep(time.Second)
		if exit == true {
			break
		}
	}
}//一般用struct作为参数,节省内存
  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值