Golang - 面试知识点小结

Golang基础

  • Go 面试题
    • 问题集合
    • 答案解析
      • 1. go 语言中的关键字有哪些?提示一共有25个。
      • 2. go 语言中类型是如何定义的?
      • 3. go 语言全局变量的定义方式是怎么样的?
      • 4. go 语言中的结构体是如何定义的?
      • 5. go 语言通过指针访问成员变量的方式有几种?
      • 6. go 语言格式化输出的方式有哪些?
      • 7. go 语言中的接口作用是什么?一个接口如果实现了一个接口的所有函数,那么?
      • 8. go 语言中 init 函数有什么特性?能够在一个包里面写多个init吗?
      • 9. go 语言如何定义多参数函数, 调用其的方式有哪些?
      • 10. go 语言中是如何进行类型转换的?
      • 11. go 语言中引用类型有哪些?
      • 12. go 语言中引用的作用是什么?
      • 13. go 语言main函数的特点有什么?
      • 14. slice切片是如何初始化的?
      • 15. go 语言中函数的定义方式有哪些?请举例说明。
      • 16. go 两个接口之间可以存在什么关系?
      • 17. go 当中同步锁有什么特点?作用是什么
      • 18. go 语言当中channel(通道)有什么特点,需要注意什么?
      • 19. go 语言当中channel缓冲有什么特点?
      • 20. go 语言中cap函数可以作用于那些内容?
      • 21. go convey是什么?一般用来做什么?
      • 22. go 语言中类型断言是什么?其作用是什么?举例说明。
      • 23. go 语言当中,切片是如何删除元素的?
      • 24. go 语言当中,如果对json进行重命名?
      • 25. go 语言当中,是如何实现类似继承的操作的?
      • 26. go 语言当中, 使用 ``for range``迭代``map``是每次顺序是一样的吗?为什么?举例说明
      • 27. go 语言中基本的数据类型有哪些?
      • 28. go 语言中switch是如何运用的?有什么特殊的地方?
      • 29. go 语言结构体在序列化时,非导出变量(以小写字母开头的变量名)在解码的时候会出现什么问题?为什么?
      • 30. go 语言当中 new 和 make 有什么区别吗?
      • 31. go 语言中 make 的作用是什么?
      • 32. ``Printf(),Sprintf(),FprintF()``都是格式化输出,有什么不同?
      • 33. go 语言当中数组和切片的区别是什么?
      • 34. go 语言当中值传递和地址传递(引用传递)如何运用?有什么区别?举例说明
      • 35. go 语言当中数组和切片在传递的时候的区别是什么?
      • 36. go 语言如何完成写入文件的操作的 ?
      • 37. go 语言是如何实现切片扩容的?
      • 38. go 语言如何实现类似foreach的操作的?
      • 39. go 语言中 runtime.GOMAXPROCS 的作用是什么?
      • 40. go 语言中是如何实现组合继承的?
      • 41. 解释一下 go 语言当中的select的随机性是什么?
      • 42. 看看下面代码的defer的执行顺序是什么? defer的作用和特点是什么?
      • 43. 看看下面切片的代码输出是什么,为什么?
      • 44. go 语言是如何实现线程安全的?下面这段代码会出现什么情况?请分析
      • 45. go 语言中 cache 缓冲池(chan)是如何实现的?有什么机制?请举例实现代码。
      • 46. go 语言当中 interface 的内部结构是什么样的?
      • 47. 下面的结构体的结果是什么?
      • 48. 下面的 channel 会出现什么结果?为什么?
      • 49. 解释一下 go 语言的同步锁的机制
      • 50. 解释一下 go 语言是一门什么类型的语言?有什么特点,主要能用来做什么?
      • 51. 解释一下 go 语言当中的强类型是什么?有何作用?

鸣谢

Go 面试题

问题集合

