面试问题学习笔记 面试问题学习笔记
  • 操作系统
  • 计算机网络
  • JavaScript/HTML
  • Go
  • Java
  • spring全家桶
  • 中间件
  • MySQL
  • Redis
  • MongoDB
  • C/C++
  • 安全相关
  • 软件相关
  • 系统相关
  • 智力题
  • 业务问题
GitHub (opens new window)
  • 操作系统
  • 计算机网络
  • JavaScript/HTML
  • Go
  • Java
  • spring全家桶
  • 中间件
  • MySQL
  • Redis
  • MongoDB
  • C/C++
  • 安全相关
  • 软件相关
  • 系统相关
  • 智力题
  • 业务问题
GitHub (opens new window)
  • Go

    • 编译原理
    • 数据结构
    • 语言基础
      • 函数调用
        • 匿名函数
        • 闭包
        • go的main函数
      • 接口
        • 值接收者和指针接收者
        • 底层实现
      • 反射
        • 反射的三大法则
        • 反射的底层与原理
      • 指针
      • nil类型
      • ... 操作符
      • 常量
    • 常用关键字
    • 并发编程
    • 内存管理
    • 元编程
    • 标准库
    • 其他
    • 面试问题
  • JAVA

  • spring全家桶

  • 中间件

  • MySQL

  • Redis

  • MongoDB

  • 后端
  • Go
小哈里
2021-03-20
目录

语言基础

# 函数调用

这里面涉及的东西其实挺深的,所以等我后面又时间再去研究

  • Go 中函数传参仅有值传递一种方式;
  • slice、map、channel都是引用类型,但是跟c++的不同;
  • slice能够通过函数传参后,修改对应的数组值,是因为 slice 内部保存了引用数组的指针,并不是因为引用传递。

Golang中函数传参存在引用传递吗? - SegmentFault 思否 (opens new window)

# 匿名函数

// 这个是带返回值的 
f:=func()string{
      return "hello world"
 }
 a:=f()
// 当然我们可以直接调用
func(a int)string{
      return "hello world"
 }(4)
1
2
3
4
5
6
7
8
9

# 闭包

什么是闭包? 闭包是由函数和与其相关的引用环境组合而成的实体。

# 函数变量(函数值)

在 Go 语言中,函数被看作是第一类值,这意味着函数像变量一样,有类型、有值,其他普通变量能做的事它也可以。

func square(x int) {
	println(x * x)
}
1
2
3
  1. 直接调用:square(1)
  2. 把函数当成变量一样赋值:s := square;接着可以调用这个函数变量:s(1)。 注意:这里 square 后面没有圆括号,调用才有。
  • 调用 nil 的函数变量会导致 panic。
  • 函数变量的零值是 nil,这意味着它可以跟 nil 比较,但两个函数变量之间不能比较。

# 什么是闭包

先看一下这个函数,函数叫incr(),返回值为func() int

func incr() func() int {
	var x int
	return func() int {
		x++
		return x
	}
}
1
2
3
4
5
6
7

调用这个函数会返回一个函数变量。下面是一段演示代码

func incr() func() int {
	var x int
	return func() int {
		x++
		return x
	}
}
func main() {
	// 获取闭包
	i:=incr()
	// 打印闭包
	println(i()) // 1
	println(i()) // 2
	println(i()) // 3
	// 下面返回了三个闭包
	println(incr()()) // 1
	println(incr()()) // 1
	println(incr()()) // 1
}

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

首先是逃逸问题

i := incr():通过把这个函数变量赋值给 i,i 就成为了一个闭包。

所以 i 保存着对 x 的引用,可以想象 i 中有着一个指针指向 x 或 i 中有 x 的地址。

由于 i 有着指向 x 的指针,所以可以修改 x,我们可以说且保持着状态所以会出现每次打印递增的情况。此时我们可以说

那么为什么下面打印结果都是一样的呢。这是因为这里调用了三次 incr(),返回了三个闭包,这三个闭包引用着三个不同的 x,它们的状态是各自独立的。

# 闭包会产生的问题

现在开始通过例子来说明由闭包引用产生的问题:

地址引用的问题

x := 1
f := func() {
	println(x)
}
x = 2
x = 3
f() // 3
1
2
3
4
5
6
7

因为闭包对外层词法域变量是引用的,所以这段代码会输出 3。可以想象 f 中保存着 x 的地址,它使用 x 时会直接解引用,所以 x 的值改变了会导致 f 解引用得到的值也会改变。

但是下面这段代码会返回1,因为我们的函数是提前调用的,所以此时已经把结果打印出来了,所以后面修改不会影响

x := 1
func() {
	println(x) // 1
}()
x = 2
x = 3
1
2
3
4
5
6

循环闭包引用问题

