chaoz的杂货铺

生命有息、学无止境、折腾不止

0%

2022-Golang-基础复盘

基础

= 和 := 的区别?

声明和赋值,仅赋值

声明:var

var type const new make 区别

type是定义一种类型,定义一个结构体
var是定义一个变量
const是定义一个常量

var 声明
值类型:分配内存空间,并赋该类型的零值
引用类型:不会分配内存,默认就是nil
var 会对int string基本类型和 struct 分配地址,并置为零值;
而对于sliect map这种引用类型的,只是存在一个指针地址,并没有分配空间,len()=0,也就是空切片;
对于指针*,系统不会分配地址,默认就是nil
全局变量声明必须以var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。

new 分配
值类型和引用类型:分配内存空间,并赋该类型的零值,返回一个指向该类型内存地址的指针

make 初始化
make用于map, slice,chan 的内存创建,因为他们三个是引用类型,直接返回这三个类型本身(引用类型),不赋零值
make也是用于内存分配的,但是和new不同,它只用于chan、map以及切片的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
注意,因为这三种类型是引用类型,所以必须得初始化,但是不是置为零值,这个和new是不一样的。

20220302155442

nil

nil其实甚至不是golang的关键词,只是一个变量名。定义在 buildin/buildin.go 中
在golang中nil代表了pointer, channel, func, interface, map 或者 slice 的zero value.
nil只能赋值给指针、channel、func、interface、map或slice类型的变量 (非基础类型) 否则会引发 panic

指针的作用

‘*’ 返回地址中的值
‘&’ 返回变量的地址

Go 允许多个返回值吗?

支持

Go 有异常类型吗?

只有错误类型,返回错误信息,是否有值来标识错误状态。

如何高效地拼接字符串

go语言中的字符串拼接是只读的,每一次的修改操作,都会去创建新的对象。使用 strings.Builder,最小化内存拷贝次数。
https://www.cnblogs.com/hiyong/p/15226261.html#stringsbuilder%E7%B1%BB%E5%9E%8B
java中的呢:
python中的:

golang的指针类型,unsafe.Pointer类型和uintptr类型的区别

指针类型

golang支持指针类型,指针类型的变量存的是一个内存地址,这个地址指向的内存空间存的才是一个具体的值。
比如int,int32,A(自定义结构体类型),string等,都是指针类型。

unsafe.Pointer类型

它是实现定位和读写的内存的基础。go runtime大量使用它。
任何类型的指针都可以被转化为Pointer
Pointer可以被转化为任何类型的指针
uintptr可以被转化为Pointer
Pointer可以被转化为uintptr

从垃圾收集器的视角来看,一个unsafe.Pointer是一个指向变量的指针,因此当变量被移动时对应的指针也必须被更新;

使用

可以让你的变量在不同的指针类型转来转去,也就是表示为任意可寻址的指针类型

uintptr类型

uintptr是golang的内置类型。是能存储指针的整形。
uintptr可以和unsafe.Pointer类型互转。
uintptr可以做指针运算,这一点有时候很重要,但是依赖平台,同一类型变量在不同的平台占用的存储空间大小不一样,在用uintptr做指针运算的时候,偏移量也会相应的不一样(后面有例子说明)。

使用

uintptr是一个无符号的整型数,足以保存一个地址

unsafe是不安全的,所以我们应该尽可能少的使用它,比如内存的操纵,这是绕过Go本身设计的安全机制的,不当的操作,可能会破坏一块内存,而且这种问题非常不好定位。

当然必须的时候我们可以使用它,比如底层类型相同的数组之间的转换;
比如使用sync/atomic包中的一些函数时;还有访问Struct的私有字段时;
unsafe包都是用于Go编译器的,在编译的时候,Go编译器已经把他们都处理了。

make 支持的类型

golang 分配内存主要有内置函数new和make,今天我们来探究一下make有哪些玩法。

make只能为slice, map, channel分配内存,并返回一个初始化的值。首先来看下make有以下三种不同的用法:

  1. make(map[string]string)
  2. make([]int, 2)
  3. make([]int, 2, 4)

第一种用法,即缺少长度的参数,只传类型,这种用法只能用在类型为map或chan的场景,例如make([]int)是会报错的。这样返回的空间长度都是默认为0的。