此处先展示题目,后面会有题目解析。

  1. go 语言中的关键字有哪些?提示一共有25个。
  2. go 语言中类型是如何定义的?
  3. go 语言全局变量的定义方式是怎么样的?
  4. go 语言中的结构体是如何定义的?
  5. go 语言通过指针访问成员变量的方式有几种?
  6. go 语言格式化输出的方式有哪些?
  7. go 语言中的接口作用是什么?一个接口如果实现了一个接口的所有函数,那么?
  8. go 语言中 init 函数有什么特性?能够在一个包里面写多个init吗?
  9. go 语言如何定义多参数函数, 调用其的方式有哪些?
  10. go 语言中是如何进行类型转换的?
  11. go 语言中引用类型有哪些?
  12. go 语言中引用的作用是什么?
  13. go 语言main函数的特点有什么?
  14. slice切片是如何初始化的?
  15. go 语言中函数的定义方式有哪些?请举例说明。
  16. go 两个接口之间可以存在什么关系?
  17. go 当中同步锁有什么特点?作用是什么
  18. go 语言当中channel(通道)有什么特点,需要注意什么?
  19. go 语言当中channel缓冲有什么特点?
  20. go 语言中cap函数可以作用于那些内容?
  21. go convey是什么?一般用来做什么?
  22. go 语言中类型断言是什么?其作用是什么?举例说明。
  23. go 语言当中,切片是如何删除元素的?
  24. go 语言当中,如果对json进行重命名?
  25. go 语言当中,是如何实现类似继承的操作的?
  26. go 语言当中, 使用 for range迭代map是每次顺序是一样的吗?为什么?举例说明
  27. go 语言中基本的数据类型有哪些?
  28. go 语言中switch是如何运用的?有什么特殊的地方?
  29. go 语言结构体在序列化时,非导出变量(以小写字母开头的变量名)在解码的时候会出现什么问题?为什么?
  30. go 语言当中 new 和 make 有什么区别吗?
  31. go 语言中 make 的作用是什么?
  32. Printf(),Sprintf(),FprintF()都是格式化输出,有什么不同?
  33. go 语言当中数组和切片的区别是什么?
  34. go 语言当中值传递和地址传递(引用传递)如何运用?有什么区别?举例说明
  35. go 语言当中数组和切片在传递的时候的区别是什么?
  36. go 语言如何完成写入文件的操作的 ?
  37. go 语言是如何实现切片扩容的?
  38. go 语言如何实现类似foreach的操作的?
  39. go 语言中 runtime.GOMAXPROCS 的作用是什么?
  40. go 语言中是如何实现组合继承的?
  41. 解释一下 go 语言当中的select的随机性是什么?
  42. 看看下面代码的defer的执行顺序是什么? defer的作用和特点是什么?
  43. 看看下面切片的代码输出是什么,为什么?
  44. go 语言是如何实现线程安全的?下面这段代码会出现什么情况?请分析
  45. go 语言中 cache 缓冲池是如何实现的?有什么机制?请举例实现代码。
  46. go 语言当中 interface 的内部结构是什么样的?
  47. 下面的结构体的结果是什么?
  48. 下面的 channel 会出现什么结果?为什么?
  49. 解释一下 go 语言的同步锁的机制
  50. 解释一下 go 语言是一门什么类型的语言?有什么特点,主要能用来做什么?
  51. 解释一下 go 语言当中的强类型是什么?有何作用?

答案解析

1. go 语言中的关键字有哪些?提示一共有25个。

// 1. 程序声明:
		import
		package
// 2. 实体声明和定义:
		chan	// 通道
		const	// 常量声明
		func	// 函数声明
		interface	// 接口声明
		map		// map 声明
		struct	// 结构体声明
		type	// 类型声明
		var 	// 变量声明
// 3. 流程控制
		go		// 开启协程
		select	// 
		break	// 跳出循环
		case	// switch选择选项
		continue	// 跳出当前循环
		default		// switch默认区域
		else		// 条件判断
		fallthrough	// 
		for			// 循环控制程序
		goto		// 并发控制程序运行函数
		if			// 条件结构程序
		range		// 循环遍历
		return		// 函数返回
		switch		// 选项选择

2. go 语言中类型是如何定义的?

拿字符串来举例,可以有下面三种方式:

func main() {
	var a string = "a"	// 定义类型并赋值
	b := "b"	// 自动判断类型并赋初值
	var c string	// 只定义类型不赋初值
	c = "c"			// 变量后面随时可以修改
	var d = "d"		// 不指明类型的并赋初值
	
	fmt.Println(a)
	fmt.Println(b)
	fmt.Println(c)
	fmt.Println(d)

}

3. go 语言全局变量的定义方式是怎么样的?

var a string = "a"

var b = "b"

c := "c"

var d string
d = "d"

func main() {
	
	fmt.Println(a)
	fmt.Println(b)
	fmt.Println(c)
	fmt.Println(d)

}

4. go 语言中的结构体是如何定义的?

使用struct可以定义结构体,具体可以参考如下的方式:


type Person struct {
	name string
	age int
}

func main() {
	p1 := Person{"Alice", 20}

	var p2 Person
	p2.name  "Panda"
	p2.age = 24

	fmt.Println(p1)
	fmt.Println(p2)
}

