Skip to content

Go 基础、类型与数据结构 ​

返回完整题目索引

来源:用户提供的《golang 云原生面试题》PDF。下列为原文完整导入,按主题重排;页码为 PDF 物理页码。技术观点与示例保留原件语境,勘误请结合 校正说明 阅读。

与其他语言相比,使用 Go 有什么好处? ​

PDF 出处:第 1–1 页 · 原卷分组:golang基础 · 源标题 p001-h02。

  1. 与其他作为学术实验开始的语言不同,Go 代码的设计是务实的。每个功能和语法决策都旨在让程序员的生活更轻松。

  2. Golang 针对并发进行了优化,并且在规模上运行良好。

  3. 由于单一的标准代码格式,Golang 通常被认为比其他语言更具可读性。

  4. 自动垃圾收集明显比 Java 或 Python 更有效,因为它与程序同时执行。

Golang 使用什么数据类型? ​

PDF 出处:第 1–1 页 · 原卷分组:golang基础 · 源标题 p001-h03。

  1. Method

  2. Boolean

  3. Numeric

  4. String

  5. Array

  6. Slice

  7. Struct

  8. Pointer

  9. Function

  10. Interface

  11. Map

  12. 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。

  1. array(数组)

  2. slice(切片)

  3. channel(通道)

输入 non-empty interface ​

PDF 出处:第 2–2 页 · 原卷分组:golang基础 · 源标题 p002-h04。

  1. 接口除了有静态类型,还有动态类型和动态值,当且仅当动态值和动态类型都为 nil 时,接口类型值才为 nil。这里的 x 的动态类型是 *int,所以 x 不为 nil

  2. 但是在此案例中,直接在main函数中的*int类型的x的值是nil

Go 语言当中 new 的作用是什么 ​

PDF 出处:第 2–2 页 · 原卷分组:golang基础 · 源标题 p002-h05。

new 创建一个该类型的实例,并且返回指向该实例的指针。new 函数是内建函数,函数定义: func new(Type) *Type

  1. 使用 new 函数来分配空间

  2. 传递给 new 函数的是一个类型,而不是一个值

  3. 返回值是指向这个新分配的地址的指针

Go 语言中 make 的作用是什么 ​

PDF 出处:第 2–2 页 · 原卷分组:golang基础 · 源标题 p002-h06。

make 的作用是为 slice, map or chan 的初始化然后返回引用 make 函数是内建函数,函数定义:

go
func make(Type, size IntegerType) Type

make(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。

  1. Printf 是标准输出,一般是屏幕,也可以重定向

  2. Sprintf()是把格式化字符串输出到指定的字符串中

  3. Fprintf()是把格式化字符串输出到文件中

Go 语言当中数组和切片的区别是什么? ​

PDF 出处:第 3–3 页 · 原卷分组:golang基础 · 源标题 p003-h03。

  1. 数组,组固定长度。数组长度是数组类型的一部分,所以[3]int 和[4]int 是两种不同的数组类型数组需要指定大小,不指定也会根据初始化,自动推算出大小,大小不可改变。数组是通过值传递的

  2. 切片,可以改变长度。切片是轻量级的数据结构,三个属性,指针,长度,容量不需要指定大小切片是地址传递(引用传递)可以通过数组来初始化,也可以通过内置函数 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。

  1. 如果变量首字母小写,则为private。无论如何不能转,因为json包里认为私有变量为不可导出的

  2. 如果变量首字母大写,则为public,不加tag,可以正常转为json里的字段,json内字段名跟结构体内字段原名一致

  3. 如果变量首字母大写,则为public,加了tag,从struct转json的时候,json的字段名就是tag里的字段名,原字段名已经没用

Go 语言当中数组和切片在传递的时候的区别是什么? ​

PDF 出处:第 4–4 页 · 原卷分组:golang基础 · 源标题 p004-h01。

  1. 数组是值传递

  2. 切片看上去像是引用传递,但其实是值传递

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 个字段的数据结构:

  1. 指向底层数组的指针

  2. 切片的长度

  3. 切片的容量

Golang Slice 的扩容机制,有什么注意点? ​

PDF 出处:第 5–5 页 · 原卷分组:golang基础 · 源标题 p005-h02。

Go 中切片扩容的策略是这样的:

  1. 首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容量

  2. 否则判断,如果旧切片的长度小于 1024,则最终容量就是旧容量的两倍

  3. 否则判断,如果旧切片长度大于等于 1024,则最终容量从旧容量开始循环增加原来的 1/4, 直到最终容量大于等于新申请的容量

  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。

  1. 双倍扩容:扩容采取了一种称为“渐进式”的方式,原有的 key 并不会一次性搬迁完毕,每次最多只会搬迁 2 个 bucket

  2. 等量扩容:重新排列,极端情况下,重新排列也解决不了,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。

  1. nil切片表示该切片尚未初始化,仅有一个类型声明,并不存在示例。引用数组指针地址为0。

  2. 空切片表示该切片以及被初始化,有一个切实存在的对象,只是该切片的元素个数为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。

  1. 相同struct类型的可以比较

  2. 不同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)