go面试基础知识点大全-1基本数据结构
# go面试基础知识点大全 1
# 数组 (array)
数组是go中最基本的数据结构之一。数组的定义方式如下:
var arr [n]type
其中n是数组的长度,type是数组中元素的数据类型。例如:
var arr [3]int
其中长度可以省略,用"..."来代替,go会自动根据元素的个数来计算长度。 例如下面定义了一个长度为3的数组,三个元素分别是整数1、2、3,
arr := [...]int{1, 2, 3}
另外,一定要注意,长度是数组类型的一部分。所以[2]int和[3]int是不同的类型。所以下面的例子编译通不过,会提示“invalid operation: arr1 == arr2 (mismatched types [2]int and [3]int)”。
package main
import "fmt"
func main() {
arr1 := [2]int{1, 2}
arr2 := [3]int{1, 2}
if arr1 == arr2 {
fmt.Println("equal")
}
}
最后要注意,数组作为参数传递给函数时,是值传递;所以在函数内对数组做任何修改,不会影响到调用者。 例如,下面的例子最后输出是"[1, 2, 3]"。
package main
import "fmt"
func test(arr [3]int) {
arr[0] = 10
}
func main() {
arr1 := [3]int{1, 2, 3}
test(arr1)
fmt.Println(arr1)
}
# 切片(slice)
切片是go中最常用的数据结构之一,是对数组的封装,可以动态的扩容。扩容机制很简单,当切片的长度小于1024时,直接双倍扩大,例如之前slice的cap(容量)是6,直接扩大到12。当切片的长度大于1024时,每次扩大1/4,直到满足需求为止;例如之前slice的cap为1024,首先扩大到1024 + 1024/4 = 1280,然后判断是否达到需求的大小,如果达到了,就结束了,没有达到,就继续扩大1280 + 1280/4 = 1600,以此类推,直到满足需求为止。 不过切片没有对应的缩减机制,也即是当容量很大,而实际长度很小时,不会自动缩小切片的容量。之所以没有提供缩减机制,我理解还是由于应用场景千奇百怪,go不能替应用层做决定,否则可能出现不断的抖动。换个角度想一想,我们可以一开始就初始化一个容量很大,但实际长度很小的切片,如果go会自动缩减,那么我们刚刚初始化的slice就可能被go自动缩小。这种缩减机制应该是由应用层来决定,因为每个应用最清楚自己的应用场景。我在ahrtr/gocontainer中为arrayList提供了一种自动缩减的机制,我在考虑要不要增加一个配置项,让应用层可以关闭自动缩减机制,这是后话。
https://github.com/ahrtr/gocontainer/blob/master/list/arraylist.go
既可以用直接赋初值的方式初始化slice,也可以用make创建slice。下面的例子就是直接初始化了一个slice,
s := []int{1, 2, 3}
其实可以为元素指定一个索引 例如,下面的例子中,元素19的索引是5。第一个元素2没有指定索引,默认就是0;19之后的元素4也没有指定索引,默认就是下一个索引,也就是6。
package main
import "fmt"
func main() {
s := []int{2, 5: 19, 4}
fmt.Printf("len(s)=%d, cap(s)=%d, s: %v\n", len(s), cap(s), s)
}
上面的输出结果为
len(s)=7, cap(s)=7, s: [2 0 0 0 0 19 4]
用make来创建slice的格式如下:
make([]T, size, cap)
第三个参数可以省略。当不提供第三个参数时,创建的slice的长度和容量相同,例如,下面就创建了一个长度和容量均为3的slice,
s := make([]int, 3)
注意:这时往slice中append新元素是从第4个(index为3)位置开始添加,而不是从第1个(index为0)位置添加,前三个元素为零值,
s = append(s, 4) // The slice contains [0, 0, 0, 4]
在上面的例子中,由于slice初始的长度和容量均为3,所以向slice中添加新的元素时,slice会自动扩容到3*2=6。注意,这时会为slice分配了新的数组存储空间。如果在扩容之前有某个指针指向了slice的某个元素,那么扩容之后,无论如何修改slice的内容,之前那个指针永远指向扩容之前的旧值。 可以将一个切片追加到另一个切片后面,这时需要使用 "..."操作符;或者直接列出所有元素。 例如下面两种方式都是正确的,
//method 1
s1 = append(s1, s2...)
//method 2
s1 = append(s1, 4, 5, 6)
另外,也可以基于一个slice做切片操作,例如,下面的例子中,s2就是基于s1产生的一个新切片。注意,s2和s1底层实际上是指向相同的数组,如果通过s2修改的某个元素的值,通过s1可以看到修改后的值。
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[2: 4]
新切片s2的容量为cap(s1) - i。上面的例子中,i为2,cap(s1)为5,所以cap(s2)等于3。值得一提的是,其实还可以提供第三个参数,用来限制新切片的容量cap;如果提供第三个参数,其值必须大于前两个参数。 例如,下面的例子中,因为提供了第三个参数7,所以s2的容量为7-3=4。
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8}
s2 := s1[3:5:7]
基于一个slice做切片操作时,两个参数都是可选的。当第2个参数省略时,默认值就是原slice或array的长度。例如下面的例子中,s1[2:]实际上等价与s1[2: 5],所以s2的值为[3, 4, 5]。
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[2: ]
这里有一个坑,当省略第二个参数时,第一个参数的值一定不能大于原slice的长度,否则会panic。 例如,下面的例子中,s1的长度和容量分别是2和8。当基于s1做切片操作时,第一个参数的值3大于s1的长度2,所以会panic:“panic: runtime error: slice bounds out of range [3:2]”。
s1 := make([]int, 2, 8)
s2 := s1[3:]
# map
map是一堆无序的key:value对的集合。所以遍历map时是无序的,也就是遍历顺序是不确定的。 在ahrtr/gocontainer中实现的linkedmap,保证了遍历的有序性,参考https://github.com/ahrtr/gocontainer/blob/master/map/linkedmap/linkedmap.go 注意,map中的key必须是可比较类型 map使用之前,一定要初始化。既可以通过直接赋初值的方式初始化,也可以通过内置函数make初始化。如果只是声明一个map,而没有初始化,那么该map就等于nil。例如,下面就是一个nil map,
var m map[string]int
向nil map中写入数据肯定是不行的,会引起panic。但是却 可以从nil map中读取数据,返回零值 那么如何判断是由于nil map返回零值,还是map中本来就包含这么一个零值的键值对?答案就是通过第二个bool类型的返回值,如果为true,表示是从map中正常读取到的值,否则就是异常情况(map为nil,或者map中不存在对应的key) 例如,
var m map[string]int
if v, ok := m["hello"]; ok {
fmt.Println(v)
} else {
fmt.Println("m is nil or m doesn't have key 'hello'")
}
map的基础数据结构定义在src/runtime/map.go里的hmap。不难看出,真正的数据都是存储在由指针成员指向的内存空间。所以,当map作为参数传递给函数后,在函数内对map做增、删、改操作,都会影响到调用者。 例如,在函数change内对map的修改,在main函数内都能看到,最后输出结果为:map[g2:5 h1:3 h2:4]。
package main
import (
"fmt"
)
func change(m map[string]int) {
m["h1"] = 3
m["h2"] = 4
delete(m, "g1")
m["g2"] = 5
}
func main() {
m := make(map[string]int)
m["g1"] = 1
m["g2"] = 2
change(m)
fmt.Println(m)
}
如果map中value不可寻址,无法直接通过 "m[key].val = newVal" 的方式直接修改,否则编译无法通过。 例如,下面的例子中第12行是错误的,
package main
import "fmt"
type student struct {
age int
}
func main() {
m := make(map[int]student)
m[1] = student{23}
m[1].age = 24 // invalid
fmt.Println(m[1].age)
}
解决方案就是将map的value改成*student。修改后的代码如下:
package main
import "fmt"
type student struct {
age int
}
func main() {
m := make(map[int]*student)
m[1] = &student{23}
m[1].age = 24
fmt.Println(m[1].age)
}
# channel
channel是golang的一种特殊的数据结构,主要用于goroutine之间通信,从而避免共享内存的方式来通信。channel通过内置函数make来创建,创建的时候可以指定缓冲的大小,也可以不指定。注意,有缓冲的channel是异步操作;而没有缓冲的channel是同步操作。 如果只是声明一个channel,而没有使用make来初始化,那么该channel就等于nil。例如,下面声明的channel就是一个nil channel,
var ch chan int
注意,不管是向一个nil channel发送数据,还是从nil channel接收数据,都会永久阻塞。 所以使用channel之前,一定要用make初始化。
channel使用完之后,使用内置函数close关闭。注意,不能重复关闭一个channel,否则会panic。 从src/runtime/chan.go中的函数closechan可以看到具体的实现,
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
向一个已经关闭的channel发送数据,同样会引起panic。 从src/runtime/chan.go中的函数chansend也可以看到具体的实现,
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
但是可以从一个已经关闭的channel中接收数据,直到channel没有数据为止。注意,当一个channel已经关闭,并且里面也没有数据时,如果尝试从channel里面读取数据,会读取到零值(例如:对于int就是0,对于string就是空字符串"")。那么如何判断是由于channel关闭了返回零值,还是channel里面的数据本来就是零值?答案就是通过第二个bool类型的返回值,如果为true,则表示第一个返回值是从channel里面正常读取到的数据,否则表示channel已经关闭了 例如,下面的例子就是通过第二个返回值来判断v是否是从channel中正常读取到的数据,
for {
if v, ok := <-ch; ok {
fmt.Printf("Received: %v\n", v)
} else {
break
}
}
最后要注意一点,不能关闭单向只读channel,否则编译会报错。 例如,下面例子中的第5行是错误的,
func test(ch <-chan int) {
for v := range ch {
fmt.Println(v)
}
close(ch)
}
# string
golang中的string是不可改变的,例如,下面的代码中第5行尝试直接修改string,是错误的,编译无法通过,
package main
func main() {
str := "hello"
str[0] = 'g' //invalid
}
string的底层其实是byte数组,len(str)返回的是字节数。 例如,下面的例子最后输出12,因为两个汉字都分别占用3个字节。
package main
import "fmt"
func main() {
str := "hello,世界"
fmt.Println(len(str))
}
但是用range遍历string时,是按照实际的字符来遍历的。 例如,下面例子中,v的类型是rune,其实就是int32的别名。
package main
import "fmt"
func main() {
str := "hello,世界"
for i, v := range str {
fmt.Printf("%d, %c\n", i, v)
}
}
上面的例子输出:
0, h
1, e
2, l
3, l
4, o
5, ,
6, 世
9, 界
在golang中,string是以utf8编码的unicode文本。注意: unicode是一种字符集,而utf8是对unicode的一种编码规则。 utf8解决了string的存储和传输的问题。
golang提供了一个包strings,里面有很多实用的函数可以用来处理string。这其中容易混淆的是下面两组函数,
func TrimLeft(s string, cutset string) string
func TrimRight(s string, cutset string) string
func TrimPrefix(s, prefix string) string
func TrimSuffix(s, suffix string) string
TrimPrefix是删除s头部的prefix字符串,如果s不是以prefix开始,则直接返回s。而TrimLeft则是删除s头部连续的包含在cutset中的字符 例如,下面的例子输出:“abab,world”,
package main
import (
"fmt"
"strings"
)
func main() {
str := "ababab,world"
fmt.Println(strings.TrimPrefix(str, "ab"))
}
如果将上面的例子第10行改成strings.TrimLeft,则最后输出",world"。 同理,TrimSuffix是删除s尾部的suffix字符串,如果s不是以suffix结尾,则直接返回s。而TrimRight则是删除s尾部连续的包含在cutset中的字符。