Go语言核心编程第1章“基础知识”

该博客聚焦Go语言基础知识学习。介绍了Go语言诞生背景与特性,讲解词法单元、常量变量、基本与复合数据类型,如指针、数组、切片等,还阐述了控制结构,包括if、switch、for语句及标签跳转等内容。

学习目标:

Go语言核心编程 第一章“基础知识”

学习内容:

Go语言核心编程 第一章“基础知识”

第一章 基础知识

1.1 语言简介

1.1.1 诞生背景
  • 当前编程语言对并发支持不是很好
  • 程序规模大,编译速度慢
  • 当前编程语言设计复杂
1.1.2 Go语言特性
Go语言特性项特性值
关键字和保留字25个
控制结构支持顺序循环分支
动静特性静态语言,支持运行时动态类型
强弱特性强类型
隐式类型推导支持
类型安全类型安全
自定义数据类型支持type自定义
面向对象支持类型组合支持面向对象
多态通过接口支持
接口Duck模型
函数支持
反射支持
泛型支持
编译模式编译成可执行程序
运行模式直接运行
内存管理支持自动垃圾回收
并发编程协程(Go原生支持)
交叉编译支持
跨平台支持
框架丰富,发展很快
标准库和第三方库丰富,发展很快
语法兼容性向前兼容性好
影响力社区活跃,Google背书
应用领域云计算基础设施软件、中间件、区块链

1.2 初始Go程序

package main
import "fmt"
func main() {
	fmt.Println("Hello world")
}
编译运行命令:go build .\main.go

1.3 词法单元

1.3.1 token

token是源程序不可再分割的单元。可以分为标识符,操作符,分隔符和字面常量等

sun:=a+b
包含5个token:sum := a + b
1.3.2 标识符

总体分为两种:
1.设计者预留的标识符,包括预留的标识符以及后续语言扩展的保留字
2.用户自行定义的标识符,主要是用户在编程中自行定义的变量名,常量名,函数名等
在这里插入图片描述

1.3.3 操作运算符和分隔符

操作符具备语法含义同时起到分割token的作用,而仅起到分割作用的包括:空格,制表符,回车和换行。
47个操作符
算数运算符(5个):+ - * / %
位运算符(6个):& | ^ << >> &^
括号(6个):{ } [ ] ( )
逻辑运算符(3个):&& || !
自增自减运算符(2个):++ –
比较运算符(6个):> >= <= < == !=
赋值和赋值复核运算符(13个)
:= = += -= *= /= %= &= |= ^= >>= <<= &^=
其他运算符(6个):: , ; . … <-
注:
&^运算符
假如有两个变量 var1 &^ var2 作&^运算
如果var2变量的位为0 则取var1变量对应的位值作为结果位
如果var2变量的位为1 则结果位取0

1.3.4 字面常量

源程序中表示固定值的符号叫做字面常量,简称字面量。字面量是由字母,数字等构成的字符串或者数值,它只能作为右值出现,所谓右值是指等号右边的值。
字面量与常量的区别在于,常量是赋过值后不能再改变的变量。

const b int = 10 b为常量,10为字面量

字面量出现在两个地方
1.常量和变量的初始化
2.表达式里或者作为调用函数的实参
Go不支持用户自定义字面量,Go中的字面量包含以下5种:

1.整型字面量
42
0600
...
2.浮点型字面量
0.
72.4
...
3.复数型字面量
0i
0.i
...
4.字符型字面量
'a'
'\t'
...
5.字符串字面量
"\n"
"abc"
...

1.4 常量和变量

1.4.1 变量

使用一个名称来绑定一块内存地址,该内存地址中存放的数据类型由定义变量时指定的类型决定,该内存地址中存放的内容可以改变

1.4.1.1 变量声明方式
显式声明
var varName dataType [ = value]
var a int = 5
如果不指定初始值,则默认将该变量初始化为类型的零值
短类型声明
varName := value
:=只能出现在函数内(包括方法内)
Go编译器会自行进行数据类型推断
支持多类型变量同时声明并赋值
a,b:="hello",1
1.4.1.2 变量属性
变量名
变量值
类型信息
可见性和可见域:Go提供自动内存管理,通常无需关注生存期和存放位置。编译器用栈逃逸技术能够自动为变量分配空间,可能在栈上也可能在堆上。
变量存储和生存期:使用统一的命名空间对变量进行管理,每个变量都有唯一的名字,包名是这个名字的前缀
1.4.2 常量定义

