抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

记录Golang面试八股文

Go基础

init和main函数的相关特点

init函数(没有输入参数、返回值)的主要作用:

  • 初始化不能采用初始化表达式初始化的变量;
  • 在程序运行前注册;
  • 实现sync.Once功能;
  • 其他

init顺序

  1. 在同一个package中, 可以多个文件中定义init方法;
  2. 在同一个go文件中, 可以重复定义init方法;
  3. 在同一个package中, 不同文件中的init方法执行按照文件名先后执行各个文件中的init方法;
  4. 在同一个文件中的多个init方法, 按照在代码中编写的顺序依次执行不同的init方法;
  5. 对于不同的package, 如果不相互依赖的话, 按照main包中import的顺序调用其包中的init()函数;
  6. 如果package存在依赖, 调用顺序为最后被依赖的最先被初始化, 例如: 导入顺序main -> A -> B -> C, 则初始化顺序为C -> B -> A -> main, 一次执行对应的init方法;

Golang的数据结构的零值是什么?

所有整型类型: 0
浮点类型: 0.0
布尔类型: false
字符串类型: “”
指针、interface、切片(slice)、channel、map、function: nil

Go的零值初始是递归的, 即数组、结构体等类型的零值初始化就是对其组成元素逐一进行零值初始化

byte和rune有什么区别

  • runebyte在Go语言中都是字符类型, 且都是别名类型;
  • byte型本质上是uint8类型的别名, 代表了ASCII码的一个字符;
  • rune型本质上是int32型的别名, 代表一个UTF-8字符;

Go struct 能不能比较

需要根据具体情况分析, 如果struct中含有不能被比较的字段类型, 就不能被比较;
如果struct中所有的字段类型都支持比较, 那么就支持比较;

不能被比较的类型

  • slice, 因为slice是引用类型, 除非是和nil比较;
  • map, 和slice同理, 如果要比较两个map只能通过循环遍历实现;
  • 函数类型;

注意

  • 结构体之间只能比较它们是否相等, 而不能比较它们的大小;
  • 只能所有属性相等而且属性顺序一致的结构体才能进行比较;

Go import的三种方式

加下划线

import下划线(如:_ “github.com/xxx/xxx”)的作用: 当导入一个包时, 该包下的文件里所有的init()函数都会被执行。然而有些时候我们并不需要把整个包都导入进来, 仅仅是希望它执行init()函数而已。这个时候就可以使用import _引用该包。
即: 使用[import _ 包路径]只是引用该包, 仅仅是为了调用init函数, 所以无法通过包名来调用包中的其他函数;

加点(.)

import和引用的包名之间加点(.)操作的含义就是这个包导入之后在调用这个包的函数时, 可以省略前戳的包名;

别名

别名操作可以把包命名成另一个用起来容易记忆的名字;

Golang的常量地址

1
2
3
4
5
6
7
8
9
const i = 100

var j = 123

func main() {
fmt.Println(&j, j)
fmt.Println(&i, i) // panic
// Go语言中, 常量无法寻址, 是不能进行取指针操作的
}

string和[]byte如何取舍

string擅长的场景:

  • 需要字符串比较的场景;
  • 不需要nil字符串的场景;

[]byte擅长的场景

  • 修改字符串的场景, 尤其是修改粒度为1个字节;
  • 函数返回值, 需要用nil表示含义的场景;
  • 需要切片操作的场景;

字符串转成byte数组, 会发生内存拷贝吗

字符串转成切片, 会产生拷贝。严格来说, 只要是发生类型强化转都会发生内存拷贝。频繁的内存拷贝操作听起来对性能不太友好。有没有什么办法可以在字符串转成切片的时候不用发生拷贝呢?

代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"reflect"
"unsafe"
)

func main() {
a := "aaa"
ssh := *(*reflect.StringHeader)(unsafe.Pointer(&a))
b := *(*[]byte)(unsafe.Pointer(&ssh))
fmt.Printf("%v", b)
}