5. go 语言通过指针访问成员变量的方式有几种?

1. 直接使用 ``:=`` 可以获取变量的地址
2. 用&xxxx来获取地址

func main() {
 
	person := Person{"Alice", 20}
 
	p1 := &person
 
	fmt.Println(person)
	fmt.Println(&person)
	fmt.Println(&p1)
	fmt.Println(*p1)
 
	fmt.Println(person.name)
	fmt.Println(&person.name)
	fmt.Println(p1.name)
	fmt.Println(&p1.name)
}
 
type Person struct {
	name string
	age  int
}

6. go 语言格式化输出的方式有哪些?

- %T 类型
- %t 布尔
- %d 10进制整数
- %x 16进制整数
- %f	 浮点数
- %s	 字符串

	person := Person{"Alice", 20}
	fmt.Printf("%T\n", person)
 
	flag := true
	fmt.Printf("%t\n", flag)
 
	number10 := 99
	fmt.Printf("%d\n", number10)
 
	//十进制100为16进制的64
	number16:= 0x64
	fmt.Printf("%X\n", number16)
	fmt.Printf("%d\n", number16)
 
	number0:= 0.123
	fmt.Printf("%f\n", number0)
 
	str:= "hello world"
	fmt.Printf("%s\n", str)

7. go 语言中的接口作用是什么?一个接口如果实现了一个接口的所有函数,那么?

一个类如果实现了一个接口的所有函数,那么这个类就实现了这个接口。

go接口

go 语言接口的作用是,可以实现OO面向对象的特性,从语法上看,Interface定义了一个或一组method(s),这些method(s)只有函数签名,没有具体的实现代码(有没有联想起C++中的虚函数?)。下面是一个运用的实例:

   type MyInterface interface{
       Print()
   }
   
   func TestFunc(x MyInterface) {}
   type MyStruct struct {}
   func (me MyStruct) Print() {}
   
   func main() {
       var me MyStruct
       TestFunc(me)
   }

8. go 语言中 init 函数有什么特性?能够在一个包里面写多个init吗?

具体可以参考: 五分钟理解golang init函数

init 是初始化函数,在包引入的时候就会调用,一个包可以写多个init函数。

init 函数的主要作用是:

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

init 函数的主要特点:

  • init 函数咸鱼main函数自动执行,不能被其他函数调用;
  • init 函数没有输入参数、返回值;
  • 每个包可以有多个init函数;
  • 同一个包的init执行顺序,golang没有明确定义,编程时注意不要依赖这个执行顺序。;
  • 不同包的init函数的执行顺序依照包导入的依赖关系决定执行顺序。

具体可以参考下面的例子:

package main                                                                                                                     

import (
   "fmt"              
)

var T int64 = a()

func init() {
   fmt.Println("init in main.go ")
}

func a() int64 {
   fmt.Println("calling a()")
   return 2
}
func main() {                  
   fmt.Println("calling main")     
}

输出:

calling a()
init in main.go
calling main

初始化顺序:变量初始化->init()->main()

实例2:


func init() {
    fmt.Println("init 1")
}
 
func init() {
    fmt.Println("init 2")
}
 
func main() {
    fmt.Println("main")
}

输出:

init 1
init 2
main

9. go 语言如何定义多参数函数, 调用其的方式有哪些?

可以直接在函数参数列表里输入…,来定义多参数函数

func add(args ...int) int {}

// 这个函数的调用方式有:
add(1, 2, 3)
add([]int{1, 2, 3}...)

这样就可以传入多个参数来给函数了。

10. go 语言中是如何进行类型转换的?

类似其他语言的强制类型转换,直接用类型名称(变量),就可以实现这样的效果。

举例如下:

type MyInt int
var a int = 1
var b MyInt = MyInt(a)

11. go 语言中引用类型有哪些?

参考书籍:看云-golang语言类型介绍

  • slice
  • map
  • channel

Golang的引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。

12. go 语言中引用的作用是什么?

能够让外部变量直接操作某块内存地址。
内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译 成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针。

引用类型:

变量存储的是一个地址,这个地址所存储的值,
在内存上通常是将其分配在堆上面,
在程序中通过GC来进行回收。
获取指针类型的所指向的值,
可以使用: “*”取值符号。
比如 var *p int, 使用*p获取p指向的值
指针、slice、map、chan都是引用类型。