每次迭代后都对 i 进行了解引用并使用得到的值且不再使用,所以下面这段代码会正常输出。

for i := 0; i < 3; i++ {
	func() {
		println(i) // 0, 1, 2
	}()
}
1
2
3
4
5

然而下面这段代码会输出3

var dummy [3]int
var f func()
for i := 0; i < len(dummy); i++ {
	f = func() {
		println(i)
	}
}
f() // 3
1
2
3
4
5
6
7
8

为啥是3呢,其实是因为i加到3才会跳出循环,此时我们打印的是i的地址,所以会打印3,但是如果我们用for range来实现,结果又不同了

var dummy [3]int
var f func()
for i := range dummy {
	f = func() {
		println(i)
	}
}
f() // 2
1
2
3
4
5
6
7
8

这是因为 for range 和 for 底层实现上的不同。还有下面这个例子

var funcSlice []func()
for i := 0; i < 3; i++ {
	funcSlice = append(funcSlice, func() {
		println(i)
	})

}
for j := 0; j < 3; j++ {
	funcSlice[j]() // 3, 3, 3
}
1
2
3
4
5
6
7
8
9
10

输出序列为 3, 3, 3。

怎么解决上面的问题呢

1. 声明新变量:

  • 声明新变量:j := i,且把之后对 i 的操作改为对 j 操作。
  • 声明新同名变量:i := i。注意:这里短声明右边是外层作用域的 i,左边是新声明的作用域在这一层的 i。原理同上。

这相当于为这三个函数各声明一个变量,一共三个,这三个变量初始值分别对应循环中的 i 并且之后不会再改变。

2. 声明新匿名函数并传参:

var funcSlice []func()
for i := 0; i < 3; i++ {
	func(i int) {
		funcSlice = append(funcSlice, func() {
			println(i)
		})
	}(i)

}
for j := 0; j < 3; j++ {
	funcSlice[j]() // 0, 1, 2
}
1
2
3
4
5
6
7
8
9
10
11
12

现在 println(i) 使用的 i 是通过函数参数传递进来的,并且 Go 语言的函数参数是按值传递的。

所以相当于在这个新的匿名函数内声明了三个变量,被三个闭包函数独立引用。原理跟第一种方法是一样的。

这里的解决方法可以用在大多数跟闭包引用有关的问题上,不局限于第三个例子。

参考:

Go 语言闭包详解 (juejin.cn) (opens new window)

# go的main函数

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

# 接口

接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。

golang-interface

我们可以使用结构体指针或者结构体来实现接口,但是默认情况下,还是推荐使用指针

type Cat struct {}
type Duck interface { ... }

func (c  Cat) Quack {}  // 使用结构体实现接口
func (c *Cat) Quack {}  // 使用结构体指针实现接口

var d Duck = Cat{}      // 使用结构体初始化变量
var d Duck = &Cat{}     // 使用结构体指针初始化变量
1
2
3
4
5
6
7
8
结构体实现接口 结构体指针实现接口
结构体初始化变量 通过 不通过
结构体指针初始化变量 通过 通过

后面一些东西过于底层,所以先暂时跳过

# 值接收者和指针接收者

其实就是初始化结构体的两种方式,在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。

package main
import "fmt"
type Person struct {
    age int
}
func (p Person) Elegance() int {
    return p.age
}
func (p *Person) GetAge() {
    p.age += 1
}
func main() {
    // p1 是值类型
    p := Person{age: 18}
    // 值类型 调用接收者也是值类型的方法
    fmt.Println(p.howOld())
    // 值类型 调用接收者是指针类型的方法
    p.GetAge()
    fmt.Println(p.GetAge())

    // ----------------------
    // p2 是指针类型
    p2 := &Person{age: 100}
    // 指针类型 调用接收者是值类型的方法
    fmt.Println(p2.GetAge())
    // 指针类型 调用接收者也是指针类型的方法
    p2.GetAge()
    fmt.Println(p2.GetAge())
}
/**
18
19
100
101
**/
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
函数和方法 值接收者 指针接收者
值类型调用者 方法会使用调用者的一个副本,类似于“传值” 使用值的引用来调用方法,上例中,p1.GetAge() 实际上是 (&p1).GetAge().
指针类型调用者 指针被解引用为值,上例中,p2.GetAge()实际上是 (*p1).GetAge() 实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针

如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。

如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。

通常我们使用指针作为方法的接收者的理由:

  • 使用指针方法能够修改接收者指向的值。
  • 可以避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。

因而呢,我们是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的本质。

如果类型具备“原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 header,而 header 本身就是为复制设计的。

如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份实体。

接口值的零值是指动态类型和动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil。

# 底层实现

go的底层主要包括下面这两部分组成

type iface struct {
	tab  *itab
	data unsafe.Pointer
}
1
2
3
4

