(计算机基础 核心知识)Go
1.Go 允许多个返回值吗?
可以。通常函数除了一般返回值还会返回一个error。
2.Go 有异常类型吗?
有。Go用error类型代替try...catch语句,这样可以节省资源。同时增加代码可读性:
_, err := funcDemo() if err != nil { fmt.Println(err) return }
也可以用errors.New()来定义自己的异常。errors.Error()会返回异常的字符串表示。只要实现error接口就可以定义自己的异常,
type errorString struct { s string } func (e *errorString) Error() string { return e.s } // 多一个函数当作构造函数 func New(text string) error { return &errorString{text} }
3.什么是协程(Goroutine)
协程是用户态轻量级线程,它是线程调度的基本单位。通常在函数前加上go关键字就能实现并发。一个Goroutine会以一个很小的栈启动2KB或4KB,当遇到栈空间不足时,栈会自动伸缩, 因此可以轻易实现成千上万个goroutine同时启动。
4.什么是 rune 类型
ASCII 码只需要 7 bit 就可以完整地表示,但只能表示英文字母在内的128个字符,为了表示世界上大部分的文字系统,发明了 Unicode, 它是ASCII的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。
Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。
在Go中,byte是uint8的别名,它们是同一种类型的两个不同的名字²⁴⁵。它们都是8位无符号整数,但是按照惯例,byte用来表示字节值,而uint8用来表示整数值²。
字符串是一个不可变的字节切片,它可以存储任意的字节,不一定是有效的UTF-8编码。如果要访问字符串中的单个字节,可以通过下标索引[,例如str 0]。如果要遍历字符串中的Unicode字符(rune),可以使用for range循环,例如for _, r := range str {}。这样每次迭代都会返回一个rune类型的值。这里有一个例子:
package main import "fmt" func main() { s := "你好, world!" fmt.Println("len(s):", len(s)) // len(s): 13 for i := 0; i < len(s); i++ { fmt.Printf("%d: %x\n", i, s[i]) // 打印每个字节的十六进制值 } for i, r := range s { fmt.Printf("%d: %c\n", i, r) // 打印每个rune的字符值 } }
5.Go 支持默认参数或可选参数吗?
不支持。但有可变参数
// 这个函数可以传入任意数量的整型参数 func sum(nums ...int) { total := 0 for _, num := range nums { total += num } fmt.Println(total) }
6.defer 的执行顺序
defer执行顺序和调用顺序相反,类似于栈后进先出(LIFO)。
defer在return之后执行,但在函数退出之前,defer可以修改返回值。下面是一个例子:
func test() int { i := 0 defer func() { fmt.Println("defer1") }() defer func() { i += 1 fmt.Println("defer2") }() return i } func main() { fmt.Println("return", test()) } // defer2 // defer1 // return 0
上面这个例子中,test返回值并没有修改,这是由于Go的返回机制决定的,执行Return语句后,Go会创建一个临时变量保存返回值。如果是有名返回(也就是指明返回值func test() (i int)
)
func test() (i int) { i = 0 defer func() { i += 1 fmt.Println("defer2") }() return i } func main() { fmt.Println("return", test()) } // defer2 // return 1
这个例子中,返回值被修改了。对于有名返回值的函数,执行 return 语句时,并不会再创建临时变量保存,因此,defer 语句修改了 i,即对返回值产生了影响
7.Go 语言 tag 的用处?
tag可以为结构体成员提供属性。常见的:
- json序列化或反序列化时字段的名称
- db: sqlx模块中对应的数据库字段名
- form: gin框架中对应的前端的数据字段名
- binding: 搭配 form 使用, 默认如果没查找到结构体中的某个字段则不报错值为空, binding为 required 代表没找到返回错误给前端
可以用反射获取结构体tag
8.结构体打印时,%v
和 %+v
的区别
%v
输出结构体各成员的值;
%+v输出结构体各成员的名称和值;
%#v
输出结构体名称和结构体各成员的名称和值
9.iota
根据我从网上搜索到的信息,go的iota用法是在常量声明中表示连续的无类型整数常量¹。它的值是该常量声明中相应的ConstSpec的索引,从零开始¹。给个例子:
package main import "fmt" const ( a = iota // a = 0 b // b = 1 c // c = 2 ) const ( d = iota + 1 // d = 1 e // e = 2 f // f = 3 ) const ( g = 1 << iota // g = 1 (iota = 0) h // h = 2 (iota = 1) i // i = 4 (iota = 2) ) func main() { fmt.Println(a, b, c) // 0 1 2 fmt.Println(d, e, f) // 1 2 3 fmt.Println(g, h, i) // 1 2 4 }
10.go里面的int和int32是同一个概念吗?
不是一个概念!千万不能混淆**。go语言中的int的大小是和操作系统位数相关的,如果是32位操作系统,int类型的大小就是4字节。如果是64位操作系统,int类型的大小就是8个字节。除此之外uint也与操作系统有关。**
int8占1个字节,int16占2个字节,int32占4个字节,int64占8个字节。
11.init() 函数是什么时候执行的?
简答: 在main函数之前执行。
详细:init()函数是go初始化的一部分,由runtime初始化每个导入的包,初始化不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。
每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的init()
函数。同一个包,甚至是同一个源文件可以有多个init()
函数。init()
函数没有入参和返回值,不能被其他函数调用,同一个包内多个init()
函数的执行顺序不作保证。
执行顺序:import –> const –> var –>init()
–>main()
一个文件可以有多个init()
函数!
好的,我会尝试生成一个init()函数的例子代码。请看:
package main import ( "fmt" ) var x int = 10 // 包级别的变量初始化 func init() { fmt.Println("init 1") // init()函数执行 x = 20 // 修改包级别的变量 } func init() { fmt.Println("init 2") // 另一个init()函数执行 } func main() { fmt.Println("main") // main()函数执行 fmt.Println(x) // 输出包级别的变量 }
输出:
init 1 init 2 main 20
12.如何知道一个对象是分配在栈上还是堆上?
Go和C++不同,Go局部变量会进行逃逸分析。如果变量离开作用域后没有被引用,则优先分配到栈上,否则分配到堆上。那么如何判断是否发生了逃逸呢?
go build -gcflags '-m -m -l' xxx.go
.
关于逃逸的可能情况:变量大小不确定,变量类型不确定,变量分配的内存超过用户栈最大值,暴露给了外部指针。
13.两个 nil 可能不相等吗?
go中两个nil可能不相等。这是因为interface类型的变量是由类型T和值V组成的,只有当T和V都是nil的时候,interface才等于nil。如果interface的T是一个指针类型,而V是一个指针的nil值,那么interface就不等于nil。例如:
type A interface{} type B struct{} var a A = (*B) (nil) fmt.Println(a == nil) //false
这里a的T是B,而V是(B) (nil),所以a不等于nil。但是如果a的T和V都是nil,那么a就等于nil。例如:
var a A = nil fmt.Println(a == nil) //true
14.简述 Go 语言GC(垃圾回收)的工作原理
垃圾回收机制是Go一大特(nan)色(dian)。Go1.3采用标记清除法, Go1.5采用三色标记法,Go1.8采用三色标记法+混合写屏障。
标记清除法
分为两个阶段:标记和清除
标记阶段:从根对象出发寻找并标记所有存活的对象。
清除阶段:遍历堆中的对象,回收未标记的对象,并加入空闲链表。
缺点是需要暂停程序STW。
标记清除法是一种垃圾回收算法,它分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器会遍历所有的可达对象,并给它们打上标记。在清除阶段,垃圾回收器会清理掉没有被标记的对象,即不可达的对象¹²。
STW是Stop-The-World的缩写,意思是在垃圾回收的时候停止用户逻辑的运行。这是为了避免在标记清除过程中,用户逻辑对对象的引用发生变化,导致标记结果不准确或者错误³⁴。例如,如果在标记阶段后,用户逻辑创建了一个新对象,并且与已经被标记的对象有引用关系,那么这个新对象就可能被错误地回收掉³⁴。
标记清除法需要STW的原因是为了保证标记清除的正确性和一致性。如果不STW,那么就需要引入额外的机制来处理并发情况下的对象引用变化,例如写屏障(write barrier)⁵⁶。写屏障是一种在写入对象引用时触发的操作,用来保证被引用的对象不会被错误地回收或者遗漏⁵⁶。
*三色标记法*:
将对象标记为白色,灰色或黑色。
白色:不确定对象(默认色);黑色:存活对象。灰色:存活对象,子对象待处理。
标记开始时,先将所有对象加入白色集合(需要STW)。首先将根对象标记为灰色,然后将一个对象从灰色集合取出,遍历其子对象,放入灰色集合。同时将取出的对象放入黑色集合,直到灰色集合为空。最后的白色集合对象就是需要清理的对象。
这种方法有一个缺陷,如果对象的引用被用户修改了,那么之前的标记就无效了。因此Go采用了写屏障技术,当对象新增或者更新会将其着色为灰色。
写屏障技术的基本思想是,在对象引用发生写操作时,触发一些额外的操作,来通知垃圾回收器,或者修改对象的颜色,或者将对象加入到某个集合中。这样,垃圾回收器就可以根据写屏障的信息,来调整自己的扫描过程,保证不会遗漏或错误回收任何对象。
一次完整的GC分为四个阶段:
- 准备标记(需要STW),开启写屏障。
- 开始标记
- 标记结束(STW),关闭写屏障
- 清理(并发)
基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,带来性能瓶颈。混合写屏障分为以下四步:
- GC开始时,将栈上的全部对象标记为黑色(不需要二次扫描,无需STW);
- GC期间,任何栈上创建的新对象均为黑色
- 被删除引用的对象标记为灰色
- 被添加引用的对象标记为灰色
总而言
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
曾获多国内大厂的 ssp 秋招 offer,且是Java5年的沉淀老兵(不是)。专注后端高频面试与八股知识点,内容系统详实,覆盖约 30 万字面试真题解析、近 400 个热点问题(包含大量场景题),60 万字后端核心知识(含计网、操作系统、数据库、性能调优等)。同时提供简历优化、HR 问题应对、自我介绍等通用能力。考虑到历史格式混乱、质量较低、也在本地积累了大量资料,故准备从头重构专栏全部内容