13. go 语言main函数的特点有什么?

  • main函数不能带参数
  • main函数不能定义返回值
  • main函数所在的包必须为main
  • main函数中可以使用flag包来获取和解析命令行参数

14. slice切片是如何初始化的?

实现切片初始化的方式,大致我学习到的可以分为两种:

  • 使用make进行切片的初始化
  • 使用数组直接定义初始化

具体可以参考下面的代码:


func main() {
	s1 := make([]int, 0)
	s2 := make([]int, 6,10)
	s3 := []int{1, 2, 3, 4, 5}
 
	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Println(s3)
 
	fmt.Println(len(s2))
	fmt.Println(cap(s2))
}

15. go 语言中函数的定义方式有哪些?请举例说明。

go语言当中函数的定义,大致我概括一下可以分为如下几类:

  • 不带参数的函数定义
  • 带参数的函数定义
  • 返回值有/无标识的函数定义
  • 多参数函数定义
  • 类函数定义

具体可以参考如下的代码:

// - 不带参数的函数定义

func main() {
	r1, r2 := getResult(1, 2)
	fmt.Println(r1)
	fmt.Println(r2)
}

// - 带参数的函数定义

func getResult(a int, b int) (c int, d int) {
	return a + b, a - b
}

// - 返回值有/无标识的函数定义

func getResult(a int, b int) (int, int) {
	return a + b, a - b
}

func getResult(a int, b int) (c int, d int) {
	return a + b, a - b
}

// - 多参数函数定义
func getResult(a int , b int , c ...int) (c int, d int) {
	return a + b, a - b
}

// - 类成员函数定义

type Person struct {
	name string
	age int
}

func (p Person) setName(n string) {
	if n == "" {
		p.name = "NaN"
	}
	p.name = n
}

关于可变参数的讲解,可以参考: Go语言“可变参数函数”终极指南
关于go中面向对象定义类成员函数,可以参考: go中的面向对象

这里展示一块面向对象使用的代码:

package main
import "fmt"

type Human struct {
    height float32
    weight int
}

func (h Human) BMIindex() (index int){
    index = h.weight / int(h.height * h.height)
    return
}

func (h Human) setHeight(height float32) {
    h.height = height
}

func main() {
    person := Human{1.83, 75}
    fmt.Printf("this person's height is %.2f m\n", person.height)
    fmt.Printf("this person's weight is %d kg\n", person.weight)
    fmt.Printf("this person's BMI index is %d\n", person.BMIindex())
    person.setHeight(1.90)
    fmt.Printf("this person's height is %.2f m\n", person.height)
}

输出结果:
    this person's height is 1.83 m
    this person's weight is 75 kg
    this person's BMI index is 25
    this person's height is 1.83 m

可以看出,我们调用person.setHeight(1.90)之后,person的height属性并没有改变为1.90。而为了解决这个问题,我们需要改变receiver。我们将setHeight()函数定义为下述形式即可。

func (h *Human) BMIindex() (index int){
    index = h.weight / int(h.height * h.height)
    return
}

16. go 两个接口之间可以存在什么关系?

  • 如果两个接口有相同的方法列表,那么他们就是等价的,可以相互赋值。
  • 如果接口A的方法列表是接口B的方法列表的自己,那么接口B可以赋值给接口A。
  • 接口查询是否成功,要在运行期才能够确定。

17. go 当中同步锁有什么特点?作用是什么

  • 当一个goroutine(协程)获得了Mutex后,其他gorouline(协程)就只能乖乖的等待,除非该gorouline释放了该Mutex
  • RWMutex在 读锁 占用的情况下,会阻止写,但不阻止读
  • RWMutex在 写锁 占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占

同步锁的作用是保证资源在使用时的独有性,不会因为并发而导致数据错乱,保证系统的稳定性。

18. go 语言当中channel(通道)有什么特点,需要注意什么?

  • 如果给一个 nil 的 channel 发送数据,会造成永远阻塞
  • 如果从一个 nil 的 channel 中接收数据,也会造成永久爱阻塞
  • 给一个已经关闭的 channel 发送数据, 会引起 pannic
  • 从一个已经关闭的 channel 接收数据, 如果缓冲区中为空,则返回一个零值

19. go 语言当中channel缓冲有什么特点?

无缓冲的 channel是同步的,而有缓冲的channel是非同步的。

20. go 语言中cap函数可以作用于那些内容?

cap函数在讲引用的问题中已经提到,可以作用于的类型有:

  • array(数组)
  • slice(切片)
  • channel(通道)