常量定义:使用一个名称来绑定一块内存地址,该内存地址中存放的数据类型由定义变量时指定的类型决定,该内存地址中存放的内容不可以改变

1.4.2.1 Go语言常量
  • 布尔型
  • 字符串型
  • 数值型
	const flag bool = true
	const str string = "hello"
	const value int64 = 5
1.4.2.2 Go语言预声明标识符iota
标识符:是用来表示变量、函数、类、对象等程序实体的名称,由字母、数字和下划线组成
预定义标识符:预先定义的标识符,具有特殊功能含义,表示常用功能、库函数或预定义常量等。
关键字是编程语言中具有特殊含义的保留字,不能用作标识符
-------------------iota用在常量声明中,初始化为0---------------
package main
import "fmt"
const (
	c0 = iota 
	c1 = iota
	c2 = iota
)
func main() {
	fmt.Println(c0) //0
	fmt.Println(c1) //1
	fmt.Println(c2) //2
}
--------------------------可以进行简化-------------------------
package main
import "fmt"
const (
	c0 = iota 
	c1 
	c2 
)
func main() {
	fmt.Println(c0) //0
	fmt.Println(c1) //1
	fmt.Println(c2) //2
}
-------------------分开的const每次从0开始-------------------------
package main
import "fmt"
const c0 = iota 
const c1 = itoa
func main() {
	fmt.Println(c0) //0
	fmt.Println(c1) //0
}
-------------------iota逐行递增-------------------------
package main
import "fmt"
const (
	c0 = iota 
	c1 
	c2 =iota
)
func main() {
	fmt.Println(c0) //0
	fmt.Println(c1) //1
	fmt.Println(c2) //2
}

1.5 基本数据类型

七类基本数据类型(20个具体类型)

1.布尔类型:
bool 默认为false,无法和整型进行相互转化

2.整型:
byte,int,int8,int32,int64,uint,uint8,uint16,uint32,uint64,uintptr
不同类型整型必须强制类型转化

3.浮点型:
float32,float64
字面量默认被识别为float64,var b:=10.00,浮点数间比较应该用math库

import (
    "fmt"
    "math"
)
func main() {
    a := 0.1 + 0.2
    b := 0.3
    mistake := 1e-9 // 定义一个误差范围
    if math.Abs(a-b) < mistake {
        fmt.Println("a 和 b 相等")
    } else {
        fmt.Println("a 和 b 不相等")
    }
}

4.复数:
complex64,complex128
value:= 3.1+6i,complex64由2个float32组成,complex128由2个float64组成。

 复数函数:
 var v = complex(2.1,3)//构造复数 [注:默认complex128]
 a:=real(v)//返回实部
 a:=image(v)//返回虚部

5.字符:
rune
GO中两种字符类型
一种是byte的字节类型,byte是unit的别名,1字节
另一种是Unicode编码的字符rune,rune是GO中int32的别称,4字节
[注:name:='a’默认为int32]

6.错误类型:
error
error类型是Golang内置类型之一,其本质上只是一个接口

package main

import "fmt"

type DivideError struct {
	dividea int
	divideb int
}

func (e *DivideError) Error() string {
	return fmt.Sprintf("cannot divide %d by %d", e.dividea, e.divideb)
}

func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, &DivideError{dividea: a, divideb: b}
	}
	return a / b, nil
}

func main() {
	result, err := divide(10, 0)
	if err != nil {
		if e, ok := err.(*DivideError); ok {
			fmt.Println("Divide error:", e)
			return
		}
		fmt.Println(err)
		return
	}
	fmt.Println(result)
}
断言的格式
if concreteValue, ok := interfaceValue.(ConcreteType); ok {
// 处理具体类型的逻辑
} else {
// 处理非具体类型的逻辑
}