tab 中存放的是类型、方法等信息。data 指针指向的 iface 绑定对象的原始数据的副本。这里同样遵循 Go 的统一规则,值传递。tab 是 itab 类型的指针。

itab 中包含 5 个字段。inner 存的是 interface 自己的静态类型。_ type 存的是 interface 对应具体对象的类型。itab 中的 _type 和 iface 中的 data 能简要描述一个变量。 _type 是这个变量对应的类型,data 是这个变量的值。这里的 hash 字段和 _type 中存的 hash 字段是完全一致的,这么做的目的是为了类型断言(下文会提到)。fun 是一个函数指针,它指向的是具体类型的函数方法。虽然这里只有一个函数指针,但是它可以调用很多方法。在这个指针对应内存地址的后面依次存储了多个方法,利用指针偏移便可以找到它们。

type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
1
2
3
4
5
6
7

# 空 interface 数据结构

空的 inferface{} 是没有方法集的接口。所以不需要 itab 数据结构。它只需要存类型和类型对应的值即可。对应的数据结构如下:

type eface struct {
	_type *_type
	data  unsafe.Pointer
}
1
2
3
4

从这个数据结构可以看出,只有当 2 个字段都为 nil,空接口才为 nil。空接口的主要目的有 2 个,一是实现“泛型”,二是使用反射。

。。。。后面的大佬直接啃到汇编去了,打扰了。。。

深入研究 Go interface 底层实现 (halfrost.com) (opens new window)

# 反射

reflect (opens new window) 实现了运行时的反射能力,包括动态修改变量、判断类型是否实现了某些接口以及动态调用方法等功能。能够让程序操作不同类型的对象1 (opens new window)。反射包中有两对非常重要的函数和类型,两个函数分别是:

  • reflect.TypeOf (opens new window) 能获取类型信息;
  • reflect.ValueOf (opens new window) 能获取数据的运行时表示;
func Test_question(t *testing.T) {
   a:=456
   fmt.Println(reflect.TypeOf(a))
   fmt.Println(reflect.ValueOf(a))
   /*
   * int
   * 456
   */
}
1
2
3
4
5
6
7
8
9

# 反射的三大法则

  1. 从 interface{} 变量可以反射出反射对象;
  2. 从反射对象可以获取 interface{} 变量;
  3. 要修改反射对象,其值必须可设置;

# 反射的底层与原理

数据interface中保存有结构数据,只要想办法拿到该数据对应的内存地址,然后把该数据转成interface,通过查看interface中的类型结构,就可以知道该数据的结构了

img

参考:

  1. 图解go反射实现原理 - 菜刚RyuGou的博客 (i6448038.github.io) (opens new window)

# 指针

go通过指针变量p访问成员变量的时候,有下面这两种方式来访问

p.name
(*p).name
1
2

为什么->不行呢,因为这个符号是用来操作管道的

  • go其实是可以自动解引的,所以我们可以不使用*来获取指针,但是go解引能力有限,只能解除一次引用
  • &是取地址符,放到变量前使用,就会返回相应变量的内存地址。
  • *用于来获取指针的内容,指针变量可以使用这个符号来获取内容
  • 结构体指针,使用 "." 操作符来访问结构体成员
  • go的指针是属于引用类型,是复合类型中的一种
  • go语言的指针不支持指针运算

# nil类型

Go语言中的引用类型只有五个:

切片 映射 函数 方法 通道

nil只能赋值给上面五种通道类型的变量以及指针变量。

# ... 操作符

有两个用法,一个用于函数里面 多参数 ,一个是append里面 合并 切片

这个一般用于函数拥有多个参数的情况下

img

下面这种方式调用是没有问题的

add([]int{1, 3, 7}...)
1

img

img

# 常量

单个声明

显式类型定义: const b string = "abc"
隐式类型定义: const b = "abc"
1
2

常量枚举

const (
    Unknown = 0
    Female = 1
    Male = 2
)
1
2
3
4
5

常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值。常量表达式中,函数必须是内置函数

特殊常量

iota,特殊常量,可以认为是一个可以被编译器修改的常量。

iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。

const (
    a = iota
    b = iota
    c = iota
)
// 第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:
const (
    a = iota
    b
    c
)
// 甚至可以这样用
const (
    a = iota   //0
    b          //1
    c          //2
    d = "ha"   //独立值,iota += 1
    e          //"ha"   iota += 1
    f = 100    //iota +=1
    g          //100  iota +=1
    h = iota   //7,恢复计数
    i          //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
编辑(master改成main) (opens new window)
上次更新: 2023/02/06, 19:47:34
数据结构
常用关键字

← 数据结构 常用关键字→

Theme by Vdoing | Copyright © 2023-2023 小哈里
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式