21. go convey是什么?一般用来做什么?

  • go convey是一个支持golang的单元测试框架
  • go convey能够自动监控文件修改并启动测试,并可以将测试结果实时输出到Web界面
  • go convey提供了丰富的断言简化测试用例的编写

22. go 语言中类型断言是什么?其作用是什么?举例说明。

详细的类型断言的学习,可以参考: Go语言圣经-类型断言

在go语言中断言的代码举例如下:

func main() {
 
	m := make(map[int]interface{})
	m[0] = Person{}
	m[1] = "abc"
 
 	// 两个参数分别获得值和对应是否是相同类型
	r1, r2 := m[0].(Person)
	r3, r4 := m[1].(string)
 
	fmt.Println(r1)
	fmt.Println(r2)
	fmt.Println(r3)
	fmt.Println(r4)
 
}
输出结果为
{}
true
abc
true

一个类型断言的作用是:一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。

类型断言是一个使用在接口值上的操作。语法上它看起来像 x . ( T ) x.(T) x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。

23. go 语言当中,切片是如何删除元素的?

在go中想要对切片进行元素的删除,具体过程如下:

func main() {
 
	s := make([]string, 0)
	s = append(s, "abc0")
	s = append(s, "abc1")
	fmt.Println(s)
 
	s = append(s[:0], s[0+1:]...)
	fmt.Println(s)
 
	s = append(s[:0], s[0+1:]...)
	fmt.Println(s)
}

// 输出结果
[abc0 abc1]
[abc1]
[]

24. go 语言当中,如果对json进行重命名?

在go当中,我们可以利用json库来对某些结构体进行相关的json格式化重命名的操作,来完成对应的数据类型的转换。


func main() {
 
	p1 := Person{"Alice", 20}
	fmt.Println(p1)
 
	bytes, _ := json.Marshal(p1)
	fmt.Println(string(bytes))
 
}
 
type Person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

// 输出结果
{Alice 20}
{"name":"Alice","age":20}

25. go 语言当中,是如何实现类似继承的操作的?

我们通过在一个新的结构体里面,定义某个类型的数据的对象,就能够实现继承这个类的数据的操作。比如类似下面这样:

func main() {
	stu := Student{Person{"Panda", 20}}
	fmt.Println(stu)
}

type Person struct {
	Name string
	Age int
}

type Student struct {
	Person
}


输出结果
{{Panda 20}}

26. go 语言当中, 使用 for range迭代map是每次顺序是一样的吗?为什么?举例说明

使用 for range迭代map时,每次迭代的顺序可能不一样,因为map的得带是随机的

func main() {
	m := make(map[string]int)
	m["string"] = 1
	
	m["int"] = 1
	m["float"] = 2
	m["bool"] = 3
	m["byte"] = 4

	
	for k, v := range m {
		fmt.Println(k, ",", v)
	}

	// 打印的顺序会出现不一样的情况
}

27. go 语言中基本的数据类型有哪些?

一共有18个,主要有:

1. bool
2. string
3. byte
4. int
5. uint
6. float

28. go 语言中switch是如何运用的?有什么特殊的地方?

go 当中switch语句和其他语言类似,只是有一个特殊的地方,switch后面可以不跟表达式


func main() {
	i := rand.Intn(2)

	switch i {
	case 0:
		fmt.Println("get 0")
	case 1:
		fmt.Println("get 1")
	}
}

// switch后面可以不跟表达式
func main() {
	i := rand.Intn(2)

	switch {
	case i == 0:
		fmt.Println("get 0")
	case i == 1:
		fmt.Println("get 1")
	}
}

29. go 语言结构体在序列化时,非导出变量(以小写字母开头的变量名)在解码的时候会出现什么问题?为什么?

结构体在序列化的时候非导出变量(以小写字母开头的变量名)不会被encode,所以在decode时这些非到处变量的值为其类型的零值。

30. go 语言当中 new 和 make 有什么区别吗?

  • new的作用是初始化一个纸箱类型的指针
  • new函数是内建函数,函数定义:
func new(Type) *Type
  • 使用new函数来分配空间
  • 传递给new函数的是一个类型,而不是一个值
  • 返回值是指向这个新非配的地址的指针

那么可以看一下代码

func main() {
	p := new(Person)
	person := Person{"Panda"}
	fmt.Printf("%T\n",p)
	fmt.Printf("%T\n",person)

}


type Person struct {
	name string
}


// 输出结果为
// *main.Person
// main.Person
 
// 所以new函数返回的是指针
// person是实体


31. go 语言中 make 的作用是什么?

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