7.字符串类型:
string
Golang中的字符串是不可变的,不能通过索引下标的方式修改字符串中的数据

func main() {
	var s string = "abc"
	s[0] = 99
}
报错信息:
# command-line-arguments
.\main.go:5:7: cannot assign to s[0] (strings are immutable)

源码分析

StringHeader is the runtime representation of a string.
Go语言内部定义的字符串结构体类型,开发者不能直接访问和使用。
type StringHeader struct {
	Data uintptr
	Len  int
}
Go语言标准库中的类型,StringHeader是一个公开可见的类型
type stringStruct struct {
	str  unsafa.Pointer
	len  int
}
	Golang字符串赋值是数组指针和长度字段的拷贝。
	多个string的值指向同一片内存区域时,内部数组指针地址相同,string地址不同
	str1 := "hello,world"
	strptr1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
	fmt.Println("strptr1 ptr:", unsafe.Pointer(strptr1.Data))
	fmt.Println("strptr1 len", strptr1.Len)
	str2 := "hello,world"
	strptr2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
	fmt.Println("strptr2 ptr:", unsafe.Pointer(strptr2.Data))
	fmt.Println("strptr2 len", strptr2.Len)
	fmt.Println("&st1", &str1)
	fmt.Println("&st2", &str2)
	strptr1 ptr: 0xcd639d
	strptr1 len 11
	strptr2 ptr: 0xcd639d
	strptr2 len 11
	&st1 0xc000088220
	&st2 0xc000088230
	------------------------------------------------------------
	改变string的值时,内部数组指针地址和长度发生变化,string地址不变
	str1 := "hello,world"
	strptr1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
	fmt.Println("before strptr1 ptr:", unsafe.Pointer(strptr1.Data))
	fmt.Println("before strptr1 len", strptr1.Len)
	fmt.Println("before &st1", &str1)
	str1 = "hello"
	strptr2 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
	fmt.Println("after strptr1 ptr:", unsafe.Pointer(strptr2.Data))
	fmt.Println("after strptr1 len", strptr2.Len)
	fmt.Println("after &st1", &str1)
	before strptr1 ptr: 0x9763af
	before strptr1 len 11
	before &st1 0xc000088220
	after strptr1 ptr: 0x9754fe
	after strptr1 len 5
	after &st1 0xc000088220
	string[]byte的互相转换
	var str = "ab"
	bytes := []byte(str)//要慎用,尤其数据量大时,每次转化都需要复制内容
	bytes[1] = 'c'
	strcov := string(bytes)
	fmt.Println(str)
	fmt.Println(strcov)
	ab
	ac
1.5.1 Go语言和Java语言数据类型对比
GO			JAVA
int8	    byte
int16		short
int32	    int
int64		long
float32	    float
float64		double
Go中byte=uint,rune=int32

1.6 复合数据类型

1.6.1 指针

Go语言支持指针,指针是一种用于存储变量内存地址的特殊类型。指针允许你间接访问变量的值和修改变量的内容。指针类型的声明为*T,同时Go支持多级指针**T。通过变量名之前加&获取变量的地址。

1.6.1.1 指针的操作
1.创建指针:你可以使用&运算符来获取一个变量的地址
var x int = 42
var ptr *int = &x
2.获取指针的值
fmt.Println(*ptr) // 输出:42
修改指针指向的值
*ptr = 100
fmt.Println(x) // 输出:100
3.传递指针给函数
func main() {
	var value int = 150
	modifyValue(&value)
	fmt.Println(value) // 输出:200
}
func modifyValue(ptr *int) {
	*ptr = 200
}
4.返回指针,Go编译器使用"栈逃逸"将这种局部变量分配在堆上
func createPointer() *int {
    value := 42
    return &value
}
ptr := createPointer()
fmt.Println(*ptr) // 输出:42
1.6.1.2 指针的特点

1.在赋值语句中,*T出现在"=“左边表示指针声明,出现在”="右边表示取指针指向的值

var a = 11
p := &a

2.Go不支持指针运算
Go由于支持垃圾回收,如果支持指针运算,会给垃圾回收的实现带来诸多不便

a := 123
p := &a
p++ //不允许,报non-numeric type +int错误