第二种用法,指定了长度,例如make([]int, 2)返回的是一个长度为2的slice

第三种用法,第二参数指定的是切片的长度,第三个参数是用来指定预留的空间长度,例如a := make([]int, 2, 4), 这里值得注意的是返回的切片a的总长度是4,预留的意思并不是另外多出来4的长度,其实是包含了前面2个已经切片的个数的。所以举个例子当你这样用的时候 a := make([]int, 4, 2),就会报语法错误。

定义一个切片长度之后,默认的为零。

因此,当我们为slice分配内存的时候,应当尽量预估到slice可能的最大长度,通过给make传第三个参数的方式来给slice预留好内存空间,这样可以避免二次分配内存带来的开销,大大提高程序的性能。

总结:
make 仅用来分配及初始化类型为 slice、map、chan 的数据。new 可分配任意类型的数据.
new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type.
new 分配的空间被清零, make 分配空间后,会进行初始化.

07 什么是 rune 类型
09 Go 支持默认参数或可选参数吗0?

golang的值类型和引用类型

1.值类型:基本数据类型int, float,bool, string以及数组和struct
值类型:变量直接存储值,内容通常在栈中分配
var i = 5 i —–> 5
2.引用类型:指针,slice,map,chan等都是引用类型
引用类型:变量存储的是一个地址,这个地址存储最终的值,内容通常在堆上分配,通过GC回收
ref r ——> 内存地址 —–> 值

defer 的执行顺序 以及原理是什么?

20220227234946

https://www.kancloud.cn/aceld/golang/1958310

原理

实现原理:

Go1.14中编译器会将defer函数直接插入到函数的尾部,无需链表和栈上参数拷贝,性能大幅提升。把defer函数在当前函数内展开并直接调用,这种方式被称为open coded defer

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func A(i int) {
defer A1(i, 2*i)
if(i > 1) {
defer A2("Hello", "eggo")
}
// code to do something
return
}
func A1(a,b int) {
//......
}
func A2(m,n string) {
//......
}
编译后(伪代码):

func A(i int) {
// code to do something
if(i > 1){
A2("Hello", "eggo")
}
A1(i, 2*i)
return
}

defer是怎么用的

https://www.liwenzhou.com/posts/Go/09_function/

如何交换 2 个变量的值?

a,b = b,a

Go 语言 tag 的用处?

在使用反射可以动态的给结构体成员赋值,正是因为有 tag,在赋值前可以使用 tag 来决定赋值的动作。比如,官方的 encoding/json 包,可以将一个 JSON 数据 Unmarshal 进一个结构体,此过程中就使用了 Tag, 该包定义一些规则,只要参考该规则设置 tag 就可以将不同的 JSON 数据转换成结构体。
基于 struct 的 tag 特性,有了诸如 json、orm 等等的应用。理解这个可以定义另一种 tag 规则,来处理特有的数据。

必须要理解的是 Tag 是 Struct 的一部分,前面说过 Tag 只有在反射场景中才有用,而反射包中提供了操作 Tag 的方法,在说方法前,有必要先了解一下 Go 是如何管理 struct 字段的。

https://learnku.com/articles/61564
https://www.flysnow.org/2017/06/25/go-in-action-struct-tag.html

切片

如何判断 2 个字符串切片(slice) 是相等的?切片的扩容方式?

切片底层:数组。
一个切片的底层数组永远不会被替换。为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。

“扩容”,切片和底层数组都是新生成的。

切片容量的增长:先是2倍数增长,当达到1024时,1.25倍增长。此外在得到新的容量会乘以slice元素的类型size,算出新的容量需要的内存capmem向上取整(sizeclasses.go文件,在这个文件的开头,给出了golang对象大小表)得到新的所需内存,再除以size,作为最终的容量,专业名词叫内存对齐。

切片缩容之后还是会引用底层的原数组,这有时候会造成大量缩容之后的多余内容没有被垃圾回收。可以使用新建一个数组然后copy的方式。