func make(Type, size IntegerType) Type

make(T, args)函数的目的和new(T)不同
仅仅用于创建slice, map, channel
而且返回类西行是实例

32. Printf(),Sprintf(),FprintF()都是格式化输出,有什么不同?

虽然这三个函数,都是格式化输出,但是输出的目标不一样

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

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

func main() {
	person := Person{"Alice"}
	s := fmt.Sprintf("类型是%T\n", person)
	fmt.Println(s)
}

Fprintf()是吧格式化字符串输出到文件中,主要用于文件操作的代码:


file, e := os.OpenFile("f:1.txt", os.O_RDWR, 0777)
defer file.Close()
if e != nil {
	fmt.Println(e)
}

fmt.Fprintln(file, "Hello, world")

33. go 语言当中数组和切片的区别是什么?

数组:

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

切片:

  1. 切片可以改变长度
  2. 切片是轻量级的数据结构,三个属性,指针,长度,容量
  3. 不需要指定大小
  4. 切片是地址传递(引用传递)
  5. 可以通过数组来初始化,也可以通过内置函数make()来初始化,初始化的时候len=cap,然后进行扩容

34. go 语言当中值传递和地址传递(引用传递)如何运用?有什么区别?举例说明

  1. 值传递只会把参数的值复制一份放进对应的函数,两个变量的地址不同,不可相互修改。
  2. 地址传递(引用传递)会将变量本身传入对应的函数,在函数中可以对该变量进行值内容的修改。

具体我们可以参考如下的代码:


func main() {
	p1 := Person{"Alice"}
	p2 := Person{"Bob"}
	change(p1)
	changeAddress(&p2)
 
	fmt.Println(p1)
	fmt.Println(p2)
}
 // 值传递
func change(p Person) {
	p.name = "Hello"
}

// 地址传递 
func changeAddress(p *Person) {
	p.name = "Hello"
}
 
type Person struct {
	name string
}

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

1. 数组是值传递
2. 切片是引用传递

具体可以参考下面的代码:

func main() {
 
	arr1 := [3]int{1, 2, 3}
	arr2 := []int{1, 2, 3}
 
	changeArr(arr1)
	changeSlice(arr2)
 
	fmt.Printf("%T\n", arr1)
	fmt.Printf("%T\n", arr2)
 
	fmt.Println(arr1)
	fmt.Println(arr2)
}
 
func changeArr(arr [3]int) {
	arr[0] = 9
}
 
func changeSlice(arr []int)  {
	arr[0] = 9
}


结果为
[1 2 3]
[9 2 3]

36. go 语言如何完成写入文件的操作的 ?

	file, err := os.OpenFile("f:/1.txt", os.O_RDWR, 0777)
	defer file.Close()
	if err != nil {
		fmt.Println(err)
		return
	}
 
	file.WriteString("hello world")

37. 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
每次都是扩容了四分之一左右

38. go 语言如何实现类似foreach的操作的?

我们下来看一下使用代码:

func pase_student() {
	m := make(map[string]*student)
	stud := []student{
		{Name:"zhou", Age:24},
		{Name: "li", Age: 23},
		{Name: "wang", Age: 22},
	}
	
	for _, stu := range stus {
		m[stu.Name] = &stu
	}
 
	fmt.Println(m)

}


看下结果
map[zhou:0xc000004440 li:0xc000004440 wang:0xc000004440]


我们再看一段直观一点的代码
	stus := []student{
		{Name: "zhou", Age: 24},
		{Name: "li", Age: 23},
		{Name: "wang", Age: 22},
	}
 
	for _, stu := range stus {
		fmt.Printf("%p\n",&stu)
	}
结果是
0xc000052400
0xc000052400
0xc000052400


原因是foreach使用副本的方式,所以&stu实际上
指向的是同一个指针
最终该指针的值是最后一个struct的值的拷贝


正确的方法应该是这样
	for i := 0; i < 3; i++ {
		stu:=stus[i]
		fmt.Printf("%p\n",&stu)
	}


39. go 语言中 runtime.GOMAXPROCS 的作用是什么?

Go中GoMAXPROCS

在 Go语言程序运行时(runtime)实现了一个小型的任务调度器。这套调度器的工作原理类似于操作系统调度线程,Go 程序调度器可以高效地将 CPU 资源分配给每一个任务。传统逻辑中,开发者需要维护线程池中线程与 CPU 核心数量的对应关系。同样的,Go 地中也可以通过 runtime.GOMAXPROCS() 函数做到,格式为:

