Appearance
Go 基础、类型与数据结构
来源:用户提供的《golang 云原生面试题》PDF。下列为原文完整导入,按主题重排;页码为 PDF 物理页码。技术观点与示例保留原件语境,勘误请结合 校正说明 阅读。
与其他语言相比,使用 Go 有什么好处?
PDF 出处:第 1–1 页 · 原卷分组:golang基础 · 源标题
p001-h02。
与其他作为学术实验开始的语言不同,Go 代码的设计是务实的。每个功能和语法决策都旨在让程序员的生活更轻松。
Golang 针对并发进行了优化,并且在规模上运行良好。
由于单一的标准代码格式,Golang 通常被认为比其他语言更具可读性。
自动垃圾收集明显比 Java 或 Python 更有效,因为它与程序同时执行。
Golang 使用什么数据类型?
PDF 出处:第 1–1 页 · 原卷分组:golang基础 · 源标题
p001-h03。
Method
Boolean
Numeric
String
Array
Slice
Struct
Pointer
Function
Interface
Map
Channe
整型切片有哪些初始化的方式
PDF 出处:第 1–1 页 · 原卷分组:golang基础 · 源标题
p001-h04。
go
s := make([]int, 0)
s := make([]int, 5, 10)
s := []int{1, 2, 3, 4, 5}Go 语言中 cap 函数可以作用于哪些内容?
PDF 出处:第 2–2 页 · 原卷分组:golang基础 · 源标题
p002-h02。
array(数组)
slice(切片)
channel(通道)
输入 non-empty interface
PDF 出处:第 2–2 页 · 原卷分组:golang基础 · 源标题
p002-h04。
接口除了有静态类型,还有动态类型和动态值,当且仅当动态值和动态类型都为 nil 时,接口类型值才为 nil。这里的 x 的动态类型是 *int,所以 x 不为 nil
但是在此案例中,直接在main函数中的*int类型的x的值是nil
Go 语言当中 new 的作用是什么
PDF 出处:第 2–2 页 · 原卷分组:golang基础 · 源标题
p002-h05。
new 创建一个该类型的实例,并且返回指向该实例的指针。new 函数是内建函数,函数定义: func new(Type) *Type
使用 new 函数来分配空间
传递给 new 函数的是一个类型,而不是一个值
返回值是指向这个新分配的地址的指针
Go 语言中 make 的作用是什么
PDF 出处:第 2–2 页 · 原卷分组:golang基础 · 源标题
p002-h06。
make 的作用是为 slice, map or chan 的初始化然后返回引用 make 函数是内建函数,函数定义:
go
func make(Type, size IntegerType) Typemake(T, args)函数的目的和 new(T)不同仅仅用于创建 slice, map, channel 而且返回类型是实例
Go 程序中的包是什么
PDF 出处:第 3–3 页 · 原卷分组:golang基础 · 源标题
p003-h01。
包(pkg)是 Go 工作区中包含 Go 源文件或其他包的目录。源文件中的每个函数、变量和类型都存储在链接包中。每个 Go 源文件都属于一个包,该包在文件顶部使用 package关键词声明。使用import导入包。
Printf(),Sprintf(),FprintF() 都是格式化输出,有什么不同
PDF 出处:第 3–3 页 · 原卷分组:golang基础 · 源标题
p003-h02。
Printf 是标准输出,一般是屏幕,也可以重定向
Sprintf()是把格式化字符串输出到指定的字符串中
Fprintf()是把格式化字符串输出到文件中
Go 语言当中数组和切片的区别是什么?
PDF 出处:第 3–3 页 · 原卷分组:golang基础 · 源标题
p003-h03。
数组,组固定长度。数组长度是数组类型的一部分,所以[3]int 和[4]int 是两种不同的数组类型数组需要指定大小,不指定也会根据初始化,自动推算出大小,大小不可改变。数组是通过值传递的
切片,可以改变长度。切片是轻量级的数据结构,三个属性,指针,长度,容量不需要指定大小切片是地址传递(引用传递)可以通过数组来初始化,也可以通过内置函数 make()来初始化,初始化的时候 len=cap,然后进行扩容
拷贝大切片一定比小切片代价大吗?
PDF 出处:第 3–3 页 · 原卷分组:golang基础 · 源标题
p003-h04。
切片底层结构
go
type SliceHeader struct {
Data uintptr
Len int
Cap int
}所有切片的大小相同;三个字段(一个 uintptr,两个int)。切片中的第一个字是指向切片底层数组的指针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。将一个 slice 变量分配给另一个变量只会复制三个机器字。所以拷贝大切片跟小切片的代价应该是一样的。
json包变量不加tag会怎么样?
PDF 出处:第 3–3 页 · 原卷分组:golang基础 · 源标题
p003-h05。
如果变量首字母小写,则为private。无论如何不能转,因为json包里认为私有变量为不可导出的
如果变量首字母大写,则为public,不加tag,可以正常转为json里的字段,json内字段名跟结构体内字段原名一致
如果变量首字母大写,则为public,加了tag,从struct转json的时候,json的字段名就是tag里的字段名,原字段名已经没用
Go 语言当中数组和切片在传递的时候的区别是什么?
PDF 出处:第 4–4 页 · 原卷分组:golang基础 · 源标题
p004-h01。
数组是值传递
切片看上去像是引用传递,但其实是值传递
Go 语言是如何实现切片扩容的?
PDF 出处:第 4–4 页 · 原卷分组:golang基础 · 源标题
p004-h03。
go
func main() {
arr := make([]int, 0)
for i := 0; i < 2000; i++ {
fmt.Println("len 为", len(arr), "cap 为", cap(arr))
arr = append(arr, i)
}
}我们可以看下结果依次是 0,1,2,4,8,16,32,64,128,256,512,1024但到了 1024 之后,就变成了 1024,1280,1696,2304每次都是扩容了四分之一左右
Golang Slice 的底层实现
PDF 出处:第 5–5 页 · 原卷分组:golang基础 · 源标题
p005-h01。
切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。切片对象非常小,是因为它是只有 3 个字段的数据结构:
指向底层数组的指针
切片的长度
切片的容量
Golang Slice 的扩容机制,有什么注意点?
PDF 出处:第 5–5 页 · 原卷分组:golang基础 · 源标题
p005-h02。
Go 中切片扩容的策略是这样的:
首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容量
否则判断,如果旧切片的长度小于 1024,则最终容量就是旧容量的两倍
否则判断,如果旧切片长度大于等于 1024,则最终容量从旧容量开始循环增加原来的 1/4, 直到最终容量大于等于新申请的容量
如果最终容量计算值溢出,则最终容量就是新申请容量
扩容前后的 Slice 是否相同?
PDF 出处:第 5–5 页 · 原卷分组:golang基础 · 源标题
p005-h03。
情况一:原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址的 Slice。情况二:原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。要复制一个 Slice,最好使用 Copy函数。
Golang 的参数传递、引用类型
PDF 出处:第 5–5 页 · 原卷分组:golang基础 · 源标题
p005-h04。
Go 语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct 等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan 等这些),这样就可以修改原内容数据。 Golang 的引用类型包括 slice、map和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针
Golang Map 底层实现
PDF 出处:第 6–6 页 · 原卷分组:golang基础 · 源标题
p006-h01。
Golang 中 map 的底层实现是一个散列表,因此实现 map 的过程实际上就是实现散表的过程。在这个散列表中,主要出现的结构体有两个,一个叫 hmap(a header for a go map),一个叫 bmap(a bucket for a Go map,通常叫其 bucket)。
Golang Map 如何扩容
PDF 出处:第 6–6 页 · 原卷分组:golang基础 · 源标题
p006-h02。
双倍扩容:扩容采取了一种称为“渐进式”的方式,原有的 key 并不会一次性搬迁完毕,每次最多只会搬迁 2 个 bucket
等量扩容:重新排列,极端情况下,重新排列也解决不了,map 存储就会蜕变成链表,性能大大降低,此时哈希因子 hash0 的设置,可以降低此类极端场景的发生
Golang Map 查找
PDF 出处:第 6–6 页 · 原卷分组:golang基础 · 源标题
p006-h03。
Go 语言中 map 采用的是哈希查找表,由一个 key 通过哈希函数得到哈希值,64 位系统中就生成一个64bit 的哈希值,由这个哈希值将 key 对应存到不同的桶(bucket)中,当有多个哈希映射到相同的的桶中时,使用链表解决哈希冲突。细节:key 经过 hash 后共 64 位,根据 hmap 中 B 的值,计算它到底要落在哪个桶时,桶的数量为 2^B,如 B=5,那么用 64 位最后 5 位表示第几号桶,在用 hash 值的高 8 位确定在 bucket 中的存储位置,当前 bmap 中的 bucket 未找到,则查询对应的 overflow bucket,对应位置有数据则对比完整的哈希值,确定是否是要查找的数据。如果当前 map 处于数据搬移状态,则优先从 oldbuckets 查找。
nil切片和空切片的区别
PDF 出处:第 15–15 页 · 原卷分组:代码分析 · 源标题
p015-h01。
nil切片表示该切片尚未初始化,仅有一个类型声明,并不存在示例。引用数组指针地址为0。
空切片表示该切片以及被初始化,有一个切实存在的对象,只是该切片的元素个数为0。引用的数组指针地址也是一个存在的内存地址
例如:
go
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
var s1 []int
s2 := make([]int, 0)
s4 := make([]int, 0)
fmt.Printf("s1 pointer:%+v, s2 pointer:%+v, s4 pointer:%+v, \n", *(*reflect.SliceHeader)(unsafe.Pointer(&s1)), *(*reflect.SliceHeader)(unsafe.Pointer(&s2)), *(*reflect.SliceHeader)(unsafe.Pointer(&s4)))
fmt.Printf("%v\n", (*(*reflect.SliceHeader)(unsafe.Pointer(&s1))).Data == (*(*reflect.SliceHeader)(unsafe.Pointer(&s2))).Data)
fmt.Printf("%v\n", (*(*reflect.SliceHeader)(unsafe.Pointer(&s2))).Data == (*(*reflect.SliceHeader)(unsafe.Pointer(&s4))).Data)
}切片数据结构
go
type SliceHeader struct {
Data uintptr //引用数组指针地址
Len int // 切片的目前使用长度
Cap int // 切片的容量
}如何获取结构体tag信息
PDF 出处:第 16–17 页 · 原卷分组:代码分析 · 源标题
p016-h01。
tag 信息可以通过反射(reflect包)获取。示例代码:
go
package main
import (
"fmt"
"reflect"
)
type J struct {
a string //小写无tag
b string `json:"B"` //小写+tag
C string //大写无tag
D string `json:"DD" otherTag:"good"` //大写+tag
}
func printTag(stru interface{}) {
t := reflect.TypeOf(stru).Elem()
for i := 0; i < t.NumField(); i++ {
fmt.Printf("结构体内第%v个字段 %v 对应的json tag是 %v , 还有otherTag? = %v\n", i+1, t.Field(i).Name, t.Field(i).Tag.Get("json"),
t.Field(i).Tag.Get("otherTag"))
}
}
func main() {
j := J{
a: "1",
b: "2",
C: "3",
D: "4",
}
printTag(&j)
}go struct能不能比较
PDF 出处:第 22–22 页 · 原卷分组:代码分析 · 源标题
p022-h01。
相同struct类型的可以比较
不同struct类型的不可以比较,编译都不过,类型不匹配
go
package main
import "fmt"
func main() {
type A struct {
a int
}
type B struct {
a int
}
a := A{1}
//b := A{1}
b := B{1}
if a == b {
fmt.Println("a == b")
} else {
fmt.Println("a != b")
}
}
// output
// command-line-arguments [command-line-arguments.test]
// ./.go:14:7: invalid operation: a == b (mismatched types A and B)