3.结构体指针访问结构体字段仍然使用"."操作符

package main
import "fmt"
// 定义一个结构体
type Person struct {
    FirstName string
    LastName  string
    Age       int
}
func main() {
    // 创建一个结构体指针
    p1 := &Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }
    // 访问结构体字段,使用"."操作符
    fmt.Println("First Name:", p1.FirstName)
    fmt.Println("Last Name:", p1.LastName)
    fmt.Println("Age:", p1.Age)
}
1.6.1.2 空指针

指针可以是空的,表示未指向任何有效的内存地址。在Go中,空指针的零值是nil。

var ptr *int // 这是一个空指针
if ptr == nil {
    fmt.Println("ptr is nil")
}
1.6.2 数组

[n]elementType
数组一般在创建时通过字面量进行初始化,单独声明一个数组类型变量而不进行初始化无意义

1.6.2.1 数组特点

1.数组创建完成长度就固定了,无法新增加元素
2.数组长度是数组类型的组成部分,[10]int和[20]int表示不同类型
3.可以根据数组创建切片
4.数组是值类型,数组赋值或者作为函数参数都是值拷贝。
数组赋值:当你将一个数组分配给另一个数组时,不会共享相同的内存位置。相反,数组中的元素会被复制到新数组中,创建了两个独立的数组,它们在内存中有不同的位置。这意味着对一个数组所做的更改不会影响到另一个数组。
函数参数传递:当你将一个数组作为函数参数传递时,也会发生值拷贝。这意味着在函数内部对数组的修改不会影响到原始数组。

4func main() {
	array1 := [3]int{1, 2, 3}
	array2 := array1 // 这不是引用,而是值拷贝
	array2[0] = 100
	fmt.Println(array1[0]) // 仍然是1,array1不受array2的影响
}
-------------------------------------------------------------
func ModifyArray(arr [3]int) {
    arr[0] = 100
}
originalArray := [3]int{1, 2, 3}
ModifyArray(originalArray)
fmt.Println(originalArray[0]) // 输出为1,原始数组不受函数内部修改的影响
1.6.2.2 数组初始化
a := [3]int{1,2,3} //指定长度和初始化字面量
a := [...]int{1,2,3} //不指定长度,但是由后面的初始化列表数量来确定其长度
a := [3]int{1:1,2:3} //指定长度和利用索引下标进行初始化,未指定时按照默认值初始化
a := [...]int{1:1,2:3} //不指定长度,利用索引下标进行初始化,数组长度为最后一个索引值决定
1.6.2.3 数组相关操作

1.数组元素访问

a := [3]int{1,2,3}
b := a[0]
for k,v :=range a{
}

2.数组长度

a := [3]int{1,2,3}
length := len(a)
1.6.3 切片
1.6.3.1 切片和数组

Go语言数组的定长性和值拷贝限制了其使用场景,Go提供了另一种数据类型slice,是一种变长数组,其数据结构中有指向数组的指针,所以是一种引用类型。

type slice struct{
	array unsafe.Pointer //指向底层数组
	len int //切片元素的数量
	cap int //底层数组的容量
}
1.6.3.2 切片的创建

1.由数组创建

var array = [...]int{0,1,2,3,4,5,6}
s1 := array[0:4]
fmt.Printf("%v\n",s1) //[0 1 2 3]

2.通过内置函数make创建切片,默认被初始化为切片元素类型的零值

a := make([]int, 10)//len10 cap10
b := make([]int, 10, 15)//len10 cap15
注意:直接声明切片类型的变量没有意义
var a []int
1.6.3.3 切片的操作

1.len()
2.cap()
3.append
4.copy()

slice := []int{1, 2, 3, 4, 5}
length := len(slice)
fmt.Println(length) // 输出:5
-------------------------------------
slice := []int{1, 2, 3, 4, 5}
capacity := cap(slice)
fmt.Println(capacity) // 输出:5
-------------------------------------
slice := []int{1, 2, 3}
slice = append(slice, 4, 5)
fmt.Println(slice) // 输出:[1 2 3 4 5]
-------------------------------------
//copy 不共享底层数数组
slice1 := []int{1, 2, 3}
slice2 := make([]int, len(slice1))
copy(slice2, slice1)
slice2[0] = 5
fmt.Println(slice2) // 输出:[5 2 3]
fmt.Println(slice1) // 输出:[1 2 3]
1.6.4 map