解释

  • StringHeader字符串在go的底层结构

    1
    2
    3
    4
    type StringHeader struct {
    Data uintptr
    Len int
    }
  • SliceHeader切片在go的底层结构

    1
    2
    3
    4
    5
    type SliceHeader struct {
    Data uintptr
    Len int
    Cap int
    }
  • 那么如果想要在底层转换二者, 只需要把StringHeader的地址强转成SliceHeader就行。那么go有个包很强的包叫unsafe

    1. unsafe.Pointer(&a)方法可以得到变量a的地址;
    2. (*reflect.StringHeader)(unsafe.Pointer(&a))可以把字符串a转成底层结构的形式;
    3. (*[]byte)(unsafe.Pointer(&ssh))可以把ssh底层结构转成byte的切片的指针;
    4. 再通过*转成指针指向的实际内容;

翻转含有中文、数字、英文字符的字符串

翻转含有中文、数字、英文字母的字符串

代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
src := "你好abc啊哈哈"
dst := reverse([]rune(src))
fmt.Printf("%v\n", string(dst))
}

func reverse(s []rune) []rune {
for i, j := 0, len(s)-1;i<j; i,j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return s
}

解释

  • rune关键字, 从golang源码中看出, 它是int32的别名(-2^31 ~ 2^31-1), 比如byte(-128 ~ 127), 可表示更多的字符。
  • 由于rune可表示的范围更大, 所以能处理一切字符, 当然也包括中文字符。在平时计算中文字符, 可用rune。
  • 因此将字符串转成rune的切片, 再进行翻转;

json包变量不加tag会怎么样?

json包里使用的时候, 结构体里的变量不加tag能不能正常转成json里的字段?

回答

  • 如果变量首字母小写, 则为private。无论如何不能转, 因为取不到反射信息
  • 如果变量首字母大写, 则为public:
    1. 不加tag, 可以正常转为json里的字段, json内字段名跟结构体内字段原名一致;
    2. 加了tag, 从structjson的时候, json的字段名就是tag里的字段名, 原字段名已经没用;

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"encoding/json"
"fmt"
)

type J struct {
a string // 小写无tag
b string `json:"B"` // 小写加tag
C string // 大写无tag
D string `json:"DD"` // 大写加tag
}

func main() {
j := J {
a: "1",
b: "2",
C: "3",
D: "4",
}
fmt.Printf("转为json前j结构体的内容 = %+v\n", j) // 转为json前j结构体的内容 = { a:1 b:2 C:3 D:4 }
jsonInfo, _ := json.Marshal(j)
fmt.Printf("转为json后的内容 = %+v\n", string(jsonInfo)) // 转为json后的内容 = { "C": "3", "DD": "4" }
}

解释

  • 结构体里定义了四个字段, 分别对应小写无tag, 小写加tag大写无tag大写加tag;
  • 转为json后首字母小写的不管加不加tag都不能转为json里的内容, 而大写的加了tag可以取别名, 不加tagjson内的字段跟结构体字段原名一致;

Go语言中cap函数可以作用于哪些内容?

  • array返回数组的元素个数
  • slice返回slice的最大容量
  • channel返回chennel的容量

Go语言的引用类型有什么?

Go语言中的引用类型有func(函数类型)、interface(接口类型)、slice(切片类型)、map(字典类型)、channel(管道类型)、*(指针类型)

for-select, 如果通道已经关闭会怎么样?如果select中只有一个case呢?

for循环select时, 如果通道已经关闭会怎么样?如果select中的case只有一个, 又会怎么样?

回答

  • for循环select时, 如果其中一个case通道已经关闭, 则每次都会执行到这个case;
  • 如果select里边只有一个case, 而这个case被关闭了, 则会出现死循环;

