Golang常见问题
Golang常见问题
简答
make和new的区别
- make:分配内存并初始化对象,只能用与三种引用对象(slice、map、channel);
- new:只分配内存并返回对象指针,对象的底层空间置为0;
数组和切片slice的区别
for i, v := range()这种方式遍历切片底层实现(但1.22版本,v地址会变化,不再赋值同一个元素)- 遍历前取size,遍历时size不变。(所以增删元素时,会存在问题,越界or遍历不完)
- i是下标,v是一个单独的变量,循环时,每次将对应下标的元素赋值给v;
- v始终是一个变量,v的地址不变;
struct能不能用==比较
- 不同类型的不能比较
- slice、map、func无法比较
- 两struct,任一个,含slice、map、func的struct则无法比较
不能用==比较:可以用reflect.DeepEqual比较
空结构体
struct{}{}占用空间吗:不占用,可以用unsafe.Sizeof(struct{}{}算出为0_的作用- import时:
import _ "context",只执行包下的init函数,但不调用包。 - 返回值
_:忽略返回值
- import时:
包中init初始化:
- 包内可以有多个init函数,文件内也可以,执行顺序按照文件名逐个初始化
- 不管包导入多少次,init只调用一次
- 包内变量先初始化,再执行init
- 整个程序整体顺序为:import -> const -> var -> init() -> main()
context
类型
context有四种类型,它本身其实是一个接口
- emptyCtx:实现接口,但操作都是返回空值
- cancelCtex:实现context和canceler接口,会通知children节点取消
- timerCtx:内部封装了cancelCtx,同时也有deadline用于终止cancelCtx。当然也可以提前调用来终止。
- valueCtx:能存储{k,v}对,会继承父ctx的{k, v}
应用场景
- trace_id:做链路追踪,不侵入业务
- 流程控制:取消控制、超时控制
channel
异常状态操作
对于已经关闭的channel,写和再次close,都会panic;读可以正常读,读完则返回对于零值,并附赠标识
对于nil的channle,关闭会直接panic,读和写都会挂起,如果是main,会直接fatal error
select交互
selecct分为阻塞(不含default)和和非阻塞(含default)
阻塞的,需要等到某一路有数据时唤醒
非阻塞,在有数据的路中随机选一路(不含default),如果都没有数据,则到default。
Map
- 非并发安全:map不是并发安全的,并发读写的话,会fatal error且无法被recover
- map遍历无序:由于存在扩容,故扩容前后数据位置可能发生变更,为了不让用户依赖这个点,设计成随机遍历,不让用户迷惑。
nil map和空map有什么区别
- 对于nil map,写会panic,读、删除则不会。
- 空map,操作都正常
defer
执行顺序
defer func():先压后执行,底层是链栈
defer的函数参数
defer压栈时,参数已经像正常函数一样压栈。
func deferCopy() {
num := 1
defer fmt.Printf("num is %d", num) // 这里会输出1,因为参数已经拷贝到栈里头。
num = 2
return
}但如果是引用类型,由于拷贝的是地址,则能够看到数据的变化。
func printArr(arr *[2]int) {
for i := range arr {
fmt.Printf("%d ", arr[i])
}
fmt.Println()
}
func deferRef() {
arr := [2]int{1, 2}
defer printArr(&arr) // 输出 99 2
arr[0] = 99
return
}defer与返回值
返回值修改,整个过程分为3步
- 设置返回值,明确会返回哪个值(已经声明则是这个,未声明的隐式创建)
- 执行所有defer语句
- 返回步骤1的结果
所以,确定完结果后,如果defer时修改,最终结果是能看到变化的。
func deferReturnVal() (res int) { // (res int) 这里声明返回值名字
res = 1
defer func() {
res++
}()
return // 到这里,先明确返回res,执行defer,res+1,返回res,也就是返回2.
// return res; 这种方式也是一样的效果
}但注意,这种未提前声明返回值的,是不会的
实际的过程是,对于未声明的变量,会隐式声明对于的返回值,再执行上面说到的3个步骤。
func deferReturnVal() int { // int 这里未声明返回值的具体名字
num := 1
defer func() {
num++
}()
return num // 返回1;
// 这里存在一个隐式的res
// 第一步,先res = num
// 第二步,执行defer,num变为2
// 第三步,返回res,此时res为1
}defer recover捕获
- 只能捕获当前goroutine,子goroutine无法捕获(拦截器处的recover,注意这个点,子goroutine无法捕获)
- 一个recover只能捕获一次panic,且一一对应
channel
map
面试代码题
多线程并发打印A、B、C
package main
import (
_ "context"
"fmt"
"time"
)
var chAStart = make(chan struct{})
var chBStart = make(chan struct{})
var chCStart = make(chan struct{})
func printA() {
for {
<-chAStart
fmt.Println("A")
time.Sleep(time.Second)
chBStart <- struct{}{}
}
}
func printB() {
for {
<-chBStart
fmt.Println("B")
time.Sleep(time.Second)
chCStart <- struct{}{}
}
}
func printC() {
for {
<-chCStart
fmt.Println("C")
time.Sleep(time.Second)
chCStart <- struct{}{}
}
}
func main() {
go printA()
go printB()
go printC()
chAStart <- struct{}{}
time.Sleep(time.Second * 10)
}常见坑
类型断言时,尽量采用ok式
从interface{}转成对应类型时,如果类型类型错误切未采用ok式,会Panic
package main
import (
"fmt"
)
// Person 定义一个示例结构体
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address string `json:"address"`
}
type FakePerson struct {
Name string `json:"name"`
Age int `json:"age"`
Address string `json:"address"`
}
func main() {
// 创建一个 Person 对象
originalPerson := Person{Name: "Alice", Age: 30, Address: "123 Wonderland Ave"}
dataStore := make(map[int]interface{}) // 创建一个 map[int]interface{}
dataStore[1] = originalPerson // 存入变成interface{}
retrievedData := dataStore[1].(FakePerson) // 运行时Panic
if false { // 加这一行是为了假装用一下retrievedData,免于编译器报错
fmt.Println(retrievedData)
}
// 推荐这种方式,不会panic
//retrievedData, ok := dataStore[1].(FakePerson)
//if !ok {
// fmt.Println("类型断言错误")
//} else {
// fmt.Println(retrievedData)
//}
}类型断言时,对象值和对象指针是两种不同类型
package main
import (
"fmt"
)
// Person 定义一个示例结构体
type Person struct {
Name string
Age int
Address string
}
func main() {
// 创建一个 Person 对象
p := &Person{Name: "Alice", Age: 30, Address: "123 Wonderland Ave"}
var i interface{} = p // 使用Person指针初始化 interface{}
// 断言并输出
if person, ok := i.(Person); ok {
fmt.Printf("Person Value: %+v\n", person)
} else {
fmt.Println("断言失败")
}
}只能是存入什么类型就是什么类型,且区分对象还是指针。
结合上一个类型错误会Panic的点,存入对象,取出来断言成指针,就很容易Panic。
不可导出变量用json打印时,无法被打印出来
常见疑惑
类的操作函数中,指针参数和结构体参数有什么区别
如果是指针实现,操作的就是原来的对象。
package main
import "fmt"
type Jiahao struct {
Money int
}
func (jh *Jiahao) AddMoney() int {
jh.Money += 1
return jh.Money
}
func main() {
originJH := Jiahao{Money: 0}
fmt.Println("Jiahao 当前的钱为:", originJH.Money) // 输出 0
originJH.AddMoney()
fmt.Println("Jiahao 执行 “func (originJH *Jiahao) AddMoney() int” 后的钱:", originJH.Money) // 输出 1
}如果是结构体实现,操作的是新拷贝的对象,并不会影响原来的对象。
package main
import "fmt"
type Jiahao struct {
Money int
}
func (jh Jiahao) AddMoney() int {
jh.Money += 1
return jh.Money
}
func main() {
originJH := Jiahao{Money: 0}
fmt.Println("Jiahao 当前的钱为:", originJH.Money) // 输出 0
newJHMoney := originJH.AddMoney()
fmt.Println("新Jiahao对象执行了AddMoney,而非OriginJh,newJHMoney为", newJHMoney) // 输出 1
fmt.Println("originJH 执行 “func (jh Jiahao) AddMoney() int” 后的钱,没有变化,还是:", originJH.Money) // 输出 0
}所以,指针实现操作原对象,而结构体实现是新拷贝一个对象并进行操作。
所以如果结构体较大,考虑用指针,如果只是为了获取一下临时的计算结果,可以用结构体。
接口实现中,指针实现和结构体实现有什么区别
这个问题类似上一个“类的操作函数中,指针参数和结构体参数有什么区别”问题,其结论“指针实现操作原对象,而结构体实现是新拷贝一个对象并进行操作。”还是适用的,但略微有区别的是多态的处理。
面向对象中多态的用法都是接口类型(或者父类对象)指向子类对象,如下面这个例子
package main
import "fmt"
type IJiahao interface {
MakeMoney() error
}
type Jiahao struct {
}
func (jh *Jiahao) MakeMoney() error {
fmt.Printf("Jiahao真的在挣钱中...\n")
return nil
}
func main() {
var ijh IJiahao = &Jiahao{}
ijh.MakeMoney()
}在golang中当然也没有问题,但针对
函数实现时,使用指针、使用结构体实现
初始化对象时,使用指针、使用结构体实现
- ```golang
var ijh IJiahao = &Jiahao{} // 使用指针初始化对象并赋值给接口类型
var jh IJiahao = Jiahao{} // 使用结构体初始化对象并赋值给接口类型这两个维度,可以组合出4种情况,这四种情况并不都能通过编译器的检查: | | 结构体实现函数 | 结构体指针实现函数 | | :------------------- | :------------- | ------------------ | | 结构体初始化变量 | 通过 | 不通过 | | 结构体指针初始化变量 | 通过 | 通过 | 都是结构体和都是指针的情况好理解,结构体是拷贝一个对象后操作这个新对象,指针是直接在原对象上操作。 而结构体实现函数,指针初始化的情况,其实也是拷贝一个新对象后处理。 重点看指针实现函数,结构体初始化的情况,如下例 ```golang package main import "fmt" type IJiahao interface { MakeMoney() error } type Jiahao struct { Money int } func (jh *Jiahao) MakeMoney() error { jh.Money += 1 fmt.Printf("Jiahao真的在挣钱中...\n") return nil } func main() { var ijh IJiahao = Jiahao{} // 注意!!!这里报错 ijh.MakeMoney() }
- ```golang
我们知道golang中函数传参是值拷贝,哪怕是传的指针,其实也发生了值拷贝,只不过拷贝的是指针,两个指针指向同一个对象。对象的操作函数,本质也是函数第一个参数多了一个对象地址。
所以var ijh IJiahao = Jiahao{}这种情况,会拷贝出一个新的Jiahao,与定义的只允许指针类型的情况func (jh *Jiahao) MakeMoney() error {冲突,所以编译报错。
当一个Interface含有多个抽象函数时,每一个抽象函数都需要符合这个规则。
继承中,指针继承和结构体继承有什么区别
实例不转换为接口的情况下无区别
总结:不转换为接口时(字段继承和字段指针继承)无区别,在代码中1,2,3,4处(即a,b,pa,pb)都可以正常调用父类的函数
package main
import "fmt"
type iter interface {
run()
sleep()
}
type base struct{}
func (p *base) run() {
fmt.Println("Base::run()")
}
func (p base) sleep() {
fmt.Println("Base::sleep()")
}
type subA struct {
base // 字段继承
}
type subB struct {
*base // 字段指针继承
}
/*
总结:
不转换为接口时(字段继承和字段指针继承)无区别,就是正常类型实例使用
在1,2,3,4处(即a,b,pa,pb)都可以正常调用父类的函数
*/
func main() {
// 实例[字段继承]
a := subA{base{}}
a.run() // 1
a.sleep()
// 实例[字段指针继承]
b := subB{&base{}}
b.run() // 2
b.sleep()
// 指针实例[字段继承]
pa := &subA{base{}}
pa.run() // 3
pa.sleep()
// 指针实例[字段指针继承]
pb := &subB{&base{}}
pb.run() // 4
pb.sleep()
}实例转换接口时有区别
总结:转换为接口时(字段继承和字段指针继承)有区别,在B1,C1,D1都可以正常运行,在A1处出现错误,即subA实例[字段继承]未实现接口 run()方法
package main
import "fmt"
type iter interface {
run()
sleep()
}
type base struct {
}
func (p *base) run() {
fmt.Println("Base::run()")
}
func (p base) sleep() {
fmt.Println("Base::sleep()")
}
type subA struct {
base
}
// ---------HERE------------
//func (p subA) run() {
// fmt.Println("subA::run()")
//}
type subB struct {
*base
}
/*
*
总结:
转换为接口时(字段继承和字段指针继承)有区别,
在B1,C1,D1都可以正常运行
在A1处出现错误,即subA实例[字段继承]未实现接口run()方法
*/
func main() {
// ======实例转换为接口=============
var i iter
// a实例[字段继承]
a := subA{base{}}
i = a // A1 error!!! : subA未实现接口,父类仅仅实现了sleep()方法,run()没有实现
i.run() // A1
i.sleep()
// 如果需要将a实例转化为接口,必须实现接口
// Base结构体已经实现了接收者为实例的sleep()方法
// 那么可以在subA结构体实现 接收者为实例接受的run()方法即可---位于HERE处
// b实例[字段指针继承]
b := subB{&base{}}
i = b // subB实现了接口
i.run() // B1
i.sleep()
// =======指针实例转换为接口==========
// 指针实例[字段继承]
pa := &subA{base{}}
i = pa // subA实现了接口
i.run() // C1
i.sleep()
// 指针实例[字段指针继承]
pb := &subB{&base{}}
i = pb // subB实现接口
i.run() // D1
i.sleep()
}总的来说,这并非严格意义上的继承,而是嵌入,值嵌入和指针嵌入。指针嵌入意味着,多个子对象可以共享同一个父对象。
interface = nil和指针的nil有什么区别
在 Go 中,一个接口值包括两部分:一个是动态类型,一个是动态值。当一个接口被赋值为 nil 时,这意味着它的动态类型和动态值都为空。此时,接口被认为是 nil。
而普通的指针,只需要赋值为nil后就被认为是nil。
但当将一个指针变量(它指向了nil)赋值给interface{},interface就不是nil了。例子如下
package main
import "fmt"
func main() {
var i interface{} = nil
var p *int = nil
if i == nil {
fmt.Println("The interface is nil") // 输出这个
} else {
fmt.Println("The interface is not nil")
}
if p == nil {
fmt.Println("The pointer is nil") // 输出这个
} else {
fmt.Println("The pointer is not nil")
}
i = p // 将 nil 指针赋值给接口,之后,p不再是nil
if i == nil {
fmt.Println("The interface is nil after assigning a nil pointer")
} else {
fmt.Println("The interface is not nil after assigning a nil pointer") // 输出这个
}
if i == p {
fmt.Println("i == p") // 输出这个
} else {
fmt.Println("i != p")
}
}i == nil为true这里,虽然初看有些惊讶,但其实很好理解,这interface{}存着这个指针的动态类型和动态值(动态类型为*int,动态值即nil),只要它有一个动态类型信息,就不会等于 nil。所以理所应当的不是nil。
i == p为true这里,在比较 interface{} 和具体的指针(即使该指针为 nil)时,会比较它们的动态类型和动态值是否相同,他们的动态类型且值是相同的,所以为true。
给interface{}赋值时,用指针和值的区别
其实就是值拷贝和指针拷贝的换皮问题,golang是值传递,interface{}也符合这个规律。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
personPtr := Person{Name: "Alice", Age: 30} // 创建一个 Person 对象
var i interface{} = &personPtr // 将指针赋值给 interface{}
personPtr.Name = "Bob" // 修改指针指向的值
// 断言并输出
if person, ok := i.(*Person); ok { // 指针,能感受到变化
fmt.Printf("Person (pointer): %+v\n", person)
}
personVal := Person{Name: "Alice", Age: 30}
var j interface{} = personVal
personVal.Name = "Bob"
// 断言并输出
if person, ok := j.(Person); ok { // 值,不能感受到变化,两者是不同的对象
fmt.Printf("Person (value): %+v\n", person)
}
}其他
打印时,各种参数
%v:输出结构体各成员的值%+v:输出结构体各成员的名称和值%#v:输出结构体名称,并 输出结构体各成员的名称和值
函数闭包:也就是匿名函数,函数内如果有外部参数,会捕获变量
多返回值:底层实现是通过在栈中提前预留空位实现
todo
interface怎么理解:原理、作为函数参数是怎么处理的
uintptr和unsafe.Pointer的区别
- 总共三种指针:普通指针类型、unsafe.Pointer、uintptr(本质不是指针)。
- 普通指针
- unsafe.Pointer
- uintptr
sync.Map
如何定位内存泄漏?
再看下golang面试题