旧的切片,无论是扩容或者缩容都会有老的切片释放出来,这个时候应该是被回收了!不然肯定会内存泄露的。如果仍然存在与老切片有关的变量,别忘了置 nil。GC 回收老切片有一个必要条件,那就是:已经没有任何代码引用它了。

再说一遍slice是引用类型之一。Go语言里不会传引用,。但是,內建数据类型中会有值类型和引用类型之分。

当我们想删除切片中的元素的时候就没那么简单了。元素复制一般是免不了的,就算只删除一个元素,有时也会造成大量元素的移动。这时还要注意空出的元素槽位的“清空”,否则很可能会造成内存泄漏。另一方面,在切片被频繁“扩容”的情况下,新的底层数组会不断产生,这时内存分配的量以及元素复制的次数可能就很可观了,这肯定会对程序的性能产生负面的影响。尤其是当我们没有一个合理、有效的”缩容“策略的时候,旧的底层数组无法被回收,新的底层数组中也会有大量无用的元素槽位。过度的内存浪费不但会降低程序的性能,还可能会使内存溢出并导致程序崩溃。由此可见,正确地使用切片是多么的重要。

数组与切片的区别

https://studygolang.com/articles/18145?fr=sidebar

数组与切片传参时的区别

数组:值传递
切片:指针传递。切片看上去像是引用传递,但其实是值传递。切片的值传递是复制的切片中数组point的指针,所以造成的现象是修改切片数据,调用者也变了,实际上他们修改的都是底层数组的数据。

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
package main

import "fmt"

func myAppend(s []int) []int {
// 这里 s 虽然改变了,但并不会影响外层函数的 s
s = append(s, 100)
fmt.Println("s3", s, len(s), cap(s))
return s
}

func myAppendPtr(s *[]int) {
// 会改变外层 s 本身
*s = append(*s, 100)
return
}

func main() {
s := make([]int, 0, 5)
fmt.Println("s1", s, len(s), cap(s))
s = append(s, 1, 1, 1)
fmt.Println("s2", s, len(s), cap(s))
newS := myAppend(s)

fmt.Println("s4", s, len(s), cap(s))
fmt.Println(newS, len(newS), len(newS))

myAppendPtr(&s)
fmt.Println("s5", s, len(s), cap(s))

s = newS

myAppendPtr(&s)
fmt.Println("s6", s, len(s), cap(s))
f0(s)
fmt.Println("s7", s, len(s), cap(s))
f1(s)
fmt.Println("s8", s, len(s), cap(s))

}

func f0(s []int) {
// i只是一个副本,不能改变s中元素的值
for _, i := range s {
i++
}
}

func f1(s []int) {

for i := range s {
s[i] += 1
}
}

20220301134207
https://www.bookstack.cn/read/qcrao-Go-Questions/%E6%95%B0%E7%BB%84%E5%92%8C%E5%88%87%E7%89%87-%E5%88%87%E7%89%87%E4%BD%9C%E4%B8%BA%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0.md

切片的线程安全

Golang 切片如何删除数据

删除数组切片中的特定的值具体应该怎么做?

slice和map的区别,slice和数组的区别

深拷贝,什么时候需要深拷贝

深拷贝

深拷贝:拷贝的是数据本身,创造一个新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值

实现深拷贝的方式:

copy(slice2, slice1)