解释

  1. for循环里被关闭的通道
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package main

    import (
    "fmt"
    "time"
    )

    const (
    fmat = "2006-01-02 15:04:05"
    )

    func main() {
    c := make(chan int)
    go func() {
    time.Sleep(1 * time.Second)
    c <- 10
    close(c)
    }()

    for {
    select {
    case x, ok := <-c:
    fmt.Printf("%v, 通道读取到: x=%v, ok=%v\n",time.Now().Format(fmat) x, ok)
    time.Sleep(500 * time.Millisecond)
    default:

    }
    }
    }
    输出结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    2024-02-27 22:29:44, 没读到信息进入default
    2024-02-27 22:29:44, 没读到信息进入default
    2024-02-27 22:29:45, 通道读取到: x=10, ok=true
    2024-02-27 22:29:45, 通道读取到: x=0, ok=false
    2024-02-27 22:29:46, 通道读取到: x=0, ok=false
    2024-02-27 22:29:46, 通道读取到: x=0, ok=false
    2024-02-27 22:29:47, 通道读取到: x=0, ok=false
    2024-02-27 22:29:47, 通道读取到: x=0, ok=false
    2024-02-27 22:29:48, 通道读取到: x=0, ok=false
    2024-02-27 22:29:48, 通道读取到: x=0, ok=false
    2024-02-27 22:29:49, 通道读取到: x=0, ok=false
    2024-02-27 22:29:49, 通道读取到: x=0, ok=false
    2024-02-27 22:29:50, 通道读取到: x=0, ok=false
    2024-02-27 22:29:50, 通道读取到: x=0, ok=false
    2024-02-27 22:29:51, 通道读取到: x=0, ok=false
    2024-02-27 22:29:51, 通道读取到: x=0, ok=false
  • c通道是一个缓冲为0的通道, 在main开始时, 启动一个协程对c通道写入10, 然后就关闭掉这个通道;
  • main中通过通过x, ok := <-c接受通道c里的值, 从输出结果里看出, 确实从通道里读出了之前塞入通道的10, 但是这个通道关闭后, 这个通道一直能读出内容;
  1. 怎样才能不读关闭后的通道
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package main

    import (
    "fmt"
    "time"
    )

    const (
    fmat = "2006-01-02 15:04:05"
    )

    func main() {
    c := make(chan int)
    go func() {
    time.Sleep(1 * time.Second)
    c <- 10
    close(c)
    }()

    for {
    select {
    case x, ok := <-c:
    fmt.Printf("%v, 通道读取到: x=%v, ok=%v\n", time.Now().Format(fmat), x, ok)
    time.Sleep(500 * time.Millisecond)
    if !ok {
    c = nil // 把关闭后的通道赋值为nil, 则select读取则会阻塞
    }
    default:
    fmt.Printf("%v, 没读到信息进入default\n", time.Now().Format(fmat))
    time.Sleep(500 * time.Millisecond)
    }
    }
    }
    输出结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    2024-02-27 23:08:06, 没读到信息进入default
    2024-02-27 23:08:07, 没读到信息进入default
    2024-02-27 23:08:07, 通道读取到: x=10, ok=true
    2024-02-27 23:08:08, 通道读取到: x=0, ok=false
    2024-02-27 23:08:08, 没读到信息进入default
    2024-02-27 23:08:09, 没读到信息进入default
    2024-02-27 23:08:09, 没读到信息进入default
    2024-02-27 23:08:10, 没读到信息进入default
    2024-02-27 23:08:10, 没读到信息进入default
    2024-02-27 23:08:11, 没读到信息进入default

go的内存逃逸是什么? 什么情况下会发生内存逃逸?

回答

golang程序变量会携带有一组校验数据, 用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验, 它就可以在栈上分配, 否则就说它逃逸了, 必须在堆上分配。

能引起变量逃逸到堆上的典型情况:

  • 在方法内把局部变量指针返回: 局部变量原本应该在栈中分配, 在栈中回收。但是由于返回时被外部调用, 因此其生命周期大于栈, 则溢出;
  • 发送指针或带有指针的值到chennel中: 在编译时, 是没有办法知道哪个goroutinue会在channel上接收数据。所以编译器没法知道变量什么时候才会释放;
  • 在一个切片上存储指针或带指针的值: 一个典型的例子就是[]*string。这会导致切片内容逃逸。尽管其后面的数组可能是在栈上分配的, 但其引用的值一定是在堆上;
  • slice的背后数组被重新分配了, 因为append时可能会超出其容量(cap): slice初始化的地方在编译时是可以知道的, 它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充, 就会在堆上分配;
  • 在interface类型上调用方法: 在interface类型上调用方法都是动态调度的–方法的真正实现只能在运行时知道。想象一个io.Reader类型的变量r, 调用r.Read(b)会使得r的值和切片b背后存储的数据逃逸掉, 所以会在堆上分配;

代码举例

通过一个例子加深理解, 接下来尝试怎么通过go build -gcflags=-m查看逃逸的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type A struct {
s string
}

func foo(s string) *A {
a := new(A)
a.s = s
return a // 返回局部变量a, 在C语言为野指针, 在golang则ok但a会逃逸到堆
}

