项目代码地址:03-ArraySliceMapFuncStruct
基本格式:var 数组变量名 [元素数量]T
...
自动推导第一维数组长度// Array 1
func function01() {
const lenA = 2
var a [lenA + 1]int // 常量表达式定义数组长度
// var lenB = 3
// b := [lenB]int{} 错误!变量不可用于定义数组长度
var b = [1]bool{true} // 基本初始化
var c [2]string = [...]string{"A", "B"} // 省略号自行推断数组长度
d := [...]int{1: -1, 3: 5} // 索引值初始化
fmt.Println(a, b, c, d) // [0 0 0] [true] [A B] [0 -1 0 5]
// 第一维可自动推导长度
citys := [...][3]string{
{"北京", "上海", "广州"},
{"重庆", "湖南", "武汉"},
}
fmt.Println(citys) // [[北京 上海 广州] [重庆 湖南 武汉]]
// 二维数组遍历
for _, v1 := range citys {
for _, city := range v1 {
fmt.Print(city)
}
} // 北京上海广州重庆湖南武汉
}
// Array 2
func function02() {
a := [...]int{1, 2}
b := a
b[0] = 3
fmt.Println(a, b) // [1 2] [3 2]
for i, length := 0, len(a); i < length; i++ {
fmt.Printf("%v ", a[i])
} // 1 2
fmt.Println()
// 同类型且内部元素可比较,那么可以使用 == 和 != 比较两个数组
fmt.Printf("%#v %#v %T %T %t %t\n", a, b, a, b, b == a, b != a) // [2]int{1, 2} [2]int{3, 2} [2]int [2]int false true
}
基本格式:var 切片变量名 []T
切片表达式:a[low: high: max]
,满足 0 <= low <= high <= max <= cap(a)
,仅 low 可以省略(默认 0)
切片不同于数组,切片长度不固定,可以通过 append
向切片动态添加元素,并且会自动扩容。切片是一个引用类型,可以基于字符串、数组、切片及其指针,通过切片表达式得到切片。
// Slice 1
func function03() {
a := [5]int{1, 2, 3, 4: -1} // 1 2 3 0 -1
strA := "Hello Golang"
ptrStrA := new(string)
ptrStrA = &strA
sliceA := a[1:5] // 基于数组
sliceStrA := strA[6:] // 基于字符串
slicePtrStrA := (*ptrStrA)[:5] // 基于字符串指针
fmt.Println(sliceA, sliceStrA, slicePtrStrA) // [2 3 0 -1] Golang Hello
sliceB := sliceA[2:] // 基于切片
fmt.Println(sliceB) // [0 -1]
sliceB[0] = -2 // 引用类型
fmt.Println(a, sliceA, sliceB) // [1 2 3 -2 -1] [2 3 -2 -1] [-2 -1]
}
切片表达式第三维 max
,表示当前截取出的切片的容量 cap = max - low
。即 low
是起始位置,high
是 len
长度的结尾位置后一位(左闭右开),max
是 cap
容量的结尾位置后一位。
如果想要动态创建切片,需要使用内置 make
函数:make([]T, size, cap)
使用 make 函数,可以一次性将切片所需容量申请到位,可以避免小切片的多次动态扩容。
// Slice 2
func function04() {
a := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Println(a, len(a), cap(a)) // [0 1 2 3 4 5 6 7] 8 8
sliceA1 := a[1:5]
fmt.Println(sliceA1, len(sliceA1), cap(sliceA1)) // [1 2 3 4] 4 7
sliceA2 := a[1:5:7]
fmt.Println(sliceA2, len(sliceA2), cap(sliceA2)) // [1 2 3 4] 4 6
b := make([]int, 1, 4)
b = append(b, 2, 3)
fmt.Println(b) // [0 2 3]
}
nil
切片和 empty
切片
nil 切片,不仅没大小容量,且不指向任何底层数组;
empty 切片,没大小容量,但指向一个特殊的内存地址 zerobase — 所有 0 字节分配的基地址
判断一个切片是否为空,使用 len(a) == 0
而不要直接使用 a == nil
,因为切片元素不是直接存储的值,所以也不允许切片之间使用 ==
比较,唯一合法可以和 nil
进行比较。
// Slice 3
func function05() {
var s1 []int // nil 切片,不仅没大小容量,且不指向任何底层数组
s2 := []int{}
s3 := make([]int, 0) // s2、s3 都是 empty 切片,没大小容量,但指向一个特殊的内存地址 zerobase - 所有 0 字节分配的基地址
fmt.Println(len(s1), len(s2), len(s3), cap(s1), cap(s2), cap(s3)) // 0 0 0 0 0 0
fmt.Println(s1 == nil, s2 == nil, s3 == nil) // true false false
fmt.Printf("%#v %#v %#v\n", s1, s2, s3) // []int(nil) []int{} []int{}
// fmt.Println(s1 == s2) // 切片之间不允许直接使用 == 比较,唯一可以和 nil 进行比较
}
基本格式:append(slice []Type, elems ...Type) []Type
切片在使用 append 函数后,可能会造成扩容,改变底层数组的地址,Go 编译器也不允许调用 append 函数后不使用其返回值,所以通常使用原变量接收 append 函数的返回值。
nil
切片直接使用 append
// Slice 4
func function06() {
var a []int // nil 切片
a = append(a, 2, 3, 4)
fmt.Println(a, len(a), cap(a)) // [2 3 4] 3 3
}
基本格式:copy(destSlice, srcSlice []T) int
不希望两个切片共享底层同一个数组,可以使用 copy
函数,执行深拷贝
// Slice 5
func function07() {
a := []int{1, 2, 3}
var b = make([]int, len(a), len(a))
copy(b, a) // 深拷贝
fmt.Println(a, b) // [1 2 3] [1 2 3]
b[0] = -1
fmt.Println(a, b) // [1 2 3] [-1 2 3]
}
x
元素:append(a[:x], a[x + 1:]...)
copy
来删除索引 x
元素:copy(a[x:], a[x + 1:])
,最终元素需要少一位输出// Slice 6
func function08() {
a := []int{1, 2, 3}
b := append(a[:1], a[2:]...)
fmt.Println(b) // [1 3]
copy(a[1:], a[2:])
fmt.Println(a[:len(a)-1]) // [1 3]
}
基本格式:map[KeyType]ValueType
Go 中的 map 是引用类型,并且必须初始化才能使用,不像 nil
切片能直接使用。
// Map 1
func function09() {
var a map[string]int
b := map[string]int{}
c := make(map[string]int)
fmt.Println(a == nil, b == nil, c == nil) // true false false
}
同样使用 make
函数初始化:make(map[T]T, cap)
,第二个参数表示初始化创建的 map 容量大小。
判断键值是否存在:val, ok := map[key]
// Map 2
func function10() {
b := map[string]int{
"周一": 1,
"周二": 2,
}
b["周三"] = 3
// 遍历 map
for k, v := range b {
println(k, v)
}
// 取值第二个返回值,表示是否存在该元素
if v, ok := b["周二"]; ok {
fmt.Println(v)
}
}
基本格式:delete(m map[Type]Type1, key Type)
// Map 3
func function11() {
m := make(map[int]string, 4)
m[1] = "Python"
m[2] = "Go"
m[3] = "Lua"
for _, v := range m {
fmt.Println(v)
} // Python Go Lua
delete(m, 2)
for _, v := range m {
fmt.Println(v)
} // Python Lua
}
基本格式:
func 函数名(参数)(返回值) {
函数体
}
Go 语言中支持函数、匿名函数和闭包,并且函数在 Go 语言中属于“一等公民”。
// 多返回值
func function13(a int, b int) (int, int) {
return b, a
}
// 返回值命名
func function12(x, y int) (ret int) {
ret = x + y
return
}
// 变参函数
func function14(nums ...int) int {
sum := 0
for _, v := range nums {
sum = sum + v
}
return sum
}
变参传递:
func function14_1() {
ret := function14([]int{1, 2, 3}...)
fmt.Print(ret) // 6
}
// 函数参数
func function15(a, b int, f func(int, int) int) {
f(a, b)
return
}
// 函数变量
func function16() {
add := func(a, b int) int {
return a + b
}
function15(1, 2, add)
}
// 函数返回值
func function17() func(int) int {
y := 1
return func(x int) int {
return x + y
}
}
闭包也可以是递归的,但要求在定义闭包之前用类型化的 var 显式声明闭包。
// 递归
func function20() {
var dfs func(int) int
dfs = func(x int) int {
if x > 3 {
return 3
}
return x + dfs(x+1)
}
ret := dfs(1)
fmt.Print(ret) // 1+2+3+3 == 9
}
程序启动时自动执行每个包的 init
函数
func init() {
fmt.Println("main init")
}
Go 语言中的 defer
语句会将其后面跟随的语句进行延迟处理。
在 defer
归属的函数即将返回时,将延迟处理的语句按 defer
定义的逆序进行执行,也就是说,先被 defer
的语句最后被执行,最后被 defer
的语句,最先被执行。
// defer
func function18() {
f1 := func() {
fmt.Print(1)
}
f2 := func() {
defer f1()
fmt.Print(2)
}
defer func() {
fmt.Print(3)
defer f2()
}() // 321
return
}
Go 函数中 return
语句在底层并不是原子操作,它分为给返回值赋值和 RET 指令两步。
而 defer
语句执行的时机就在返回值赋值操作后,RET 指令执行前。如下图所示:(参考)
defer
注册要延迟执行的函数时该函数所有的参数都需要确定其值func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func function19() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
}
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4
panic 的一种常见用法是:当函数返回我们不知道如何处理(或不想处理)的错误值时,中止操作。
recover 可以阻止 panic 中止程序,并让它继续执行。
举例:
当其中一个客户端连接出现严重错误,服务器不希望崩溃。 相反,服务器希望关闭该连接并继续为其他的客户端提供服务。
注意:
defer
的函数中调用 recover
,不可以直接 defer recover()
defer
一定要在可能引发 panic
的语句之前定义// panic, recover
func function21() {
defer func() {
if err := recover(); err != nil {
fmt.Println("recover err:", err)
}
}() // recover err: function error!
func() {
panic("function error!")
}()
fmt.Println("After panic") // 不会执行
}
当跳出引发 panic 的函数时,defer 会被激活, 其中的 recover 会捕获 panic,并且 recover 的返回值是在调用 panic 时抛出的错误。
最后一行代码不会执行,因为 main 程序的执行在 panic 点停止,并在继续处理完 defer 后结束。
语法规则:type NewType SourceType
类型定义定义了全新类型,该类型与源类型底层类型相同。
语法规则:type TypeAlias = Type
类型别名和其源类型本质上同属一个类型。
// 类型定义,类型别名
func function22() {
type MyInt int // 类型定义
var a int = 1
var b MyInt
b = MyInt(a) // 必须显示转换
fmt.Printf("%T\n", b) // main.MyInt
type MyFloat = float32 // 类型别名
var A float32 = 0.1
var B MyFloat
B = A // 只是别名,本质同属一个类型
fmt.Printf("%T\n", B) // float32
}
从上述输出结果来看,类型定义是表示 main 包下定义的类型 MyInt,而类型别名仍然是源类型,类型别名只存在源代码中,编译时会自动替换为原来的类型。
基本格式:
type 类型名称 struct {
字段名称 字段类型
}
在声明结构体字段时,也可以指定标签 tag
,形如 key1:"value1" key2:"value2"
,并且需要用反引号包裹。标签信息可以在程序运行时通过反射机制被读取,比如被用于在结构体变量和 JSON 数据之间转换时,通过 tag
建立联系。
// 结构体
type Info struct {
Email string
Phone string
}
type Person struct {
ID int64 `json:"id" form:"id"` // tag 标签
Name string
Contact Info
}
type Node struct {
val int
next *Node
}
func function23() {
a := Info{
Email: "[email protected]",
Phone: "123456",
}
// 结构体简单,按顺序初始化
b := Info{
"[email protected]",
"987654",
}
fmt.Printf("%T %T\n", a, Person{}) // main.Info main.Person
fmt.Println(a, b) // {[email protected] 123456} {[email protected] 987654}
}
func function24() {
a := struct {
x, y int
}{
1, 2,
}
fmt.Println(a) // {1 2}
}
func function27() {
type Student struct {
Name string
Info // 结构体嵌套
}
var a Student
a = Student{
Name: "Cauchy",
Info: Info{
Email: "[email protected]",
Phone: "123456",
},
}
fmt.Println(a.Name, a.Email, a.Phone) // Cauchy [email protected] 123456
}
一般结构体较为复杂,则可以使用返回指针对象。
// 自定义结构体构造函数
func NewInfo() *Info {
return &Info{
Email: "[email protected]",
Phone: "123456",
}
}
func function29() {
a := NewInfo()
fmt.Println(a) // &{[email protected] 123456}
}
内存对齐同 C++ 语言类似,不过多阐述。
需要注意的是,一个空的结构体不占用内存空间,但是当一个结构体最后一个字段是空结构体,编译器会额外填充 1 字节。
func function25() {
fmt.Println(unsafe.Sizeof(struct{}{})) // 0
type structA struct {
A int8 // 1 byte
B struct{} // 0 byte
}
var a structA
fmt.Println(unsafe.Sizeof(a)) // 2
}
这是为了防止对结构体最后一个零内存占用字段进行取地址操作时发生越界,而空结构体不放最后一个字段,不会额外填充。
当然,额外填充一字节也是会进行内存对齐的,unsafe.Alignof
返回变量的对齐要求:
func function26() {
type structA struct {
A int32 // 4 byte
B struct{} // 0 byte
}
var a structA
fmt.Println(unsafe.Sizeof(a), unsafe.Alignof(a)) // 8 4
}
利用 encoding/json
包的 json.Marshal
和 json.Unmarshal
进行序列化和反序列化。
// JSON
func function28() {
type Phone struct {
Price int `json:"price"`
Name string `json:"name"`
}
s := Phone{
Price: 3888,
Name: "小米",
}
s1, _ := json.Marshal(s) // JSON 序列化
fmt.Printf("%s\n", s1) // {"price":3888,"name":"小米"}
var a Phone
_ = json.Unmarshal(s1, &a) // JSON 反序列化
fmt.Println(a) // {3888 小米}
}
上述序列化后,字段变为了 tag
定义的名称 price
和 name
。
Go 允许为特定的变量设置专用的函数,这个专用的函数被称为方法(method)。通过方法接收者(receiver)来绑定方法和对象,接收者类似于 this、self。
方法声明
func (接收者 接收者类型) 方法名(参数) (返回值) {
方法体
}
建议接收者类型名称为首字母小写
接收者类型:值接收者、指针接收者
通过结构体嵌套进行组合,实现面向对象的继承
Go 的语法糖 .
运算符,如果是指针,则会自动获取变量的地址
Go 通过内嵌其他结构体进行组合,每个被嵌入的结构体均会为其提供一些方法。当进行方法调用,会先查找当前结构体中是否声明了该方法,没有则依此去内嵌字段的方法中查找。
type Course struct {
Name string
Score int8
}
// 值接收者
func (c Course) getName() string {
return c.Name
}
// 指针接收者
func (c *Course) setScore(score int8) {
c.Score = score
}
type Math struct {
People int8
Course
}
func function30() {
m := Math{
People: 36,
Course: Course{
Name: "math",
Score: 100,
},
}
fmt.Println(m.Course.Name, m.Score) // math 100
m.setScore(120)
fmt.Println(m.Course.getName(), m.Course.Score) // math 120
}
通过类型定义,声明一个新的类型,再为其声明方法。
type MyBool bool
func (m MyBool) SayType() {
fmt.Printf("%T\n", m)
}
//type PtrMyBool *MyBool
//func (p PtrMyBool) SayType() {} // *MyBool' is a pointer type
func function31() {
var a MyBool
a.SayType() // main.MyBool
}
补充
Go 中根据首字母的大小写确定访问权限,无论是方法名、常量、变量名还是结构体的名称。
可以简单的理解:首字母大写是公有的,首字母小写是私有的。