(计算机基础 核心知识)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可以为结构体成员提供属性。常见的:

  1. json序列化或反序列化时字段的名称
  2. db: sqlx模块中对应的数据库字段名
  3. form: gin框架中对应的前端的数据字段名
  4. 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分为四个阶段:

  1. 准备标记(需要STW),开启写屏障。
  2. 开始标记
  3. 标记结束(STW),关闭写屏障
  4. 清理(并发)

基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,带来性能瓶颈。混合写屏障分为以下四步:

  1. GC开始时,将栈上的全部对象标记为黑色(不需要二次扫描,无需STW);
  2. GC期间,任何栈上创建的新对象均为黑色
  3. 被删除引用的对象标记为灰色
  4. 被添加引用的对象标记为灰色

总而言

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

曾获多国内大厂的 ssp 秋招 offer,且是Java5年的沉淀老兵(不是)。专注后端高频面试与八股知识点,内容系统详实,覆盖约 30 万字面试真题解析、近 400 个热点问题(包含大量场景题),60 万字后端核心知识(含计网、操作系统、数据库、性能调优等)。同时提供简历优化、HR 问题应对、自我介绍等通用能力。考虑到历史格式混乱、质量较低、也在本地积累了大量资料,故准备从头重构专栏全部内容

全部评论

相关推荐

吴offer选手:HR:我KPI到手了就行,合不合适关我什么事
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务