runtime.GOMAXPROCS(逻辑CPU数量)

这里的逻辑CPU数量可以有如下几种数值:

  • < 1 <1 <1: 不修改任何数值。
  • = 1 =1 =1: 单核心运行。
  • > 1 >1 >1: 多核心运行。

runtime.GOMAXPROCS 的作用是:调整并发的运行性能。

看下代码


func main() {
    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup{}
    wg.Add(20)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println("A: ", i)
            wg.Done()
        }()
    }
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println("B: ", i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}
 
先看第一个循环
首先,for循环很快就结束了
然后开启10go协程
所以输出了10个A:10
 
再看第二个循环
每次循环把i当做参数传入函数
但是go协程启动的顺序是不一定的
所以输出10个数字,顺序是不一定的

40. go 语言中是如何实现组合继承的?

可以来看一段代码:

func (p *People) ShowA() {
    fmt.Println("showA")
    p.ShowB()
}
func (p *People) ShowB() {
    fmt.Println("showB")
}
 
type Teacher struct {
    People
}
 
func (t *Teacher) ShowB() {
    fmt.Println("teacher showB")
}
 
func main() {
    t := Teacher{}
    t.ShowA()
}
 
首先,调用ShowA方法
那么会输出ShowA
然后People指针调用ShowB方法
这时候这个指针不知道自己是Teacher
所以还是走People的ShowB
所以结果是
ShowA
ShowB

41. 解释一下 go 语言当中的select的随机性是什么?

看一段代码:


func main() {
    runtime.GOMAXPROCS(1)
    int_chan := make(chan int, 1)
    string_chan := make(chan string, 1)
    int_chan <- 1
    string_chan <- "hello"
    select {
    case value := <-int_chan:
        fmt.Println(value)
    case value := <-string_chan:
        panic(value)
    }
}


我们看到select中的两个case都满足
那么会随机选择一个来执行
所以程序有可能崩溃,也有可能不会崩溃


42. 看看下面代码的defer的执行顺序是什么? defer的作用和特点是什么?

defer讲解

defer的作用是:

  1. 你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。
  2. 当defer语句被执行时,跟在defer后面的函数会被延迟执行。
  3. 直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。
  4. 你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。

defer的常用场景:

  • defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。
  • 通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。
  • 释放资源的defer应该直接跟在请求资源的语句后。

观察下面的程序中defer的执行顺序是什么


func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
}
 
func main() {
    a := 1
    b := 2
    defer calc("1", a, calc("10", a, b))
    a = 0
    defer calc("2", a, calc("20", a, b))
    b = 1
}
结果为
"10" 1 2 3
"20" 0 2 2
"2"  0 2 2
"1"  1 3 4
 
原因:
1. 函数calc调用的时候,b参数是使用的新的calc的返回值,所以先运行作为参数的calc
2. defer会造成延迟运行,所以main中定义的两个defer会按照与定义相反的顺序返回结果

43. 看看下面切片的代码输出是什么,为什么?


func main() {
    s := make([]int, 5)
    s = append(s, 1, 2, 3)
    fmt.Println(s)
    
	// 结果为0,0,0,0,0,1,2,3
	// make初始化是有默认值的,这里默认值是0

}

44. go 语言是如何实现线程安全的?下面这段代码会出现什么情况?请分析


type UserAges struct {
	ages map[string]int
	sync.Mutex
}

func (ua *UserAges) Add(name string, age int) {
	ua.Lock()
	defer ua.Unlock()
	ua.ages[name] = age
}

func (ua *UserAges) Get(name string) int {
	if age, ok := ua.ages[name]; ok {
		return age
	}
	return -1
}


这段代码有可能会出现
fatal error: concurrent map read and map write
原因是在读取的时候没有给数据源加锁

我们可以修改一下代码,保证线程安全

type UserAges struct {
	ages map[string]int
	sync.Mutex
}
 
func (ua *UserAges) Add(name string, age int) {
	ua.Lock()
	defer ua.Unlock()
	ua.ages[name] = age
}
 
func (ua *UserAges) Get(name string) int {
    ua.Lock()
    defer ua.Unlock()
    if age, ok := ua.ages[name]; ok {
        return age
    }
    return -1
}

45. go 语言中 cache 缓冲池(chan)是如何实现的?有什么机制?请举例实现代码。

个人小结一下定义go中的缓冲池需要的步骤如下:

  1. 定义chan接口
  2. 编写线程运行函数
  3. 开启线程锁
  4. 添加缓冲
  5. 释放线程锁
  6. 返回缓冲池

