Golang面试题整理(一)

Golang面试题整理(一)

golang的特点和优势

在单机上,一个大型go应用编译只需要数秒;

简单的依赖管理,不像C那样复杂的文件和库依赖;

没有继承,不用浪费精力去解释类型之间的关系,go语言里的类比其他OOP语言里的类更轻;

垃圾回收;

并发,程序可以很方便的运行在多核上,对多核的利用效率很高(GMP模型)。

如何使用并发

1
2
3
go func(){
  // todo
}()

go关键字加函数调用

并发和并行

并发指一个处理器同时处理多个任务(其实是交替执行,操作系统时间片)

并行指多个处理器同时处理多个任务。

并发是逻辑上的同时执行,并行是物理上的同时发生。

数据竞争

多个协程并发处理同一个变量。可通过锁或原值操作处理问题

编译时可使用--race 进行数据竞争检测,但这样会增加cpu和内存消耗,生产中不要使用

go build -race .

使用defer处理程序panic

1
2
3
4
5
6
7
8
9
func foo(){
  defer func(){  //recover()必须放在defer func(){}()内(匿名函数)
    if err:=recover();err!=nil{    //如果发生panic,则err不为nil
			 //do something
    }
    //do something
  }()
  panic(any)
}

go怎么实现接口

不需要显示的生成 只要实现了接口定义的所有方法就视为实现了该接口

结构体实现接口

 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
// 定义一个手机接口
type Phone interface{
  Call(num int64)
}
// 小米手机
type XiaoMi struct{
  Size float32
}
// 华为手机
type HuaWei struct{
  Size float32
}
//小米手机实现call方法
func (xiaoMi *XiaoMi) Call(num int64){
}
//华为手机实现call方法
func (huaWei *HuaWei) Call(num int64){
}

func main() {
	var phone Phone
	phone = &XiaoMi{Size: 5.5}
	phone.Call(10086)
	phone = &HuaWei{Size: 6.0}
	phone.Call(10010)
}

自定义类型实现接口

 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"


type Printer interface {
	PrintSelf()
	PrintType()
}

type MyInt int


func (mi MyInt) PrintSelf() {
	fmt.Println(mi)
}


func (mi MyInt) PrintType() {
	fmt.Printf("%T", mi)
}

func main() {
	var p Printer
	var mi MyInt = 100
	p = &mi
	p.PrintSelf()
	p.PrintType()
}

程序运行时可以修改struct定义吗

不可以

怎么进行强制类型转换

目标类型(变量)

1
2
3
4
5
func main() {
	var a int = 10
	var b float32 = float32(a)
	fmt.Println(b)
}

结构体嵌套

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type A struct {
	b    B
	Size float32
}

type B struct {
	Size float32
}

func main() {
	a := A{
		b: B{
			Size: 1.0,
		},
		Size: 2.0,
	}
	fmt.Println(a.b.Size)
	fmt.Println(a.Size)
}

怎么实现继承

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type A struct {
	B
	Size string
}

type B struct {
	Size float32
}

func main() {
	a1 := A{
		B: B{
			Size: 3.0,
		},
		Size: "2",
	}
	fmt.Println(a1.Size) // 2
	fmt.Println(a1.B.Size) // 3
}

匿名结构体

 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
func main() {
	jsonStr := `
{"name":"test","age":18}
`
	// 使用map接收
	var m map[string]any
	err := json.Unmarshal([]byte(jsonStr), &m)
	if err != nil {
		panic(err)
	}
	fmt.Println(m["name"])
	fmt.Println(m["age"])
	// 使用匿名结构体接收
	var s struct {
		Name string `json:"name"`
		Age  int    `json:"age"`
	}
	err = json.Unmarshal([]byte(jsonStr), &s)
	if err != nil {
		panic(err)
	}
	fmt.Println(s.Name)
	fmt.Println(s.Age)

}

什么是方法

method其实就是归属于一个type的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type MyInt int

func (m *MyInt) Add(n MyInt) MyInt {
	return *m + n
}

func (m MyInt) Sub(n MyInt) MyInt {
	return m - n
}

func main() {
	var tempMyInt MyInt = 10
	var m1 = &tempMyInt
	fmt.Println(m1.Add(2))
	fmt.Println(m1.Sub(2))

	var m2 MyInt = 10
	fmt.Println(m2.Add(2))
	fmt.Println(m2.Sub(2))
}

值拥有的方法,其指针也默认拥有;反之不然

但是这里的Add方法是指针拥有的方法 但是m2作为值类型也能调用 是因为go进行了自动类型转换

什么是包

工作空间内的一个目录 包含若干个go文件,目录和包名可以不一样,同一个包内不可以有重复的变量 函数 结构体

什么是module

module是特定版本的包的集合。

如何创建常量

1
const Pi = 3.14

讲一下channel

channel 通道 类似队列

有缓冲和无缓冲的channel

向一个已关闭的channel发送内容会发生panic

可以从一个已关闭的channel里读取数据

