Go语言系列:两三天也学不完Go语言的进阶语法知识
前言
这次接着之前Go
语言的学习,继续来学习Go
语言的基础知识。上一篇Go语言系列:半天学完Go语言的最最基础的语法知识文章需要半天时间,那这次预计需要一天时间了,甚至更多,所以这次也要耐心看完哦。
Go语言的基础语法
上次聊到Go
语言的常量,这次书接上回,接着往下聊,慢慢地往后深入→
一、枚举
Go
语言中其实是没有枚举类型的,但是,可以使用常量iota
来模拟枚举。
iota
常量生成器用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const
声明语句中,在第一个声明的常量所在的行,iota
默认会被置为0
,然后在每一个有常量声明的行自动+1
。
package main
import "fmt"
// 定义一个名为 Sex 类型, 实际类型为 int
type Sex int
// 定义性别男女
const (
Woman Sex = iota // 将枚举值Woman定义为Sex类型,并搭配iota开始生成枚举值,默认从0开始
Man
)
func main() {
// 输出枚举值
fmt.Println(Woman, Man)
// 使用枚举类型,并赋值
var sex Sex = Man
fmt.Println(sex)
}
输出结果如下:
从打印的结果可以看到,Woman
枚举值定义了0
, 那么Man
的值会自动+1
,变成了1
。看来是没有问题的。
当然,iota
除了每次自增1
以外,还可以利用iota
来完成一些更复杂的操作。如下:
package main
import "fmt"
const (
val1 = 1 << iota // 移位操作,左移一位
val2
val3
val4
)
func main() {
// 输出枚举整型值
fmt.Printf("%d %d %d %d\n", val1, val2, val3, val4)
// 输出枚举二进制格式的值
fmt.Printf("%b %b %b %b", val1, val2, val3, val4)
}
输出结果如下:
在实际应用场景中,通常需要获取枚举值对应的字符串描述。可以如下操作:
package main
import "fmt"
// 定义一个名为 Sex 类型
type Sex int
const (
Woman Sex = iota // 开始生成枚举值,默认从 0 开始
Man
)
// 定义一个 Sex 类型的方法 String(), 返回字符串
func (s Sex) String() string {
switch s {
case Woman:
return "女"
case Man:
return "男"
}
return "N/A"
}
func main() {
// 输出枚举 Woman 的字符串描述,以及整型值
fmt.Printf("%s %d", Woman, Woman)
}
输出结果如下:
二、指针
在Go
语言中,指针有两个核心概念:
- 类型指针:允许对这个指针类型的数据进行修改。传递数据使用指针,而无需拷贝数据。类型指针不能进行偏移和运算。
- 切片:由指向其实元素的原始指针、元素数量和容量组成。
Go
语言中,通过&
操作符对变量进行“取地址”操作,如下:
p := &v // v 的类型为 T
上述代码中,v
表示被取地址的变量,取到的地址变量p
进行接收,p
的类型为*T
,成为T
的指针类型。*
代表指针。
来看下面这个指针的取地址示例:
代码输出结果如下:
注意:代码每次运行的结果是不同的,表示 num
和 website
两个变量在运行时的地址。
总结: 变量、指针和地址三者的关系是:每个变量都拥有地址,指针的值表示这个地址。
现在能通过 &
获取变量的指针,那么要如何通过指针取值呢?代码如下:
代码输出结果如下:
总结:变量、指针地址、指针变量、取地址、取值的相互关系和特性见下
- 对变量进行取地址 (
&
) 操作,可以获取这个变量的指针变量;- 指针变量的值是指针地址;
- 对指针变量进行取值(
*
)操作,可以获取指针变量指向的实际值。
下面就继续来看使用指针修改值,如下:
package main
import "fmt"
// 数值交换函数
func swap(a, b *int) {
// 取 a 指针的值,赋给临时变量 t
t := *a
// 取 b 指针的值,赋给 a 指针指向的变量
*a = *b
// 将 a 指针的值赋给 b 指针指向的变量
*b = t
}
func main() {
// 声明两个变量 x, y, 值分别为 1,2
x, y := 1, 2
// 交换变量值
swap(&x, &y)
// 输出交换后的 x, y 值
fmt.Println(x, y)
}
上述代码中输出结果如下:
那如果把上面代码中的swap()
函数改为交换的是指针值呢?
func swap(a, b *int) {
b, a = a, b
}
func main() {
// 声明两个变量 x, y, 值分别为 1,2
x, y := 1, 2
// 交换变量值
swap(&x, &y)
// 输出交换后的 x, y 值
fmt.Println(x, y)
}
代码输出如下:
可以看到,值交换失败了。上面swap()
函数交换的是a
和b
的地址,交换完毕后,它们实际指向的值并没有发生改变。这就好比放在桌子上的两个钱包,将位置交换后,里面存放的钱并没有发现改变一样。
Go
语言中还提供了new()
函数来创建指针,代码如下:
str := new(string)
*str = "黑土豆"
fmt.Println(*str)
注意:new()
函数可以创建一个对应类型的指针,同时会分配内存,被创建的指针指向的值为默认值。
三、数组
在Go
语言中,数组一旦声明,那么大小就确定了,仅可以修改数组成员,但是不能改变大小。
Go
语言中定义数组的格式如下:
var 数组变量名 [元素数量]T
说明:
- 数组变量名: 定义一个数组的变量名;
- 元素数量: 定义数组的大小;
T
可以是任意基本类型,甚至可以是数组本身,若为数组,则可以实现多维数组。
直接来个示例就明了了,如下:
package main
import "fmt"
func main() {
// 定义一个变量为 arr, 成员类型为 string, 大小为 3 的数组
var arr [3]string
// 赋值操作
arr[0] = "a"
arr[1] = "b"
arr[2] = "c"
fmt.Println(arr)
}
代码输出结果如下:
那下面来看Go
语言中怎么初始化数组,分别有两种方式,如下:
- 定义数组的时候,将数组提前初始化好
var arr = [3]string{"a", "b", "c"}
- 将定义数组大小的操作交给编译器,让编译器在编译时,根据元素的个数来确定大小
var arr = [...]string{"a", "b", "c"}
...
表示让编译器来确定数组大小。如上的代码编译器会自动将这个数组的大小设置为3
。
Go
语言中又是怎么遍历数组的呢?通过for range
来遍历数组,如下:
package main
import "fmt"
func main() {
// 定义一个变量为 arr, 成员类型为 string, 大小为 3 的数组
var arr = [...]string{"a", "b", "c"}
for index, v := range arr {
fmt.Printf("index: %d, value: %s\n", index, v)
}
}
上面代码中,index
表示数组当前的下标, v
表示当前元素的值。
代码输出结果如下:
四、切片
Go
语言中切片和数组类似,都是数据集合。和数组不同的是,切片是一块动态分配大小的连续空间。
如何声明切片?切片的声明格式如下:
var name []T
说明:
name
表示切片变量名;T
表示切片类型。
来看一个具体的示例代码, 如下:
package main
import "fmt"
func main() {
// 声明整型切片
var numSlice []int
// 声明字符串切片
var strSlice []string
// 声明一个空切片, {} 表示已经分配内存,但是切片里面的元素是空的
var numSliceEmpty = []int{}
// 输出3个切片
fmt.Println(numSlice, strSlice, numSliceEmpty)
// 输出3个切片大小
fmt.Println(len(numSlice), len(strSlice), len(numSliceEmpty))
// 切片判定是否为空结果
fmt.Println(numSlice == nil)
fmt.Println(strSlice == nil)
fmt.Println(numSliceEmpty == nil)
}
代码输出结果如下:
Go
语言中可以通过make()
函数动态的创建一个切片。格式如下:
make( []T, size, cap )
说明:
T
:切片中元素的类型;size
:表示为这个类型分配多少个元素;cap
:预分配的元素数量,该值设定后不影响size
, 表示提前分配的空间,设置它主要用于降低动态扩容时,造成的性能问题。
示例代码如下:
package main
import "fmt"
// 使用 make() 函数构造切片
func main() {
a := make([]int, 3)
b := make([]int, 3, 10)
fmt.Println(a, b)
fmt.Println(len(a), len(b))
}
代码输出结果如下:
注意:len()
函数计算的是元素的个数,与切片容量无关。
Go
语言中可以通过append()
可以为切片动态添加元素。代码如下:
package main
import "fmt"
// 使用 append() 函数为切片添加元素
func main() {
// 声明一个字符串类型的切片
var strSlice []string
// 循环动态向 strSlice 切片中添加 6 个元素,并打印相关参数
for i := 0; i < 6; i++ {
line := fmt.Sprintf("abc %d", i)
strSlice = append(strSlice, line)
fmt.Printf("len: %d, cap: %d, pointer: %p, content: %s\n", len(strSlice), cap(strSlice), strSlice, strSlice[i])
}
}
代码输出结果如下:
注意:通过上面的代码输出,会发现len()
并不等于cap
。这是因为当切片空间不足以容纳足够多的元素时,切片会自动进行扩容操作, 扩容规律按切片容量的2
倍进行扩容,如 1、2、4、8、16 ....
另外,append()
函数除了添加一个元素外,还能一次性添加多个元素。如下:
package main
import "fmt"
func main() {
var strSlice []string
// 添加一个元素
strSlice = append(strSlice, "abc")
// 添加多个元素
strSlice = append(strSlice, "def", "ghi", "jkl")
// 添加切片
list := []string{"mno", "pqr"}
// list 后面的 ... 表示将 list 整个添加到 strSlice 切片中
strSlice = append(strSlice, list...)
fmt.Println(strSlice)
}
代码输出结果如下:
Go
语言中可以通过从数组或切片生成新的切片。格式如下:
slice [开始位置:结束位置]
说明:
slice
表示切片目标;- 开始位置和结束位置对应目标切片的下标。
从数组中生成切片,代码如下:
package main
import "fmt"
// 从数组中生成切片
func main() {
var arr = [5]int{1, 2, 3, 4, 5}
fmt.Println(arr, arr[1:2])
}
代码输出结果如下:
注意:[2]
是arr[1:2]
切片操作的结果(取出的元素不包括结束位置的元素)。
Go
语言中可以从指定范围中生成切片,示例代码如下:
package main
import "fmt"
// 从指定范围中生成切片
func main() {
var arr = [10]int{}
// 向数组中添加元素
for i := 0; i < 10; i++ {
arr[i] = i + 1
}
// 指定区间
fmt.Println(arr[5:9])
// 中间到尾部所有元素
fmt.Println(arr[6:])
// 开头到中间所有元素
fmt.Println(arr[:8])
// 切片本身
fmt.Println(arr[:])
}
代码输出结果如下:
总结:
- 若不填写结束位置,如
arr[5:]
, 则表示从下标5置到数组的结束位置。- 若不填写开始位置,如
arr[:5]
,则表示从0到下标5的位置。- 若开始位置和结束位置都不填写,如
arr[:]
, 则会生成一个和原有切片一样的切片。
Go
语言中若把切片的开始位置和结束位置都设置为0,则会生成一个空的切片,即重置切片。示例代码如下:
package main
import "fmt"
// 重置切片
func main() {
var arr = [10]int{}
// 向数组中添加元素
for i := 0; i < 10; i++ {
arr[i] = i + 1
}
fmt.Println(arr[0:0])
}
Go
语言中可以通过copy()
函数将一个切片中的数据复制到另一个切片中,使用格式如下:
copy( destSlice, srcSlice []T) int
说明:
srcSlice
代表源切片;destSlice
代表目标切片。
注意:目标切片必须有足够的空间来装载源切片的元素个数。返回值为整型,表示实际发生复制的元素个数。
package main
import "fmt"
// 复制切片元素到另一个切片
func main() {
// 源分片
srcSlice := make([]int, 10)
// 给源分片赋值
for i := 0; i < 10; i++ {
srcSlice[i] = i
}
// 目标分片
destSlice := make([]int, 10)
// 将 srcSlice 分片的数据复制到 destSlice 中
copy(destSlice, srcSlice)
fmt.Println(srcSlice)
fmt.Println(destSlice)
}
代码输出结果如下:
Go
语言中并没有提供特定的函数来删除切片中元素,但是可以利用切片的特性来达到目的。如下:
package main
import "fmt"
// 从切片中删除元素
func main() {
// 声明一个字符串类型的切片
arr := []string{"a", "b", "c", "d", "e", "f", "g"}
// 指定删除位置,也就是 u 元素
index := 1
// 打印删除位置之前和之后的元素,
// arr[:index] 表示的是被删除元素的前面部分数据,
// arr[index+1:] 表示的是被删除元素后面的数据
fmt.Println(arr[:index], arr[index+1:])
// 将删除点前后的元素拼接起来
arr = append(arr[:index], arr[index+1:]...)
fmt.Println(arr)
}
代码输出结果如下:
注意:Go
语言中切片删除元素的本质即:以被删除元素为分界点, 将前后两个部分的内存重新连接起来。
五、字典
在Go
语言中提供的字典容器为 map
, map
使用散列表(hash)
实现。格式代码如下:
map [keyType]valueType
说明:
keyType
表示键类型;valueType
表示键对应的值类型。
注意:键和键对应的值总是以一对一的形式存在。
来看下面这段map
的示例代码,如下:
package main
import "fmt"
func main() {
// 定义一个键类型为字符串,值类型为整型的 map
m := make(map[string]int)
// 向 map 中添加一个键为 “abc”,值为 1 的映射关系
key := "abc"
m[key] = 1
// 输出 map 中键为 “abc” 对应的值
fmt.Println(m[key])
// 尝试输出一个不能存在的键,会输出该值类型的默认值
n := m["def"]
fmt.Println(n)
}
代码输出结果如下:
注意:如果尝试从map
中获取一个并不存在的键(key)
, 此时会输出值类型的默认值,整型的默认值为0
。
map
还存在另外一种初始化方式, 通过大括号的方式来初始化字典map
, 冒号左边的是键(key)
, 右边的是值(value)
,键值对之间使用逗号分隔。代码如下:
m := map[int](string){
1: "a",
2: "b",
3: "c",
}
Go
语言中字典map
的遍历需要使用for range
循环,代码如下:
package main
import "fmt"
// 遍历map
func main() {
m := map[int](string){
1: "a",
2: "b",
3: "c",
}
// 通过 for range 遍历, 获取 key, value 值并打印
for key, value := range m {
fmt.Printf("key: %d, value: %s\n", key, value)
}
}
代码输出结果如下:
如果只需要遍历值,也可以通过匿名变量来实现:
for _, value := range m {
fmt.Printf("value: %s\n", value)
}
只遍历键时,通过下面这种方式:
for key := range m {
fmt.Printf("key: %d\n", key)
}
注意: 字典map
是一种无序的数据结构,不要期望输出时按照一定顺序输出。如果需要按顺序输出,请使用切片来完成。
Go
语言中是通过内置函数delete()
来删除键值对,格式如下:
delete(map, 键)
说明:
map
表示要删除的目标map
对象;- 键表示要删除的
map
中key
键。
示例代码如下:
package main
import "fmt"
// 删除字典 map 中键值对
func main() {
m := map[int](string){
1: "a",
2: "b",
3: "c",
}
// 删除 map 中键为 1 的键值对
delete(m, 1)
// 通过 for range 遍历, 获取 key, value 值并打印
for key, value := range m {
fmt.Println(key, value)
}
}
代码输出结果如下:
六、list(列表)
列表是一种非连续存储的容器,由多个节点组成,节点通过一些变量将彼此串联起来。列表常见的数据结构有: 单链表、双链表等
在Go
语言中,列表的实现都在container/list
包中,内部实现原理是双链表。列表能够方便高效地进行元素的删除、插入操作。
在Go
语言中list
的初始化方法有两种:New
和声明。如下:
- 通过
container/list
包中的New
方法来初始化list
, 如下:
变量名 := list.New()
- 通过声明初始化
list
,如下:
var 变量名 = list.List
那在Go
语言中是如何向list
(列表)中添加元素的呢?通过双链表支持往队列前面或后面添加元素,对应的方法分别为:
PushFront
PushBack
示例代码如下:
l := list.New()
l.PushFront("abc")
l.PushBack("xyz")
关于list
(列表)插入元素的方法,如下表所示:
方法 | 功能 |
---|---|
InsertAfter(v interface{}, mark *Element) *Element | 在mark 点后面插入元素 |
InsertBefore(v interface{}, mark *Element) *Element | 在mark 点前面插入元素 |
PushFrontList(other *List) | 添加other 列表中的元素到头部 |
PushBackList(other *List) | 添加other 列表中的元素到尾部 |
在Go
语言中list
(列表)的插入函数的返回值是一个*list.Element
结构,可以通过它来完成对列表元素的删除,代码如下:
package main
import (
"container/list"
"fmt"
)
func main() {
l := list.New()
// 头部添加字符串
l.PushFront("abc")
// 尾部添加字符串
l.PushBack("xyz")
// 尾部添加一个整型,并保持元素句柄
element := l.PushBack(1)
// 在 1 之后添加字符串 2
l.InsertAfter("2", element)
// 在 1 之前添加字符串 0
l.InsertBefore("0", element)
// 删除 element 对应的元素
l.Remove(element)
for i := l.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
}
代码输出结果如下:
如果不执行l.Remove(element)
,则list
保存的元素就是如下所示:
在Go
语言中遍历list
(列表) 需要搭配Front()
函数获取头元素,遍历过程中,只要元素不为空则可继续调用Next
函数往下遍历,从上面的示例就可以看到,代码如下:
package main
import (
"container/list"
"fmt"
)
// 遍历 list (列表)
func main() {
l := list.New()
// 头部添加字符串
l.PushFront("abc")
// 尾部添加字符串
l.PushBack("xyz")
// 遍历
for i := l.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
}
注意,在for
语句遍历中:
- 其中
i := l.Front()
表示初始赋值,用来获取列表的头部下标; - 然后每次会循环会判断
i != nil
,若等于空,则会退出循环,否则执行i.Next()
继续循环下一个元素;
代码输出如下:
至此,Go
语言的进阶语法知识就介绍完了,对于很久没有看Go
语言的我来说,一时半会儿还真没有吃透,只有后续通过项目来实践掌握。希望大家看完能有所收获!
往期精彩文章
后语
小伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走呗^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。
转载自:https://juejin.cn/post/7153546868448821285