Go语言中map类型的格式为map[K]T,是一种引用类型

1.6.4.1 map的创建
// 使用make创建一个空的map
m := make(map[string]int) //map的容量使用默认值
m := make(map[string]intlen) //map的容量使用给定的len值

// 使用字面量初始化map
m := map[string]int{
    "apple": 5,
    "banana": 3,
}
1.6.4.2 map的操作
1.使用键向map添加元素,如果键已经存在,将会更新对应的值。
m["orange"] = 7
2.通过键获取map的值
value := m["banana"] // 获取键"banana"对应的值
3.通过delete函数删除map的值
delete(m, "apple") // 删除键"apple"对应的键值对
4.检查键是否存在
value, exists := m["pear"]
if exists {
    fmt.Println("pear exists with value", value)
} else {
    fmt.Println("pear does not exist")
}
5.使用for range遍历map
for key, value := range m {
    fmt.Println(key, value)
}
6.map的零值:如果一个map没有初始化,它的零值是nil,即一个空的map
var m map[string]int
if m == nil {
    fmt.Println("map is nil")
}
7.map是无序的:map中的键值对没有固定的顺序,每次迭代可能以不同的顺序访问键值对
8.map的值可以是任何类型,但是map的键必须是可以进行相等性比较的类型
9.map键值对的个数通过len()获得
fmt.Println(len(map))

注:Go内置的map不是并发安全的,并发安全的map可以使用标准包sync中的map
不要直接修改map value内某个元素的值,如果想修改map的某个键值必须整体赋值

// 不要直接修改 map 值内的元素
m := map[string][]int{
    "a": {1, 2, 3},
    "b": {4, 5, 6},
}
// 以下代码是不允许的,会导致编译错误
// m["a"][0] = 100
// 要修改 map 中的某个键值对,必须重新分配整个键值对
m["a"] = []int{100, 2, 3}
// 现在 map 中的键"a"对应的值被修改了
fmt.Println(m["a"]) // 输出:[100 2 3]
1.6.5 struct
1.6.5.1 struct特点

1.struct结构中的类型可以是任意类型
2.struct的存储空间是连续的,其字段按照声明时的顺序存放

1.6.5.2 struct形式

struct的两种形式:
1.struct类型的字面量

struct {
	FeildName FeildType
	FeildName FeildType
	FeildName FeildType
}

2.自定义的struct类型

type TypeName struct {
	FeildName FeildType
	FeildName FeildType
	FeildName FeildType
}
1.6.5.3 struct初始化
type Person struct{
 	Name string
 	Age  int
}
type Student struct{
	*Person
	Number int
}
不推荐
a:=Person{"Tom",12}
推荐
p := &Person{
	Name : "dada",
	age : 15,
}
s := Student{
	Person : p,
	Number : 110,
}
1.6.6 接口

第四章介绍

1.6.7 通道

第五章介绍

1.7 控制结构

1.7.1 if语句

Go没有条件运算符 a>b?a:b

err,file := os.Open("xxx")
if err != nil {
	return nil,err
}
defer file.Close()
//do something
1.7.2 switch语句

1.switch支持default语句,当所有case分支都不满足时,执行default语句,并且default语句可以放到任意位置,不影响switch的判断逻辑
2.通过fallthrough语句强制执行下个case子句,不用判断下一个case子句能否满足
3.switch后面可以跟一个可选的简单的初始化语句

1点的演示
package main
import "fmt"
func main() {
	switch i := "x"; i {
	default:
		fmt.Printf("default")
	case "y", "Y":
		fmt.Println("yes")
	case "n", "N":
		fmt.Println("no")
	}
}
default
---------------------------------------------2点和第3点的演示
package main
import "fmt"
func main() {
	switch i:="y"; i{
	  case "y","Y":
	  	fmt.Println("yes")
	  	fallthrough
	  case "n","N":
	  	fmt.Println("no")
	}
}
yes
no
1.7.3 for语句