func main() {
a := foo("hello")
b := a.s + "world"
c := b + "!"

fmt.Println(c)
}

执行go build -gcflags=-m test_Demo_01.go

1
2
3
4
5
6
7
8
9
10
11
12
go build -gcflags=-m test_Demo_01.go
# command-line-arguments
./test_Demo_01.go:9:6: can inline foo
./test_Demo_01.go:16:10: inlining call to foo
./test_Demo_01.go:20:13: inlining call to fmt.Println
./test_Demo_01.go:9:10: leaking param: s
./test_Demo_01.go:10:10: new(A) escapes to heap
./test_Demo_01.go:16:10: new(A) does not escape
./test_Demo_01.go:17:11: a.s + "world" does not escape
./test_Demo_01.go:18:9: b + "!" escapes to heap
./test_Demo_01.go:20:13: ... argument does not escape
./test_Demo_01.go:20:14: c escapes to heap
  • ./test_Demo_01.go:10:10: new(A) escapes to heap说明new(A)逃逸了, 符合上述提到的常见情况的第一种;
  • ./test_Demo_01.go:17:11: a.s + "world" does not escape 说明b变量没有逃逸, 因为它只存在方法内存中, 会在方法结束时被回收;
  • ./test_Demo_01.go:18:9: b + "!" escapes to heap说明c变量逃逸, 通过fmt.Println(a ...interface{})打印的变量, 都会发生逃逸;

Go关键字fallthrough有什么作用

fallthrough关键字只能用在switch中, 且只能在每一个case分支中的最后一行, 作用是如果这个case分支被执行, 将继续执行下一个case分支, 而且不会取判断下一个分支额case条件是否成立;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import main

func main() {
switch "a" {
case "a"
fmt.Println("匹配a")
fallthrough
case "b":
fmt.Println("a成功了, 也执行b分支")
case "c":
fmt.Println("a成功了, c分支会执行吗")
default:
fmt.Println("默认执行")
}
}

/**
匹配a
a成功了, 也执行b分支
*/

空结构体占不占内存空间? 为什么使用空结构体?

空结构体是没有内存大小的结构体;

通过unsafe.Sizeof()可以查看空结构体的宽度, 代码如下:

1
2
var s struct{}
fmt.Println(unsafe.Sizeof(s)) // printfs 0

准确的来说, 空结构体有一个特殊起点: zerobase变量; zerobase是一个占用8个字节的uintptr全局变量。每次定义struct{}类型的变量, 编译器只是把zerobase变量的地址给出去。也就是说空结构体的变量的内存地址都是一样的。

空结构体的使用场景主要有三种:

  • 实现方法接收者: 在业务场景下, 我们需要将方法组合起来, 代表其一个”分组”的, 便于后续拓展和维护;
  • 实现集合类型: 在Go语言的标准库中并没有提供集合(Set)的相关实现, 因此一般在代码中图方便, 会直接用map来替代: type Set map[string]struct{}
  • 实现通道: 在Go channel的使用场景中, 常常会遇到通知型channel, 其不需要发送任何数据, 只是用于协调Goroutinue的运行, 用于流转各类状态或是控制并发情况;

Go语言中, 下面哪个关于指针的说法是错误的?

  • 指针不能进行算术运算
  • 指针可以比较
  • 指针可以是nil
  • 指针可以指向任何类型

针对在Go语言中只能指向相同类型的结构体或者基本类型。例如,一个int类型的变量, 只能指向int类型的指针。如果尝试将一个不同类型的指针赋给一个变量, 将会导致编译错误。

Go语言的接口类型是如何实现的?

在Go语言中, 接口类型是通过**类型嵌入(embedding)**的方式实现的。每个实现了接口的类型的结构体中都有一个隐含的成员, 该成员是指向接口类型的指针。通过这种方式, 接口实现了堆类型的约束和定义。

具体来说, 当一个类型实现了某个接口的所有方法后, 该类型就被认为是实现了该接口。结构体中, 可以通过嵌入接口类型的方式来实现接口方法。在实现接口方法时, 方法的签名需要与接口定义中的方法签名保持一致。

Go string的底层实现

源码包src/runTime/string.go stringStruct定义了string的数据结构

1
2
3
4
Type stringStruct struct {
str unsafe.Pointer // 字符串的首地址
len int // 字符串的长度
}