1
2
3
4
v,ok:=<-ch //ch已关闭且为空时,ok为true。
for value:=range ch{
  
}

数据与切片

数组是切片的底层数据结构

切片可以扩容,数组不可以

1
2
3
var arr [8]int = [8]int{1,2,3}
var arr = [8]int{1,2,3}
arr:=[...]int{1,2,3}   

讲一下map

1
2
3
mp:=map[string]int{}
mp:=make(map[string]int)
mp:=make(map[string]int,10)

go为什么不保证map顺序?因为扩容的时候保证顺序会带来无谓的开销。

go为什么故意对map乱序?go是一个非常“工程化”的语言,故意乱序是为了让大家都知道它不保证map顺序,减少工程师犯错的几率。

插入和查找时间复杂度为O(1),可以自动扩容

1
2
3
4
5
6
//遍历map
for key,value:=range mp{
  //key和value是副本
  value = "newValue1" // 不生效
  mp[key] = "newValue2" // 生效
}

用map实现一个Set

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import (
	"fmt"
)

type Empty struct {
}

type Set struct {
	m map[any]Empty
}

func (s *Set) Add(items ...interface{}) {
	for _, item := range items {
		s.m[item] = Empty{}
	}
}

func (s *Set) Remove(item any) {
	delete(s.m, item)
}

func (s *Set) Contains(item any) bool {
	_, ok := s.m[item]
	return ok
}

func (s *Set) Clear() {
	s.m = make(map[any]Empty)
}

func (s *Set) Size() int {
	return len(s.m)
}

func NewSet(items ...any) *Set {
	s := &Set{}
	s.m = make(map[any]Empty)
	s.Add(items...)
	return s
}

func main() {
	set := NewSet("AA", "C", 546, false, false, true)
	for a := range set.m {
		fmt.Print(" ", a)
	}
	fmt.Println()
	set.Add("dd", "ff", 23.2)
	set.Remove("C")
	for a := range set.m {
		fmt.Print(" ", a)
	}
	set.Clear()
	fmt.Println()
	fmt.Println("Size: ", set.Size())
}

空白标识符:_

_相当于一个占位符,通常一个变量只定义不使用编译器会报错,但_可以避免报错

1
2
3
4
5
6
7
8
9
func Add(a, b int) int {
    fmt.Println(a + b)
    return a + b
}

func main() {
    a := Add(3, 4)
    _ = a
}

当函数返回多个值,如果有些值不需要使用,也可以使用_进行占位

如何创建、使用函数闭包

函数闭包其实就是把匿名函数赋给一个变量

1
2
3
4
5

a:=func(a int) int{
  return a+3
}
_=a(7)

类型断言

1
2
3
4
5
var val any=8
i,ok:=val.(int)
if ok{
  fmt.Printf("%d",i)
}

type switch的用法

1
2
3
4
5
6
7
8
9
var val any=8
switch v:=val.(type){
case int:
  fmt.Printf("%d",v)
case string:
  fmt.Printf("%s",v)
default:
  fmt.Println("other type")
}

reflect的用法

reflect.TypeOf(x)

reflect.ValueOf(x)

go语言反射里最重要的两个概念是Type和Value,Type用于获取类型相关的信息(比如Slice的长度,struct的成员,函数的参数个数),Value用于获取和修改原始数据的值(比如修改slice和map中的元素,修改struct的成员变量)

crypto包

对称加密AES DES

非对称加密 RSA ECC

哈希 sha256 md5

os包

操作文件,目录

1
2
3
// 接收系统信号
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)

byte和rune

byte是一个字节,rune是4个字节。一个rune可以表示unicode里的任意一个字符,比如一个英文、一个中文、一个标点、一个符号等等。

rune和int32

在Go语言中,runeint32 是两种不同的类型,但它们之间存在密切的关系。

  1. rune 类型
    • runeint32 的别名(alias)。
    • 它用于表示 Unicode 码点(code point),即 Unicode 字符的整数表示。
    • 由于 Unicode 码点的范围是 0 到 0x10FFFF,因此 rune 类型可以表示所有 Unicode 字符。
  2. int32 类型
    • int32 是一个 32 位的有符号整数类型。
    • 它的取值范围是 -2^31 到 2^31 - 1。

由于 runeint32 的别名,因此它们在内存中占用相同的大小(32 位),并且可以互换使用。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
	"fmt"
)

func main() {
	var r rune = 'A'
	var i int32 = r

	fmt.Printf("rune: %c, int32: %d\n", r, i)
}

在这个例子中,rune 类型的变量 r 被赋值为字符 ‘A’,然后将其转换为 int32 类型的变量 i。输出结果将是:

1
rune: A, int32: 65

这表明 runeint32 之间可以无缝转换。

交叉编译

GOOS=windows GOARCH=amd64 go build -o myprogram.exe

则编译出在windows上的可执行文件(windows上的可执行文件必须以.exe结尾)

GOOS=darwin GOARCH=amd64 go build -o myprogram

则编译出在Mac上的可执行文件

如何拼接2个string

  1. StringBuilder

  2. stings.Join()