title: Go全栈面试题(1) -Go基础语法面试题
tags: go
author: Clown95
Golang基础面试
使用Go编程有什么好处?
以下是使用Go编程的好处 :
- 支持环境采用类似于动态语言的模式。例如,类型推断(x:= 0是int类型的变量x的有效声明)。
- 编译时间很快。
- InBuilt并发支持:轻量级进程(通过goroutines),通道,select语句。
- 简洁,简洁,安全。
- 支持接口和类型嵌入。
- 生成静态链接的本机二进制文件,没有外部依赖项。
一个包是否可以在一个源文件里面多次引用?
一个Go源文件可以多次引入同一个包。但是每次的引入名称必须不同。这些相同的包引入引用着同一个包实例。
简述内置的print和println函数与fmt和log标准库包中相应的打印函数有什么区别?
- 内置的print/println函数总是写入标准错误。 fmt标准包里的打印函数总是写入标准输出。 log标准包里的打印函数会默认写入标准错误,然而也可以通过函数来配置。
- 内置print/println函数的调用不能接受数组和结构体参数。
- 对于组合类型的参数,内置的print/println函数将输出参数的底层值部的地址,而fmt和log标准库包中的打印函数将输出参数的字面值。
- 目前(Go 1.12),对于标准编译器,调用内置的print/println函数不会使调用参数引用的值逃逸到堆上,而fmt和log标准库包中的的打印函数将使调用参数引用的值逃逸到堆上。
- 如果一个实参有String() string或Error() string方法,那么fmt和log标准库包里的打印函数在打印参数时会调用这两个方法,而内置的print/println函数则会忽略参数的这些方法。
- 内置的print/println函数不保证在未来的Go版本中继续存在。
什么是byte?什么是rune? 如何将[]byte
和[]rune
类型的值转换为字符串?
在Golang里,byte是uint8类型的一个别名。 换言之,byte 和 uint8是相同的类型。 rune和int32属于同样类似的关系。一个rune通常被用来存储一个Unicode码点。
[]byte和[]rune类型的值可以被显式地直接转换成字符串,反之亦然。
Golang中数组和切片之间的区别?
Slices是Golang中数组的包装器。 总是优先使用切片而不是数组。 极少数情况下数组会有益。
一种情况是数组的大小是固定的(比如存储IPv4地址)
还有一种是当我们不希望函数修改原始副本时,我们可能会使用数组。但是即使在这种情况下,也应该有一种传递切片的方法。
在以下两个切片声明中有什么区别(如果有的话),哪一个更可取?
var a []int
和
a := []int{}
如果未使用切片,则第一个声明不会分配内存,因此首选此声明方法。
什么可以/不可以作为方法的接收者?
结构体,指向结构体的指针,甚至内置类型的别名,如’type myIntType int’。都可以充当接收者。甚至函数也可以是接收器。
我们不能使用以下东西作为接收类型:
- 方法,如果我们在对象类型上定义方法,它就不能像普通函数一样用作接收方类型。
- 接口,在Go中,接口为类型定义了一组可能的操作。它们没有定义实际的实现。因此它们不能用作方法的接收者,因为方法是关于实现的。
值接收者和指针接收者的区别?
方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是值接收者,也可以是指针接收者。
在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。
也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。
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
函数和方法 | 值接收者 | 指针接收者 |
---|---|---|
值类型调用者 | 方法会使用调用者的一个副本,类似于“传值” | 使用值的引用来调用方法,上例中,() 实际上是 (&p1).GetAge() |
指针类型调用者 | 指针被解引用为值,上例中,()实际上是 (*p1).GetAge() | 实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针 |
如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。
如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。
通常我们使用指针作为方法的接收者的理由:
-
使用指针方法能够修改接收者指向的值。
-
可以避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。
因而呢,我们是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的本质。
如果类型具备“原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 header,而 header 本身就是为复制设计的。
如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份实体。
接口值的零值是指动态类型和动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil。
哪些类型不支持比较?
- 映射(map)
- 切片
- 函数
- 包含不可比较字段的结构体类型
- 元素类型为不可比较类型的数组类型
- 不支持比较的类型不能用做映射类型的键值类型。
请注意:
尽管映射,切片和函数值不支持比较,但是它们的值可以与类型不确定的nil标识符比较。
如果两个接口值的动态类型相同且不可比较,那么在运行时比较这两个接口的值会产生一个恐慌。
go语言中哪些类型的值可以被取地址,哪些不可以被取地址?
以下的值是不可以寻址的:
- 字符串的字节元素
- 映射元素
- 接口值的动态值(类型断言的结果)
- 常量值
- 字面值
- 声明的包级别函数
- 方法(用做函数值)
- 中间结果值
- 函数调用
- 显式值转换
- 各种操作,不包含指针解引用(dereference)操作,但是包含:
- 数据通道接收操作
- 子字符串操作
- 子切片操作
- 加法、减法、乘法、以及除法等等。
请注意:&T{}在Go里是一个语法糖,它是tmp := T{}; (&tmp)的简写形式。 所以&T{}是合法的,并不代表字面值T{}是可寻址的。
以下的值是可寻址的,因此可以被取地址:
- 变量
- 可寻址的结构体的字段
- 可寻址的数组的元素
- 任意切片的元素(无论是可寻址切片或不可寻址切片)
- 指针解引用(dereference)操作
为什么映射元素不可被取地址?
如果映射元素可以被取地址,则每个映射元素的地址在它的生命期内必须保持不变。 这阻碍了Go编译器在实现映射时使用更加有效率的算法。 对于标准编译器,映射元素的内部地址在运行时刻有可能发生改变。
为什么非空切片的元素总是可被取地址,即便对于不可寻址的切片也是如此?
切片的内部类型是一个结构体,类似于
struct {
elements unsafe.Pointer // 引用着一个元素序列
length int
capacity int
}
每一个切片间接引用一个元素序列。 尽管一个非空切片是不可取地址的,它的内部元素序列需要开辟在内存中的某处,因而必须是可取地址的。 取一个切片的元素地址事实上是取内部元素序列上的元素地址。 这就是为什么不可寻址的非空切片的元素是也可以被取地址的。
哪些类型是值类型,那些是引用类型?
- 值类型:基本数据类型,int,float,bool,string,以及数组和struct
- 引用类型:指针,slice,map,chan等都是引用类型
go struct能不能比较?
- 相同struct类型的可以比较
- 不同struct类型的不可以比较,编译都不过,类型不匹配
为什么使用空 struct{}?
如果要保存一些内存,可以使用空结构。空结构不会为其值获取任何内存。
a := struct{}{}
println(unsafe.Sizeof(a))
// Output: 0
- 如果使用的是map,而且map又很长,通常会节省不少资源
- 空struct{}也在向别人表明,这里并不需要一个值。
以下是一些有用的示例:
- 在map里节省资源的用途:
set := make(map[string]struct{})
for _, value := range []string{"apple", "orange", "apple"} {
set[value] = struct{}{}
}
fmt.Println(set)
// Output: map[orange:{} apple:{}]
- 使用seen哈希,就像遍历MAP时一样:
seen := make(map[string]struct{})
for _, ok := seen[v]; ok {
// First time visiting a vertex.
seen[v] = struct{}{}
}
- 构建对象时,只对一组方法感兴趣而没有中间数据,或者当您不打算保留对象状态时。在下面的示例中,无论方法是在相同的情况下调用(情况#1)还是在两个不同的对象上调用(情况#2),它都没有区别:
type Lamp struct{}
func (l Lamp) On() {
println("On")
}
func (l Lamp) Off() {
println("Off")
}
func main() {
// Case #1.
var lamp Lamp
lamp.On()
lamp.Off()
// Output:
// on
// off
// Case #2.
Lamp{}.On()
Lamp{}.Off()
// Output:
// on
// off
}
- 当您需要一个通道来发出事件信号时,却不需要发送任何数据。这个事件也不是最后一个,在这种情况下,您将使用close(ch)内置函数。
func worker(ch chan struct{}) {
// Receive a message from the main program.
<-ch
println("roger")
// Send a message to the main program.
close(ch)
}
func main() {
ch := make(chan struct{})
go worker(ch)
// Send a message to a worker.
ch <- struct{}{}
// Receive a message from the worker.
<-ch
println(“roger")
// Output:
// roger
// roger
}
slice的len,cap,扩容,共享。
一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。
- len切片的长度是它所包含的元素个数。
- cap切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
- append新建对象,s2指向了新对象,函数退出新对象释放。
在函数内用append时,append会自动以倍增的方式扩展slice_2的容量,但是扩展也仅仅是函数内slice_2的长度和容量,slice_1的长度和容量是没变的,所以在函数外打印时看起来就是没变。原来的s1还是s1,append没有影响,但是s2修改的操作有影响,因为s2直接操作了s1的内存。
slice被设计为指向数组的指针,在需要扩容时,会将底层数组上的值复制到一个更大的数组上然后指向这个新数组。
slice有个特性是允许多个slice指向同一个底层数组,这是一个有用的特性,在很多场景下都能通过这个特性实现 no copy 而提高效率。但共享同时意味着不安全。
func main() {
a := make([]int, 2, 2)
a[0], a[1] = 1, 2
b := append(a[0:1], 3)
c := append(a[1:2], 4)
fmt.Println(b,c)
}
比如说这段代码我们期望的值是[1 3] [2 4],但是却变成了[1 3] [3 4]。
那么怎么解决数据共享的问题?
- make出一个新slice,然后先copy前缀到新数组上再追加。
- 利用go中slice的一个小众语法,a[0:1:1] (源[起始index,终止index,cap终止index]),强迫追加时复制到新数组。
map如何顺序读取?
可以通过sort中的排序包进行对map中的key进行排序
package main
import (
"fmt"
"sort"
)
func main() {
var m = map[string]int{
"hello": 0,
"morning": 1,
"my": 2,
"girl": 3,
}
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println("Key:", k, "Value:", m[k])
}
}
使用range修改结构体切片的元素的值会发生什么情况?怎么避免这种情况?
因为for遍历时,使用的是副本,每次遍历仅进行struct值拷贝,而不是指向值的指针。
我们可以通过指针更改元素,通过指针更改元素,如果不设置中间变量的话,又会照成所有的map的值都是一样的。
Go中的零指针是什么?
如果您没有要分配的确切地址,Go编译器会将Nil值分配给指针变量。这是在变量声明时完成的。指定为nil的指针称为nil指针。nil指针是一个常量,在几个标准库中定义了零值。
如何使用go实现set?
根据go中map的keys的无序性和唯一性,可以将其作为set
如何比较两个结构体?如何比较两个接口?
可以使用==操作符比较两个结构体,就像使用其他简单类型一样。
只要确保它们不包含任何片、映射或函数,有这些情况下代码将不会被编译。
type Foo struct {
A int
B string
C interface{}
}
a := Foo{A: 1, B: "one", C: "two"}
b := Foo{A: 1, B: "one", C: "two"}
println(a == b)
// Output: true
type Bar struct {
A []int
}
a := Bar{A: []int{1}}
b := Bar{A: []int{1}}
println(a == b)
// Output: invalid operation: a == b (struct containing []int cannot be compared)
只要底层类型“简单”且相同,就可以使用==运算符比较两个接口。否则,否则代码会在运行时出现恐慌:
var a interface{}
var b interface{}
a = 10
b = 10
println(a == b)
// Output: true
a = []int{1}
b = []int{2}
println(a == b)
// Output: panic: runtime error: comparing uncomparable type []int
包含映射、切片(但不包含函数)的结构和接口,都可以使用()函数进行比较:
var a interface{}
var b interface{}
a = []int{1}
b = []int{1}
println(reflect.DeepEqual(a, b))
// Output: true
a = map[string]string{"A": "B"}
b = map[string]string{"A": "B"}
println(reflect.DeepEqual(a, b))
// Output: true
temp := func() {}
a = temp
b = temp
println(reflect.DeepEqual(a, b))
// Output: false
为了比较字节片,在bytes包中有很好的助手函数:()、()和()。后者用于比较忽略大小写的文本字符串,这比()快得多。
为什么两个nil值有时候会不相等?
一个接口值可以看作是一个包裹非接口值的盒子。被包裹在一个接口值中的非接口值的类型必须实现了此接口值的类型。 在Go中,很多种类型的类型的零值都是用nil来表示的。 一个什么都没包裹的接口值为一个零值接口值,即nil接口值。 一个包裹着其它非接口类型的nil值的接口值并非什么都没包裹,所以它不是(或者说它不等于)一个nil接口值。
当对一个nil接口值和一个nil非接口值进行比较时(假设它们可以比较),此nil非接口值将先被转换为nil接口值的类型,然后再进行比较; 此转换的结果为一个包裹了此nil非接口值的一个副本的接口值,此接口值不是(或者说它不等于)一个nil接口值,所以此比较不相等。
Go中切片,map,struct 在64位机器中占用字节是多少?
在64位系统下,Golang的切片占用字节是24位,map和struct都是8位.
简述go语言中make和new的区别。
- new 它只接受一个参数,这个参数是一个类型,分配好内存后,返回一个指向该类型内存地址的指针。同时请注意它同时把分配的内存置为零,也就是类型的零值。
- make也是用于内存分配的,但是和new不同,它只用于chan、map以及切片的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
注意,因为这三种类型是引用类型,所以必须得初始化,但是不是置为零值,这个和new是不一样的。
switch流程控制代码块中的case表达式能重复吗?
switch流程控制代码块中的数字常量case表达式不能重复,但是布尔常量case表达式可以重复。
简述go中的main和init函数的区别。
Go里面有两个保留的函数:init函数和main函数。下边就来比较一下两个函数的异同。
- 相同点:两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
- 不同点:init可以应用于任意包中,且可以重复定义多个。main函数只能用于main包中,且只能定义一个。
两个函数的执行顺序:
- 对同一个go文件的init()调用顺序是从上到下的
- 对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数
- 对不同的package,如果不相互依赖的话,按照main包中"先import的后调用"的顺序调用其包中的init()
- 如果package存在依赖,则先调用最早被依赖的package中的init()
- 最后调用main函数
简述go中的defer
defer函数属延迟执行,延迟到调用者函数执行 return 命令前被执行。多个defer之间按LIFO先进后出顺序执行。
- 若函数中有多个 defer,其执行顺序为 先进后出,可以理解为栈。
- return 会做几件事:
- 给返回值赋值
- 调用 defer 表达式
- 返回给调用函数
- 若 defer 表达式有返回值,将会被丢弃。
在实际开发中,defer 的使用经常伴随着闭包与匿名函数的使用。小心踩坑:
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
defer func() {
fmt.Println(i)
}()
}
}
Output:
5
5
5
5
5
解释一下,defer 表达式中的 i 是对 for 循环中 i 的引用。到最后,i 加到 5,故最后全部打印 5。
如果将 i 作为参数传入 defer 表达式中,在传入最初就会进行求值保存,只是没有执行延迟函数而已。
for i := 0; i < 5; i++ {
defer func(idx int) {
fmt.Println(idx)
}(i) // 传入的 i,会立即被求值保存为 idx
}
Go中的defer函数使用下面的两种情况下结果是多少,为什么?
a := 1
defer fmt.Println("the value of a1:",a)
a++
defer func() {
fmt.Println("the value of a2:",a)
}()
运行:
the value of a1: 1
the value of a1: 2
第一种情况:
defer fmt.Println("the value of a1:",a)
defer延迟函数调用的(a)函数的参数值在defer语句出现时就已经确定了,所以无论后面如何修改a变量都不会影响延迟函数。
第二种情况:
defer func() {
fmt.Println("the value of a2:",a)
}()
defer延迟函数调用的函数参数的值在defer定义时候就确定了,而defer延迟函数内部所使用的值需要在这个函数运行时候才确定。
每个测试文件必须以什么结尾?每个测试文件必须导入什么包?功能测试函数必须以什么为前缀? 要执行压力测试需要带上什么参数?在压力测试用例中,要在循环体内使用什么?以使测试可以正常的运行。
- 测试文件必须以“_test.go”结尾
- 必须import testing这个包
- 所有的测试用例函数必须是Test开头
- 测试函数TestXxx()的参数是 ,执行压力测试需要带上参数-bench
- 压力测试循环体内要使用
无缓冲和缓冲通道之间有什么区别?
无缓冲的channel是同步的,而有缓冲的channel是非同步。
无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送 goroutine 和接收 goroutine 同时准备好,才可以完成发送和接收操作。
- channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。
- channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
select可以用于什么?
Go的select主要是处理多个channel的操作.
-
为请求设置超时时间
在 golang 1.7 之前, http 包并没有引入 context 支持,通过 向一个坏掉的服务发送请求会导致响应缓慢。类似的场景下,我们可以使用 select 控制服务响应时间。 -
完成 channel
它可以用于保证流水线上每个阶段goroutine 的退出。 -
退出 channel
在很多场景下,quit channel 和 done channel 是一个概念。在并发程序中,通常 main routine 将任务分给其它 go routine 去完成,而自身只是起到调度作用。这种情况下,main 函数无法知道 其它goroutine 任务是否完成,此时我们需要 quit channel;
selcet是怎么执行的?
- select中的case语句必须是一个channel操作
- select中的default子句总是可运行的,速度非常快。
- 如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行。
- 如果没有可运行的case语句,且有default语句,那么就会执行default的动作。
- 如果没有可运行的case语句,且没有default语句,select将阻塞,直到某个case通信可以运行
JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗?
首先JSON 标准库对 nil slice 和 空 slice 的处理是不一致.
通常错误的用法,会报数组越界的错误,因为只是声明了slice,却没有给实例化的对象。
var slice []int
slice[1] = 0
此时slice的值是nil,这种情况可以用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。
empty slice 是指slice不为nil,但是slice没有值,slice的底层的空间是空的,此时的定义如下:
slice := make([]int,0)
slice := []int{}
当我们查询或者处理一个空的列表的时候,这非常有用,它会告诉我们返回的是一个列表,但是列表内没有任何值。
总之,nil slice 和 empty slice是不同的东西,需要我们加以区分的.
并发编程概念是什么?
并行是指两个或者多个事件在同一时刻发生;并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群
并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。而并行是真正意义上的“同时执行”。
并发编程是指在一台处理器上“同时”处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
简述协程,线程,进程的区别。
-
进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。 -
线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。 -
协程
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。
协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。
如何复制silce、map、interface{} ?
您可以使用内置copy()函数复制切片:
a := []int{1, 2}
b := []int{3, 4}
check := a
copy(a, b)
fmt.Println(a, b, check)
// Output: [3 4] [3 4] [3 4]
这里,check变量用于保存对原始切片描述的引用,以确保它真正被复制。
另一方面,在下一个示例中,操作不复制切片内容,只复制切片描述:
a := []int{1, 2}
b := []int{3, 4}
check := a
a = b
fmt.Println(a, b, check)
// Output: [3 4] [3 4] [1 2]
您可以通过遍历其键来复制Map。是的,不幸的是,这是在Go中复制Map的最简单方法:
a := map[string]bool{"A": true, "B": true}
b := make(map[string]bool)
for key, value := range a {
b[key] = value
}
以下示例仅复制map的描述:
a := map[string]bool{"A": true, "B": true}
b := map[string]bool{"C": true, "D": true}
check := a
a = b
fmt.Println(a, b, check)
// Output: map[C:true D:true] map[C:true D:true] map[A:true B:true]
Go中没有内置方式来复制接口。不,该()功能不存在。
Golang的内存模型,为什么小对象多了会造成gc压力。
通常小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配.
数据竞争(Data Race)问题怎么解决?能不能不加锁解决这个问题?
同步访问共享数据是处理数据竞争的一种有效的方法.golang在1.1之后引入了竞争检测机制,可以使用 go run -race 或者 go build -race来进行静态检测。
其在内部的实现是,开启多个协程执行同一个命令, 并且记录下每个变量的状态.
竞争检测器基于C/C++的ThreadSanitizer 运行时库,该库在Google内部代码基地和Chromium找到许多错误。这个技术在2012年九月集成到Go中,从那时开始,它已经在标准库中检测到42个竞争条件。现在,它已经是我们持续构建过程的一部分,当竞争条件出现时,它会继续捕捉到这些错误。
竞争检测器已经完全集成到Go工具链中,仅仅添加-race标志到命令行就使用了检测器。
$ go test -race mypkg // 测试包
$ go run -race mysrc.go // 编译和运行程序
$ go build -race mycmd // 构建程序
$ go install -race mypkg // 安装程序
要想解决数据竞争的问题可以使用互斥锁,解决数据竞争(Data race),也可以使用管道解决,使用管道的效率要比互斥锁高.
什么是channel,为什么它可以做到线程安全?
Channel是Go中的一个核心类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication),Channel也可以理解是一个先进先出的队列,通过管道进行通信。
Golang的Channel,发送一个数据到Channel 和 从Channel接收一个数据 都是 原子性的。而且Go的设计思想就是:不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。也就是说,设计Channel的主要目的就是在多任务间传递数据的,这当然是安全的。
赋值是原子操作吗?
对于标准编译器来说,赋值不是原子操作。
函数调用(d)和数据通道接收<-(d)操作之间有何区别?
两者都会将当前的goroutine执行暂停一段时间。 区别在于(d)函数调用将使当前的协程进入睡眠字状态,但是当前协程的(主)状态依然为运行状态; 而数据通道接收<-(d)操作将使当前协程进入阻塞状态
64位整数值的地址是否能保证总是64位对齐的,以便可以被安全地原子访问?
传递给sync/atomic标准库包中的64位函数的地址必须是64位对齐的,否则调用这些函数将在运行时导致恐慌产生。对于标准编译器和gccgo编译器,在64位架构下,64位整数的地址将保证总是64位对齐的。 所以它们总是可以被安全地原子访问。 但在32位架构下,64位整数的地址仅保证是32位对齐的。 所以原子访问某些64位整数可能会导致恐慌。
分布式锁实现原理,用过吗?
在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:
- 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
- 高可用的获取锁与释放锁;
- 高性能的获取锁与释放锁;
- 具备可重入特性;
- 具备锁失效机制,防止死锁;
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
通常分布式锁以单独的服务方式实现,目前比较常用的分布式锁实现有三种:
- 基于数据库实现分布式锁。
- 基于缓存(redis,memcached,tair)实现分布式锁。
- 基于Zookeeper实现分布式锁。
尽管有这三种方案,但是不同的业务也要根据自己的情况进行选型,他们之间没有最好只有更适合!
- 基于数据库的实现方式
基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
创建一个表:
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
`desc` varchar(255) NOT NULL COMMENT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
想要执行某个方法,就使用这个方法名向表中插入数据:
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName');
因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
成功插入则获取锁,执行完成后删除对应的行数据释放锁:
delete from method_lock where method_name ='methodName';
注意:这里只是使用基于数据库的一种方法,使用数据库实现分布式锁还有很多其他的用法可以实现!
使用基于数据库的这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决及优化:
1、因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;
2、不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;
3、没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;
4、不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。
5、在实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。
- 基于Redis的实现方式
选用Redis实现分布式锁原因:
- Redis有很高的性能;
- Redis命令对此支持较好,实现起来比较方便
主要实现方式:
- SET lock currentTime+expireTime EX 600 NX,使用set设置lock值,并设置过期时间为600秒,如果成功,则获取锁;
- 获取锁后,如果该节点掉线,则到过期时间ock值自动失效;
- 释放锁时,使用del删除lock键值;
使用redis单机来做分布式锁服务,可能会出现单点问题,导致服务可用性差,因此在服务稳定性要求高的场合,官方建议使用redis集群(例如5台,成功请求锁超过3台就认为获取锁),来实现redis分布式锁。详见RedLock。
优点:性能高,redis可持久化,也能保证数据不易丢失,redis集群方式提高稳定性。
缺点:使用redis主从切换时可能丢失部分数据。
- 基于ZooKeeper的实现方式
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
- 创建一个目录mylock;
- 线程A想获取锁就在mylock目录下创建临时顺序节点;
- 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
- 线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
- 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。
上面的三种实现方式,没有在所有场合都是完美的,所以,应根据不同的应用场景选择最适合的实现方式。
在分布式环境中,对资源进行上锁有时候是很重要的,比如抢购某一资源,这时候使用分布式锁就可以很好地控制资源。
读写锁或者互斥锁读的时候能写吗?
Go中读写锁包括读锁和写锁,多个读线程可以同时访问共享数据;写线程必须等待所有读线程都释放锁以后,才能取得锁;同样的,读线程必须等待写线程释放锁后,才能取得锁,也就是说读写锁要确保的是如下互斥关系,可以同时读,但是读-写,写-写都是互斥的。
Channel是同步的还是异步的.
Channel是异步进行的。
channel存在3种状态:
- nil,未初始化的状态,只进行了声明,或者手动赋值为nil
- active,正常的channel,可读或者可写
- closed,已关闭,千万不要误认为关闭channel后,channel的值是nil
说一下异步和非阻塞的区别?
- 异步和非阻塞的区别:
- 异步:调用在发出之后,这个调用就直接返回,不管有无结果;异步是过程。
- 非阻塞:关注的是程序在等待调用结果(消息,返回值)时的状态,指在不能立刻得到结果之前,该调用不会阻塞当前线程。
- 同步和异步的区别:
- 步:一个服务的完成需要依赖其他服务时,只有等待被依赖的服务完成后,才算完成,这是一种可靠的服务序列。要么成功都成功,失败都失败,服务的状态可以保持一致。
- 异步:一个服务的完成需要依赖其他服务时,只通知其他依赖服务开始执行,而不需要等待被依赖的服务完成,此时该服务就算完成了。被依赖的服务是否最终完成无法确定,一次它是一个不可靠的服务序列。
- 消息通知中的同步和异步:
- 同步:当一个同步调用发出后,调用者要一直等待返回消息(或者调用结果)通知后,才能进行后续的执行。
- 异步:当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果)。在调用结束之后,通过消息回调来通知调用者是否调用成功。
- 阻塞与非阻塞的区别:
- 阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务,函数只有在得到结果之后才会返回。
- 非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
同步与异步是对应的,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的。
阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞。
阻塞是使用同步机制的结果,非阻塞则是使用异步机制的结果。
Goroutine和线程的区别?
从调度上看,goroutine的调度开销远远小于线程调度开销。
OS的线程由OS内核调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用一个调度器内核函数。这个函数暂停当前正在运行的线程,把他的寄存器信息保存到内存中,查看线程列表并决定接下来运行哪一个线程,再从内存中恢复线程的注册表信息,最后继续执行选中的线程。这种线程切换需要一个完整的上下文切换:即保存一个线程的状态到内存,再恢复另外一个线程的状态,最后更新调度器的数据结构。某种意义上,这种操作还是很慢的。
Go运行的时候包涵一个自己的调度器,这个调度器使用一个称为一个M:N调度技术,m个goroutine到n个os线程(可以用GOMAXPROCS来控制n的数量),Go的调度器不是由硬件时钟来定期触发的,而是由特定的go语言结构来触发的,他不需要切换到内核语境,所以调度一个goroutine比调度一个线程的成本低很多。
从栈空间上,goroutine的栈空间更加动态灵活。
每个OS的线程都有一个固定大小的栈内存,通常是2MB,栈内存用于保存在其他函数调用期间哪些正在执行或者临时暂停的函数的局部变量。这个固定的栈大小,如果对于goroutine来说,可能是一种巨大的浪费。作为对比goroutine在生命周期开始只有一个很小的栈,典型情况是2KB, 在go程序中,一次创建十万左右的goroutine也不罕见(2KB*100,000=200MB)。而且goroutine的栈不是固定大小,它可以按需增大和缩小,最大限制可以到1GB。
goroutine没有一个特定的标识。
在大部分支持多线程的操作系统和编程语言中,线程有一个独特的标识,通常是一个整数或者指针,这个特性可以让我们构建一个线程的局部存储,本质是一个全局的map,以线程的标识作为键,这样每个线程可以独立使用这个map存储和获取值,不受其他线程干扰。
goroutine中没有可供程序员访问的标识,原因是一种纯函数的理念,不希望滥用线程局部存储导致一个不健康的超距作用,即函数的行为不仅取决于它的参数,还取决于运行它的线程标识。
如何实现消息队列(多生产者,多消费者)?
根据Goroutine和channel的读写可以实现消息队列 。
package main
import (
"fmt"
"time"
)
func consumer(cname string, ch chan int) {
//可以循环 for i := range ch 来不断从 channel 接收值,直到它被关闭。
for i := range ch {
fmt.Println("consumer-----------", cname, ":", i)
}
fmt.Println("ch closed.")
}
func producer(pname string, ch chan int) {
for i := 0; i < 4; i++ {
fmt.Println("producer--", pname, ":", i)
ch <- i
}
}
func main() {
//用channel来传递"产品", 不再需要自己去加锁维护一个全局的阻塞队列
ch := make(chan int)
go producer("生产者1", ch)
go producer("生产者2", ch)
go consumer("消费者1", ch)
go consumer("消费者2", ch)
time.Sleep(10 * time.Second)
close(ch)
time.Sleep(10 * time.Second)
}
如何进行大文件排序?
设想你有一个20GB的文件,每行一个字符串,说明如何对这个文件进行排序。
内存肯定没有20GB大,所以不可能采用传统排序法。但是可以将文件分成许多块,针对每个快各自进行排序,存回文件系统。然后将这些块逐一合并,最终得到全部排好序的文件。
孤儿进程,僵尸进程
-
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
-
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。