切片与数组
前言
在Go中,数组和切片两者看起来长得几乎一模一样,但功能有着不小的区别,数组是定长的数据结构,长度被指定后就不能被改变,而切片是不定长的,切片在容量不够时会自行扩容。
数组
如果我们事先就知道了要存放数据的长度,而且我们在后续使用中不会有扩容的需求,我们就可以考虑使用数组
注意:在Go中,数组是值类型而非引用,它并不是指向数组头部元素的指针
数组的初始化
数组在上面长度时只能是一个常量,而绝不能是一个变量
var a[5] int //正确定义 //错误示范 l:=1 var b[l] int
数组的初始化写法一般有以下几种:
- 只初始化长度
var a[5] int
- 利用元素初始化
a:=[5]int{1,2.3}
- 利用new获得一个指针
num:=new([5]int)
- 以上几种方式都会给
nums
分配一片固定大小的内存,区别只是最后一种得到的值是指针
数组的使用
只要有数组名和下标,就可以访问数组中对应的元素。
fmt.Println(nums[0])
同样的也可以修改数组元素
nums[0] = 1
还可以通过内置函数len
来访问数组元素的数量
len(nums)
内置函数cap
来访问数组容量,数组的容量等于数组长度,容量对于切片才有意义。
cap(nums
数组的切割
切割数组的格式为arr[startIndex:endIndex]
,切割的区间为左闭右开,例子如下:
nums := [5]int{1, 2, 3, 4, 5} nums[1:] // 子数组范围[1,5) ->2 3 4 5 nums[:5] // 子数组范围[0,5) -> 1 2 3 4 5 nums[2:3] // 子数组范围[2,3) -> 3 nums[1:3] // 子数组范围[1,3) -> 2 3
切片
切片在Go中应用的更加广泛一些,用于存放不知道长度的数据,且后续使用过程中可能会频繁的插入和删除元素(我个人觉得比较像STL
容器中的vector
)
切片的初始化
切片的初始化主要有以下几种方式:
var a[]int a:=[]int{1,2,3} a:=make([]int,0,0) a:=new([]int)
可以看到切片与数组在外貌上的区别,仅仅只是少了一个初始化长度。通常情况下,推荐使用make
来创建一个空切片,只是对于切片而言,make
函数接收三个参数:类型,长度,容量。切片的长度代表着切片中元素的个数,切片的容量代表着切片总共能装多少个元素,切片与数组最大的区别在于切片的容量会自动扩张,而数组不会。
注意:
- 切片的底层实现依旧是数组,是引用类型。我们可以将它理解为指向底层数组的指针
- 通过
var nums []int
这种方式声明的切片,默认值为nil
,所以不会为其分配内存,而在使用make
进行初始化时,建议预分配一个足够的容量,可以有效减少后续扩容的内存消耗
向切片里面插入元素
切片的插入元素主要是基于append
函数来实现的,append
的函数签名是这样的:
func append(slice []Type,elems ...Type)
说明:
slice:待添加元素的切片
elems:添加的元素
接下来,给大家演示一个简单的demo:
package main import "fmt" func main() { a:=[]int{1,2,3,4,5} a=append(a,[]int {6,7,8,9,10}...) for i,v:=range a{ fmt.Printf("a[%d]=%d\n",i,v) } }
上面简单的展示了以下,如何利用append
函数插入一个另一个切片,值得注意的是:这里的省略号不能省略,它的作用是展开我们要插入切片,否则会出现下面这个报错:
当然我们也可以来选择插入元素的位置:
- 从头部插入
package main import "fmt" func main() { a:=[]int{1,2,3,4,5} a=append(a,[]int {6,7,8,9,10}...) for i,v:=range a{ fmt.Printf("a[%d]=%d\n",i,v) } }
- 从指定下标i插入
package main import "fmt" func main() { nums := make([]int, 0, 0) nums = append(nums, 1, 2, 3, 4, 5, 6, 7) i:=3 nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...) for i,v:=range nums{ fmt.Printf("nums[%d]=%d\n",i,v) } }
- 尾部插入元素
nums = append(nums, 99, 100) fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]
删除切片元素
从头部删除n个元素
nums = nums[n:] fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]
从尾部删除n个元素
nums = nums[:len(nums)-n] fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]
从中间指定下标i位置开始删除n个元素
nums = append(nums[:i], nums[i+n:]...) fmt.Println(nums)// i=2,n=3,[1 2 6 7 8 9 10]
删除所有元素
nums = nums[:0] fmt.Println(nums) // []
拷贝
切片在拷贝时需要确保目标切片有足够的长度,例如
func main() { dest := make([]int, 0) src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} fmt.Println(src, dest) fmt.Println(copy(dest, src)) fmt.Println(src, dest) }
[1 2 3 4 5 6 7 8 9] [] 0 [1 2 3 4 5 6 7 8 9] []
将长度修改为10,输出如下
[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0 0] 9 [1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 0]
多维切片和多维数组
- 数组的初始化
var a [5][5]int
- 切片的初始化 ```go slice=make([][]int,5) for i;i<len(slice);i++:{ slice[i]=make([]int,5) }
切片与数组都可以使用简单表达式来进行切割,但是拓展表达式只有切片能够使用,该特性于Go1.2版本添加,主要是为了解决切片共享底层数组的读写问题,主要格式为如下,需要满足关系low<= high <= max <= cap
,使用拓展表达式切割的切片容量为max-low
slice[low:high:max] low`与`high`依旧是原来的含义不变,而多出来的`max`则指的是最大容量,例如下方的例子中省略了`max`,那么`s2`的容量就是`cap(s1)-low
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9 s2 := s1[3:4] // cap = 9 - 3 = 6
那么这么做就会有一个明显的问题,s1
与s2
是共享的同一个底层数组,在对s2
进行读写时,有可能会影响的s1
的数据,下列代码就属于这种情况
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9 s2 := s1[3:4] // cap = 9 - 3 = 6 // 添加新元素,由于容量为6.所以没有扩容,直接修改底层数组 s2 = append(s2, 1) fmt.Println(s2) fmt.Println(s1)
最终的输出为
[4 1] [1 2 3 4 1 6 7 8 9]
可以看到明明是向s2
添加元素,却连s1
也一起修改了,拓展表达式就是为了解决此类问题而生的,只需要稍微修改一下就能解决该问题
func main() { s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9 s2 := s1[3:4:4] // cap = 4 - 3 = 1 // 容量不足,分配新的底层数组 s2 = append(s2, 1) fmt.Println(s2) fmt.Println(s1) }
现在得到的结果就是正常的
[4 1] [1 2 3 4 5 6 7 8 9]
clear
在go1.21新增了clear
内置函数,clear会将切片内所有的值置为零值,
package main import ( "fmt" ) func main() { s := []int{1, 2, 3, 4} clear(s) fmt.Println(s) }
输出
[0 0 0 0]
如果想要清空切片,可以
func main() { s := []int{1, 2, 3, 4} s = s[:0:0] fmt.Println(s) }
限制了切割后的容量,这样可以避免覆盖原切片的后续元素。
字符串
在Go中,字符串本质上是一个不可变的只读的字节数组,也是一片连续的内存空间。
原生字符串和普通字符串
- 普通字符串
由""
双引号表示,支持转义,不支持多行书写 - 原生字符串
由反引号表示,不支持转义,支持多行书写,原生字符串里面所有的字符都会原封不动的输出,包括换行和缩进
接下来用一个很简单的demo给大家演示一下:
package main import "fmt" func main() { s1:="1235468976\nover" s2:=`1234567890\nover` fmt.Println(s1,s2) }
字符串的访问
由于字符串的本质是字节数组,所以字符串的访问形式与数组切片完全一致,比如下面这个demo:
package main import "fmt" func main() { s2:=`1234567890\nover` fmt.Println(s2[3]) }
它的输出为:
我们可以看到输出的是字节而不是字符
切割字符串
package main import "fmt" func main() { s2:=`1234567890\nover` fmt.Println(s2[1:4]) }
输出为:
覆盖字符串
尝试修改字符串元素
func main() { str := "this is a string" str[0] = 'a' // 无法通过编译 fmt.Println(str) }
func main() { str := "this is a string" str = "that is a string" fmt.Println(str) }
字符串的转换
字符串可以转换为字节切片,而字节切片也可以转换为
字符串,示例如下:
package main import "fmt" func main() { s1:="abcdefg" bytes:=[]byte(s1) fmt.Println(bytes) s2:=string(bytes) fmt.Println(s2) }
输出为:
字符串的内容是只读的不可变的,无法修改,但是字节切片是可以修改的。
func main() { str := "this is a string" fmt.Println(&str) bytes := []byte(str) // 修改字节切片 bytes = append(bytes, 96, 97, 98, 99) // 赋值给原字符串 str = string(bytes) fmt.Println(str) }
注意:两种类型之间的转换都需要进行数据拷贝,其性能损耗会随着长度的增加而增长
字符串拷贝
字符串的拷贝其实类似于数组切片的拷贝,而字符串拷贝其实本质上是字节切片拷贝,它的实现方式主要有以下两种,接下来我会用两个小demo来演示一下:
第一种方法
package main import "fmt" func main(){ var s1,s2 string s1="fengxu" bytes:=make([]byte,len(s1)) copy(bytes,s1) s2=string(bytes) fmt.Println(s2) }
当然我们也可以使用strings.Clone
函数package main
package main import( "fmt" "strings" ) func main(){ var s1,s2 string s1="fengxu" s2=strings.Clone(s1) fmt.Println(s2) }
字符串的拼接
字符串的拼接的方法主要有三种:
直接用+
package main import( "fmt" _"strings" //不适用该包用匿名导入 ) func main(){ var s1,s2 string s1="My name is" s2=s1+" fengxu" fmt.Println(s2) }
转换为字符切片再加入元素
package main import( "fmt" _"strings" ) func main() { s1 := "fengxu" bytes := make([]byte, len(s1)) copy(bytes, s1) bytes = append([]byte("My name is "),bytes ...) fmt.Println(string(bytes)) }
但是其实这两种字符串的拼接性能都比较差,如果追求性能的话,我们可以考虑使用内置函数string.Builder
package main import( "fmt" "strings" ) func main() { bulider:=strings.Builder{} bulider.WriteString("Hello,") bulider.WriteString(" World!") fmt.Println(bulider.String()) //输出:Hello, World }
字符串的遍历
在开头我们就已经提到过,Go中的字符串就是一个只读的字节切片,也就是说字符串的组成单位是字节而不是字符。这种情况经常会在遍历字符串时遇到,例如下方的代码
func main() { str := "hello world!" for i := 0; i < len(str); i++ { fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i])) } }
输出结构应该是这样的:
104,68,h 101,65,e 108,6c,l 108,6c,l 111,6f,o 32,20, 119,77,w 111,6f,o 114,72,r 108,6c,l 100,64,d 33,21,!
其实对于字节而言输出大姑子姐的英文字母,其实问题并不大,但是我们要明确一点,在ASCII码中中文字符其实是三个字节,这样会使我们的结果出现偏差,Go字符串是明确支持utf8的,应对这种情况就需要用到rune
类型,在使用for range
进行遍历时,其默认的遍历单位类型就是一个rune
,例如下方代码
func main() { str := "hello 世界!" for _, r := range str { fmt.Printf("%d,%x,%s\n", r, r, string(r)) } }
输出如下
104,68,h 101,65,e 108,6c,l 108,6c,l 111,6f,o 32,20, 19990,4e16,世 30028,754c,界 33,21,!
rune
本质上是int32
的类型别名,unicode字符集的范围位于0x0000 - 0x10FFFF之间,最大也只有三个字节,合法的UTF8编码最大字节数只有4个字节,所以使用int32
来存储是理所当然,上述例子中将字符串转换成[]rune
再遍历也是一样的道理,如下
func main() { str := "hello 世界!" runes := []rune(str) for i := 0; i < len(runes); i++ { fmt.Println(string(runes[i])) } }
还可以使用uft8
包下的工具,例如
func main() { str := "hello 世界!" for i, w := 0, 0; i < len(str); i += w { r, width := utf8.DecodeRuneInString(str[i:]) fmt.Println(string(r)) w = width } }
映射表
一般来说,映射表的数据结构实现主要有两种:哈希表(hash table)和搜索树(search tree),前者实现原理是基于哈希映射而后者则是红黑树,区别在于前者无序,后者有序,本篇旨在介绍常见的数据结构,故不做详细介绍,后面我会单开一篇文章来介绍其实现原理
map的初始化
在Go中,map的键类型必须是可以比较的,比如string
,int
,而像[]int
这种不能比较的也就不能作为map
的键,接下来我们来看一下map初始化的两种方法:
- 直接用字面量来初始化
形式主要为:
map[keyType]ValueType{} • 1
示例如下:
mp := map[int]string{ 0: "a", 1: "a", 2: "a", 3: "a", 4: "a", } mp := map[string]int{ "a": 0, "b": 22, "c": 33, }
- 利用make函数为其分配内存
make函数分配内存会接受两个参数,分别是类型与初始容量,例子如下:
mp := make(map[string]int, 8) mp := make(map[string][]int, 10)
注意:map是引用类型,零值或未初始化的map可以访问,但是无法存放元素,所以必须要为其分配内存
对映射表的访问
访问一个map的方式就像通过索引访问一个数组一样。
func main() { mp := map[string]int{ "a": 0, "b": 1, "c": 2, "d": 3, } fmt.Println(mp["a"]) fmt.Println(mp["b"]) fmt.Println(mp["d"]) fmt.Println(mp["f"]) }
0 1 3 0
通过代码可以观察到,即使map中不存在"f"
这一键值对,但依旧有返回值。map对于不存的键其返回值是对应类型的零值,并且在访问map的时候其实有两个返回值,第一个返回值对应类型的值,第二个返回值一个布尔值,代表键是否存在,例如:
func main() { mp := map[string]int{ "a": 0, "b": 1, "c": 2, "d": 3, } if val, exist := mp["f"]; exist { fmt.Println(val) } else { fmt.Println("key不存在") } }
备注:这里我们来解释一下f val, exist := mp["f"]; exist
这句代码的含义:
对map求长度
func main() { mp := map[string]int{ "a": 0, "b": 1, "c": 2, "d": 3, } fmt.Println(len(mp)) }
映射表的存值
map存值的方式也类似数组存值一样,例如:
func main() { mp := make(map[string]int, 10) mp["a"] = 1 mp["b"] = 2 fmt.Println(mp) }
存值时使用已存在的键会覆盖原有的值
func main() { mp := make(map[string]int, 10) mp["a"] = 1 mp["b"] = 2 if _, exist := mp["b"]; exist { mp["b"] = 3 } fmt.Println(mp) }
但是也存在一个特殊情况,那就是键为math.NaN()
时
func main() { mp := make(map[float64]string, 10) mp[math.NaN()] = "a" mp[math.NaN()] = "b" mp[math.NaN()] = "c" _, exist := mp[math.NaN()] fmt.Println(exist) fmt.Println(mp) }
false map[NaN:c NaN:a NaN:b]
通过结果可以观察到相同的键值并没有覆盖,反而还可以存在多个,也无法判断其是否存在,也就无法正常取值。因为NaN是IEE754标准所定义的,其实现是由底层的汇编指令UCOMISD
完成,这是一个无序比较双精度浮点数的指令,该指令会考虑到NaN的情况,因此结果就是任何数字都不等于NaN,NaN也不等于自身,这也造成了每次哈希值都不相同。关于这一点社区也曾激烈讨论过,但是官方认为没有必要去修改,所以应当尽量避免使用NaN作为map的键。
映射表键值对的删除
删除一个键值对需要用到内置函数delete
,函数形式如下:
func delete(m map[Type]Type1, key Type)
示例代码如下:
func main() { mp := map[string]int{ "a": 0, "b": 1, "c": 2, "d": 3, } fmt.Println(mp) delete(mp, "a") fmt.Println(mp) }
**注意:**如果值为NaN,甚至没法删除该键值对。
映射表的遍历
通过for range
可以遍历map,例如
func main() { mp := map[string]int{ "a": 0, "b": 1, "c": 2, "d": 3, } for key, val := range mp { fmt.Println(key, val) } }
c 2 d 3 a 0 b 1 • 1 • 2 • 3 • 4
可以看到结果并不是有序的,也印证了map是无序存储。值得一提的是,NaN虽然没法正常获取,但是可以通过遍历访问到,例如
func main() { mp := make(map[float64]string, 10) mp[math.NaN()] = "a" mp[math.NaN()] = "b" mp[math.NaN()] = "c" for key, val := range mp { fmt.Println(key, val) } }
NaN a NaN c NaN b
映射表的清空
在go1.21之前,想要清空map,就只能对每一个map的key进行delete
func main() { m := map[string]int{ "a": 1, "b": 2, } for k, _ := range m { delete(m, k) } fmt.Println(m) }
但是go1.21更新了clear函数,就不用再进行之前的操作了,只需要一个clear就可以清空
func main() { m := map[string]int{ "a": 1, "b": 2, } clear(m) fmt.Println(m) }
set
Set是一种无序的,不包含重复元素的集合,Go中并没有提供类似的数据结构实现,但是map的键正是无序且不能重复的,所以也可以使用map来替代set。
func main() { set := make(map[int]struct{}, 10) for i := 0; i < 10; i++ { set[rand.Intn(100)] = struct{}{} } fmt.Println(set) }
map[0:{} 18:{} 25:{} 40:{} 47:{} 56:{} 59:{} 81:{} 87:{}] • 1
映射表使用的注意事项
map并不是一个并发安全的数据结构,Go团队认为大多数情况下map的使用并不涉及高并发的场景,引入互斥锁会极大的降低性能,map内部有读写检测机制,如果冲突会触发fatal error
。例如下列情况有非常大的可能性会触发fatal
。在这种情况下,需要使用sync.Map
来替代,这个我们在后续并发的文章中会提到
指针
前言
Go保留了指针,在一定程度上保证了性能,同时为了更好的GC(后面会介绍什么是GC)和安全角度考虑,又限制了指针的使用
指针的创建
指针的常用操作符主要有两个,一个是取地址符&
,另一个是解引用符*
。
对一个变量进行取地址,会返回对应类型的指针,例如:
func main() { num := 2 p := &num fmt.Println(p) }
指针存储的是变量num
的地址
0xc00001c088 • 1
解引用符则有两个用途,第一个是访问指针所指向的元素,也就是解引用,例如
func main() { num := 2 p := &num rawNum := *p fmt.Println(rawNum) }
p
是一个指针,对指针类型解引用就能访问到指针所指向的元素。还有一个用途就是声明一个指针,例如:
func main() { var numPtr *int fmt.Println(numPtr) }
<nil>
*int
即代表该变量的类型是一个int
类型的指针,不过指针不能光声明,还得初始化,需要为其分配内存,否则就是一个空指针,无法正常使用。要么使用取地址符将其他变量的地址赋值给该指针,要么就使用内置函数new
手动分配,例如:
func main() { var numPtr *int numPtr = new(int) fmt.Println(numPtr) }
更多的是使用短变量
func main() { numPtr := new(int) fmt.Println(numPtr) }
new
函数只有一个参数那就是类型,并返回一个对应类型的指针,函数会为该指针分配内存,并且指针指向对应类型的零值,例如:
func main() { fmt.Println(*new(string)) fmt.Println(*new(int)) fmt.Println(*new([5]int)) fmt.Println(*new([]float64)) }
0 [0 0 0 0 0] []
禁止指针运算
什么是指针偏移
在Go里面是不允许指针运算的,也就是说在Go里面是不允许指针偏移的情况出现,或许大家不是很清楚什么是指针偏移,接下来未来大家简单了解一下:
首先我们知道在c++中,指针可以看作数组中头部元素的地址而我们可以让这个指针发生偏移进而得到其他元素,如下:
int main() { int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; int *p = &arr[0]; cout << &arr << endl << p << endl << p + 1 << endl << &arr[1] << endl; }
输出:
0x31d99ff880 0x31d99ff880 0x31d99ff884 0x31d99ff884
而这种操作在Go中是不允许的。
拓展
在后面我会介绍标准库unsafe
标准库,可以一定程度上实现指针偏移,大家有兴趣也可以结合官方文档进行学习
new与make
在前面的几节已经很多次提到过内置函数new
和make
,两者有点类似,但也有不同,下面复习下。
func new(Type) *Type
- 返回值是类型指针
- 接收参数是类型
- 专用于给指针分配内存空间
func make(t Type, size ...IntegerType) Type
- 返回值是值,不是指针
- 接收的第一个参数是类型,不定长参数根据传入类型的不同而不同
- 专用于给切片,映射表,通道分配内存。
结构体
前言
Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。结构体可以存储一组不同类型的数据,是一种复合类型,下面是一个示例:
type builder struct { name string age int sex string }
结构体的声明
结构体的声明非常简单,例子如下:
type Person struct { name string age int }
结构体本身以及其内部的字段都遵守大小写命名的暴露方式。对于一些类型相同的字段,可以像如下方式声明:
type Rectangle struct { height, width, area int color string }
结构体的创建
Go不存在构造方法,大多数情况下采用如下的方式来创建。
programmer := Programmer{ Name: "jack", Age: 19, Job: "coder", Language: []string{"Go", "C++"}, }
初始化的时候就像map
一样指定字段名称再初始化字段值,不过也可以省略字段名称。
programmer := Programmer{ "jack", 19, "coder", []string{"Go", "C++"}}
当省略字段名称时,就必须初始化所有字段,且必须按照声明的顺序初始化。
func NewProgrammer() Programmer { return Programmer{ "jack", 19, "coder", []string{"Go", "C++"}} }
补充:其实我们也可以专门编写一个代码来初始化结构体,子类函数我们一般会叫它工厂方法,这也是其为什么没有构造方法的原因之一,后面在下一篇文章中我在详细介绍。
结构体的组合
前言
我们知道,Go本身其实刻意的弱化了面向对象功能并且抛弃了类与继承,但是我们还是要有办法表示结构体与结构体之间的关系,这时候我们就要使用组合来模拟出类似于继承的效果
组合实现的两种方式
- 显式组合
type Person struct { name string age int } type Student struct { p Person school string } type Employee struct { p Person job string }
- 而我们在使用时也需要显式的指定字段
p
student := Student{ p: Person{name: "jack", age: 18}, school: "lili school", } fmt.Println(student.p.name)
- 匿名组合
匿名组合可以不用显式的指定字段
type Person struct { name string age int } type Student struct { Person school string } type Employee struct { Person job string }
- 匿名字段的名称默认为类型名,调用者可以直接访问该类型的字段和方法,但除了更加方便以外与第一种方式没有任何的区别。
student := Student{ Person: Person{name: "jack",age: 18}, school: "lili school", } fmt.Println(student.name)
结构体指针
对于结构体指针而言,不需要解引用就可以直接访问结构体的内容,例子如下:
p := &Person{ name: "jack", age: 18, } fmt.Println(p.age,p.name)
在编译的时候会转换为(*p).name
,(*p).age
,其实还是需要解引用,不过在编码的时候可以省去,算是一种语法糖。
空结构体
空结构体没有字段,不占用内存空间,可以通过unsafe.SizeOf
函数来计算占用的字节大小
func main() { type Empty struct { } fmt.Println(unsafe.Sizeof(Empty{})) }
输出
0
空结构体的使用场景有很多,比如之前提到过的,作为map
的值类型,可以将map
作为set
来进行使用,又或者是作为通道的类型,即代表一个不发送数据的通道。
结语
在上面我们介绍了数组与切片,字符串,映射表,指针以及结构体等常见的基本数据结构,由于这篇文章主要是一些基础性的知识,所以有关slice,map的具体实现原理,我并没有去展开讲,这一部分我会在介绍一些底层原理如Go的垃圾回收机制时一并奉上,由于博主也是刚刚开始学习Go语言,有些东西表达可能并不清楚,也欢迎大家斧正,下篇文章我们将要进入Go语言面向对象编程的部分,虽然Go语言淡化了面向对象,但是面向对象的抽象,封装,继承与多态依旧是我们书写代码时上面值得借鉴的思想,所以下一篇我将尝试利用Go语言来实现面向对象,同时我也会基于牛客写一些关于Go语言的算法题题解,欢迎大家指正。