声明:
如下代码所示, 可以声明一个string变量赋予初值

1
2
var str string
str = "hello world"

字符串构建过程是根据字符串构建stringStruct, 再转化成string, 转化源码如下:

1
2
3
4
5
6
7
8
func gostringnocopy(str *byte) string {
ss := stringStruct{ // 先构造stringStruct
str: unsafe.Pointer(str),
len: findnull(str)
}
s := *(*string)(unsafe.Pointer(&ss)) // 再将stringStruct转换成string
return s
}

Go如何避免panic

首先明确panic定义: go把真正的异常叫做panic, 是指出现重大错误, 比如数据越界之类的编程BUG或者是那些需要人工介入才能修复的问题, 比如程序启动时加载资源出错等等。

几个容易出现panic的点:

  • 函数返回值或参数为指针类型, nil, 未初始化结构体, 此时调用容易出现panic, 可加 !=nil 进行判断;
  • 数组切片越界
  • 如果我们关闭未初始化通道, 重复关闭通道, 向已经关闭的通道中发送数据, 这三种情况会引发panic, 导致程序崩溃;
  • 如果我们直接操作未初始化的映射(map), 也会引发panic, 导致程序崩溃;
  • 另外, 操作映射可能会遇到的更为严重的一个问题是, 同时对同一个映射并发读写, 它会触发runtime.throw, 不像panic可以使用recover捕获。所以, 我们再对同一个映射并发读写时, 一定要使用锁;
  • 如果类型断言使用不当, 比如我们不接受布尔值的话, 类型断言失败也会引发panic, 导致程序崩溃;
  • 如果很多时候不可避免地出现了panic, 记得使用defer/recover;

空结构体的使用场景

空结构体(empty struct)是在Go语言中一个特殊地概念, 它没有任何字段。在Go中, 它通常被称为匿名结构体或零宽度结构体。尽管没有字段, 但它在某些请款下仍然有其他用途

1. 占位符

空结构体可以用作占位符, 用于表示某个数据结构或数据集合地存在而不实际存储任何数据。这在某些数据结构的实现中非常有用, 特别是要实现某种数据结构的集合或映射时, 但并不需要存储实际的值。

1
2
3
// 表示集合中是否包含某个元素的映射
set := make(map[string]struct{})
set["apple"] = struct{}{}

2. 信号量

空结构体可以用作信号量, 用于控制并发操作。通过向通道发送或接收空结构体, 可以实现信号的传递和同步;

1
2
3
4
5
6
7
8
9
// 用通道作为信号量
semaphore := make(chan struct{}, 5) // 控制并发数为5
go func() {
semaphore <- struct{}{} // 获取信号量
defer func() {
<- semaphero
}() // 释放信号量
// 执行并发操作
}()

3.强调结构

有时, 空结构体可用于强调某个结构的重要性或存在。它可以用作结构体的标签, 表示关注该结构的存在而不是其内容;

1
2
3
4
5
6
7
// 表示一篇文章的元信息, 不包含实际内容
type Article struct {
Title string
Author string
PublishedAt time.Time
MetaData struct{} // 空结构体强调元信息的存在
}

4. JSON序列化

在处理JSON数据时, 有时需要表示一个空对象, 可以使用空结构体来表示JSON中的空对象{};

1
2
3
emptyJSON := struct{}{}
jsonBytes, _ := json.Marshal(emptyJSON)
fmt.Println(string(jsonBytes)) // 输出: {}

尽管空结构体没有字段, 但它在上述情况下提供了一种轻量级的方式来处理特定的需求, 而无需分配额外的内存或定义具体的数据结构。

struct的特点:

  • 用来自定义复杂数据结构;
  • struct里面可以包含多个字段(属性);
  • struct类型可以定义方法, 注意和函数的区分;
  • struct类型是值类型;
  • struct类型可以嵌套;
  • Go语言没有calss类型, 只有struct类型;

特殊之处:

  • 结构体是用户单独定义的类型, 不能和其他类型进行强制转换;
  • golang中的struct没有构造函数, 一般可以使用工厂模式来解决这个问题;
  • 我们可以为struct的每个字段, 写上一个tag。这个tag可以通过反射的机制获取到, 最常用的场景就是json序列化和反序列化;
  • 结构体中的字段可以没有名字, 即匿名字段;

评论