具体可以看如下的代码:


func (set *threadSafeSet) Iter() <-chan interface{} {
	ch := make(chan interface{})
	go func() {
		set.RLock()
 
		for elem := range set.s {
			ch <- elem
		}
 
		close(ch)
		set.RUnlock()
 
	}()
	return ch
}

完整的演示示例如下:


type threadSafeSet struct {
	sync.RWMutex
	s []interface{}
}
 
func (set *threadSafeSet) Iter() <-chan interface{} {
	// ch := make(chan interface{}) // 解除注释看看!
	ch := make(chan interface{}, len(set.s))
	go func() {
		set.RLock()
 
		for elem, value := range set.s {
			ch <- elem
			fmt.Println("Iter:", elem, value)
		}
 
		close(ch)
		set.RUnlock()
 
	}()
 
	return ch
}
 
func main() {
 
	th := threadSafeSet{
		s: []interface{}{"1", "2"},
	}
	v := <-th.Iter()
	fmt.Sprintf("%s%v", "ch", v)
}

运行结果
Iter: 0 1
Iter: 1 2

46. go 语言当中 interface 的内部结构是什么样的?

type People interface {
	Show()
}

type Student struct {}

func (stu *Student) Show() {

}

func live() People {
	var stu *Student
	return stu
}

func main() {
	if live() == nil {
		fmt.Println("AAAAAA")
	} else {
		fmt.Println("BBBBBB")
	}
}

我们说一下interface的内部结构
interface分为两种
1.空接口
2.带方法的接口


var people interface{}
type People interface{
	sayHello()
}

底层结构如下:
type eface struct {      //空接口
    _type *_type         //类型信息
    data  unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type iface struct {      //带有方法的接口
    tab  *itab           //存储type信息还有结构实现方法的集合
    data unsafe.Pointer  //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type _type struct {
    size       uintptr  //类型大小
    ptrdata    uintptr  //前缀持有所有指针的内存大小
    hash       uint32   //数据hash值
    tflag      tflag
    align      uint8    //对齐
    fieldalign uint8    //嵌入结构体时的对齐
    kind       uint8    //kind 有些枚举值kind等于0是无效的
    alg        *typeAlg //函数指针数组,类型实现的所有方法
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}
type itab struct {
    inter  *interfacetype  //接口类型
    _type  *_type          //结构类型
    link   *itab
    bad    int32
    inhash int32
    fun    [1]uintptr      //可变大小 方法集合
}

可以看出iface比eface中间多了一层itab结构
itab存储_type信息和[]fun方法集
从上面的结构我们可以看出
data指向了nil
但是并不代表interfacenil
所以返回值不为空
这里的fun方法集定义了接口的接收规则
在编译的过程中需要验证是否实现接口


47. 下面的结构体的结果是什么?

func main() {
	var p1 Person
	var p2 Person
	p2.name= "Alice"
	p3:=Person{}
 
	fmt.Println(p1)
	fmt.Println(p2)
	fmt.Println(p3)
}

结果为
{}
{Alice}
{}


48. 下面的 channel 会出现什么结果?为什么?

会出现随机的结果,因为go 启动的时机是随机的,所以 num打印和channel的打印顺序是随机交错的。


	channel := make(chan int)
 
	for i := 0; i < 10; i++ {
		go func() {
			fmt.Println(i)
			channel <- i
		}()
	}
 
	for i := 0; i < 10; i++ {
		num := <-channel
		fmt.Println("num:", num)
	}

49. 解释一下 go 语言的同步锁的机制

  1. 当一个go程获得了Mutex,其他的只能等待,除非这个go程释放这个Mutex
  2. RWMutex在读锁占用的情况下,会阻止写,但不会阻止读
  3. RWMutex在写锁占用的情况下,会阻止写,也阻止读

50. 解释一下 go 语言是一门什么类型的语言?有什么特点,主要能用来做什么?

go语言是Google开发的一种:

  1. 静态强类型
  2. 编译型
  3. 并发型
  4. 有垃圾回收功能
    的编程语言

51. 解释一下 go 语言当中的强类型是什么?有何作用?

强类型指的是程序中表达的任何对象所从属的类型
都必须能在编译时刻确定
常见的强类型语言有
C++, Java, Python, Golang等
适合大规模信息系统开发

javascript是弱类型的
比如

var a = 1;
var b = 'a';
console.log(a+b)

你可能感兴趣的:(Golang学习之路,数据结构)