第一种:死循环,类似while(1)

for{}

第二种:类似于while(condition)

for condition{}

第三种:正常for循环语句

for init;condition;post{}

第四种:对数组、切片、字符串、map和chanel的访问

访问数组
for index,value := range arry{}
访问切片
for index,value := range slice{}
访问通道
for value := range chanel{}
访问字符串
for index,value := range string
访问map
for key,value := range map{}
1.7.4 标签和跳转
1.7.4.1 标签

标签:用来标示一个语句的位置。
基本语法 labelName:

代码示例:
package main
func main() {
	i := 1
start:
	fmt.Print(i)
	i++
	if i <= 5 {
		goto start
	}
}
12345
1.7.4.2 goto语句

goto用于函数内部的跳转,需要配合标签一起使用
基本语法 goto Label
特点:
1.只能函数内部使用。
2.不能跳过内部变量声明语句。
3.只能跳到上级作用域或者同级作用域,不能跳到内部作用域。

1.只能函数内部使用。
正确用法:
func main() {
	goto outside
outside:
	fmt.Println("This is outside the function.")
}
-----------------------------------------------------
错误语法:不能跨函数使用
func toexample()  {
	goto outside
}
func example() {
outside:
	fmt.Println("This is outside the function.")
}
2.不能跳过内部变量声明语句。
package main
import "fmt"
func main() {
    i := 1
    goto skipDeclaration // 错误:不能跳过变量声明语句
    j := 2
skipDeclaration:
    fmt.Printf("i: %d, j: %d\n", i, j)
}
3.只能跳到上级作用域或者同级作用域,不能跳到内部作用域。
package main
import "fmt"
func main() {
    i := 1
inner:
    j := 2
    fmt.Printf("i: %d, j: %d\n", i, j)
    return
outer:
    i = 10 
    goto inner // 错误:不能跳到内部作用域,j := 2仅在inner中生效,因此报错
}
1.7.4.3 break语句

两种用法
1.单独使用
2.结合标签使用,跳出标识符所标示的for、switch和select语句的执行,同时要求标签必须和break在同一个函数内。

package main
import "fmt"
func main() {
outerLoop:
	for i := 1; i <= 3; i++ {
		fmt.Printf("Outer loop: %d\n", i)
		for j := 1; j <= 3; j++ {
			fmt.Printf("Print j: %d\n", j)
			if j == 2 {
				break outerLoop // 使用 break + 标签 中断外部循环
			}
		}
	}
}
Outer loop: 1
Print j: 1
Print j: 2
1.7.4.4 continue语句

两种用法
1.单独使用
2.结合标签使用,跳出标识符所标示的fo语句的本次执行,要求标签必须和continue在同一个函数内。

package main
import "fmt"
func main() {
outerLoop:
	for i := 1; i <= 3; i++ {
		fmt.Printf("Outer loop: %d\n", i)
		for j := 1; j <= 3; j++ {
			fmt.Printf("Print j: %d\n", j)
			if j == 2 {
				continue outerLoop // 使用 break + 标签 中断外部循环
			}
		}
	}
}
Outer loop: 1
Print j: 1
Print j: 2
Outer loop: 2
Print j: 1
Print j: 2
Outer loop: 3
Print j: 1
Print j: 2
1.7.4.5 return和函数调用

return语句和函数调用也能引起程序控制流的跳转
return 语句用于从函数中提前返回,并可以选择返回一个或多个值。当执行 return 语句时,函数的执行将立即终止,控制流将返回到函数的调用点,同时传递指定的返回值。

func add(a, b int) int {
    result := a + b
    return result
}

当一个函数在 Go 中被调用时,控制流会从调用点跳转到被调用函数内部执行。一旦被调用函数执行完毕,控制流将返回到调用函数的地方,继续执行调用函数的剩余部分。

func main() {
    fmt.Println("Start of main")
    someOtherFunction()
    fmt.Println("End of main")
}
func someOtherFunction() {
    fmt.Println("Inside someOtherFunction")
}

参考书籍:Go语言核心编程

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值