遍历append赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := make([]int, 5, 5)
fmt.Printf("slice1: %v, %p
", slice1, slice1)
copy(slice2, slice1)
fmt.Printf("slice2: %v, %p
", slice2, slice2)
slice3 := make([]int, 0, 5)
for _, v := range slice1 {
slice3 = append(slice3, v)
}
fmt.Printf("slice3: %v, %p
", slice3, slice3)
}

slice1: [1 2 3 4 5], 0xc0000b0030
slice2: [1 2 3 4 5], 0xc0000b0060
slice3: [1 2 3 4 5], 0xc0000b0090

浅拷贝

拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化

实现浅拷贝的方式:

引用类型的变量,默认赋值操作就是浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
slice2 := slice1
func main() {
slice1 := []int{1, 2, 3, 4, 5}
fmt.Printf("slice1: %v, %p
", slice1, slice1)
slice2 := slice1
fmt.Printf("slice2: %v, %p
", slice2, slice2)
}

slice1: [1 2 3 4 5], 0xc00001a120
slice2: [1 2 3 4 5], 0xc00001a120

链表

基础知识

相关算法

实际场景

go怎么实现封装继承多态

container/ring包中的循环链表的适用场景都有哪些?
你使用过container/heap包中的堆吗?它的适用场景又有哪些呢?

字典

在 Go 语言的字典中,每一个键值都是由它的哈希值代表的。也就是说,字典不会独立存储任何键的值,但会独立存储它们的哈希值。

要把字典的键类型设定为任何接口类型。如果非要这么做,请一定确保代码在可控的范围之内。

首先,每个哈希桶都会把自己包含的所有键的哈希值存起来。Go 语言会用被查找键的哈希值与这些哈希值逐个对比,看看是否有相等的。如果一个相等的都没有,那么就说明这个桶中没有要查找的键值,这时 Go 语言就会立刻返回结果了。

如果有相等的,那就再用键值本身去对比一次。为什么还要对比?原因是,不同值的哈希值是可能相同的。这有个术语,叫做“哈希碰撞”。

所以,即使哈希值一样,键值也不一定一样。如果键类型的值之间无法判断相等,那么此时这个映射的过程就没办法继续下去了。最后,只有键的哈希值和键值都相等,才能说明查找到了匹配的键 - 元素对。

性能

应该优先考虑哪些类型作为字典的键类型?
求哈希和判等操作的速度越快,对应的类型就越适合作为键类型。

以求哈希的操作为例,宽度越小的类型速度通常越快。对于布尔类型、整数类型、浮点数类型、复数类型和指针类型来说都是如此。对于字符串类型,由于它的宽度是不定的,所以要看它的值的具体长度,长度越短求哈希越快。

不建议你使用这些高级数据类型作为字典的键类型,不仅仅是因为对它们的值求哈希,以及判等的速度较慢,更是因为在它们的值中存在变数。

比如,对一个数组来说,我可以任意改变其中的元素值,但在变化前后,它却代表了两个不同的键值。

在值为nil的字典上执行读操作会成功吗,那写操作呢?
var m map[string]int //报错
var m = make(map[string]int)

由于字典是引用类型,所以当我们仅声明而不初始化一个字典类型的变量的时候,它的值会是nil。

哈希碰撞

把任意长度的值通过哈希算法输出固定长度的散列值。

如果两个值对应的哈希值时一样的,称之为哈希碰撞。数据量多的情况下,冲突必然存在。

如何避免:开放寻址法和链接法。(迷啊
开放寻址:检测哈希表中的下一个位置,索引加一。可能会得到三种结果:命中;未命中,键为空;继续查找,该位置键存在,但键不同

列表

底层实现是什么样的?

Golang可见性规则(公有与私有,访问权限)

Go语言没有像其它语言一样有public、protected、private等访问控制修饰符,它是通过字母大小写来控制可见性的,如果定义的常量、变量、类型、接口、结构、函数等的名称是大写字母开头表示能被其它包访问或调用(相当于public),非大写开头就只能在包内使用(相当于private,变量或常量也可以下划线开头)

指针

我们需要修改结构体的变量内容的时候,方法传入的结构体变量参数需要使用指针,也就是结构体的地址

需要修改map中的架构体的变量的时候也需要使用结构体地址作为map的value

如果仅仅是读取结构体变量,可以不使用指针,直接传递引用即可

*type 这里的type这个变量存放的东西是地址。指向地址符号
&type获取到地址。取地址符号。

程序是如何检查对象指针来寻找和调度所需函数。

执行规则

if/for/switch/goto

for

三种方式
20220301001504

switch

switch是一个条件语句,它计算表达式并将其与可能匹配的列表进行比较,并根据匹配执行代码块。它可以被认为是一种惯用的方式来写多个if else子句。
switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上直下逐一测试,直到匹配为止。 switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加break。
而如果switch没有表达式,它会匹配true
Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。fallthrough应该是某个case的最后一行。如果它出现在中间的某个地方,编译器就会抛出错误。
case中的表达式是可选的,可以省略。如果该表达式被省略,则被认为是switch true,并且每个case表达式都被计算为true,并执行相应的代码块。
20220301000505

变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。 您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3。
20220301000425

Type Switch

20220301000720

goto

goto:可以无条件地转移到过程中指定的行。

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

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

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

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

Fprintf()是把格式化字符串输出到文件中。

错误处理

panic/recover

语法糖

:=

20220228151752
20220228151842
20220228151903

变量作用域

全局变量的作用域是整个包,局部变量的作用域是该变量所在的花括号内,这是一个很基础的问题。我们通常会使用golang的一个语法糖:=来给变量赋值,这种方式可以节省掉我们定义变量的代码,让代码变的更加简洁,但是如果你定义了一个全局变量,又不小心用:=来给它赋值,就会出现一些问题。

  1. 尽量少的使用全局变量。
  2. 尽量少的使用:=语法糖。
  3. 使用:=的时候要确保左值没有被定义过。

可变参数

20220228153712
20220228153856

静态类型与动态类型编程语言之间的区别

静态类型 VS 动态类型

在静态语言中,一旦声明一个变量是int类型,之后就只能将int类型的数据赋值给它,否则就会引发错误,而动态类型则没有这样的限制,你将什么类型的数据赋值给变量,这个变量就是什么类型。

动态类型:
PHP Ruby Python
常见的静态类型语言则有:
C、C++、JAVA、C#

强类型 VS 弱类型

一个int类型的数据与一个float类型的数据相加,最终的结果是一个float类型的数据,这个过程就发生了隐式类型转换。
PHP,Perl都属于弱类型语言,其他编程语言,你所常见的,比如java, C, C++, Python皆属于强类型语言。

select

select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。

select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。
说明:
每个case都必须是一个通信
所有channel表达式都会被求值
所有被发送的表达式都会被求值
如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行。
否则:
如果有default子句,则执行该语句。
如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

PS:
早期的select函数是用来监控一系列的文件句柄,一旦其中一个文件句柄发生IO操作,该select调用就会被返回。golang在语言级别直接支持select,用于处理异步IO问题。

go 打印时 %v %+v %#v 的区别?

%v 只输出所有的值;
%+v 先输出字段名字,再输出该字段的值;
%#v 先输出结构体名字值,再输出结构体(字段名字+字段的值);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"

type student struct {
id int32
name string
}

func main() {
a := &student{id: 1, name: "微客鸟窝"}

fmt.Printf("a=%v \n", a) // a=&{1 微客鸟窝}
fmt.Printf("a=%+v \n", a) // a=&{id:1 name:微客鸟窝}
fmt.Printf("a=%#v \n", a) // a=&main.student{id:1, name:"微客鸟窝"}
}

Go 语言中如何表示枚举值(enums)?

空 struct{} 的用途

go类型断言

结构体与联合体的区别

Go 管理依赖 gomod命令,gomod最后的版本号如果没有tag,是怎么生成的

一个a+b程序从编译到运行都发生了什么

go你们用的什么版本,版本特性

go的值传递和引用传递

Go 语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷 贝的内容有时候是非引用类型(int、string、struct 等这些),这样就在函 数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan 等这 些),这样就可以修改原内容数据。

Golang 的引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针

怎么用go实现一个栈

Go 如何查看性能:pprof

Go 如何进行调试:gdb/delve

Go 如何打印堆栈:runtime.Caller

Gin 框架

gin框架的路由是怎么处理的?

中间件和工作机制有了解吗?

go实现不重启热部署

go性能分析工具

Go语言第三方包依赖的管理方式

使用pprof和go-torch做性能分析

https://www.cnblogs.com/li-peng/p/9391543.html
https://zhuanlan.zhihu.com/p/371713134
https://segmentfault.com/a/1190000016412013
https://www.jianshu.com/p/f4690622930d

Go Module 存在的意义与解决的问题

https://studygolang.com/articles/23964?fr=sidebar

规范

一个相关的原则是:既不要把你程序的细节暴露给外界,也尽量不要让外界的变动影响到你的程序。你可以想想这个原则在这里可以起到怎样的指导作用。

如果一个包要依赖另一个包,这个时候如何写单元测试

喜欢这篇文章?打赏一下作者吧!

欢迎关注我的其它发布渠道