Golang常见问题

Golang常见问题

简答

  1. make和new的区别

    1. make:分配内存并初始化对象,只能用与三种引用对象(slice、map、channel);
    2. new:只分配内存并返回对象指针,对象的底层空间置为0;
  2. 数组和切片slice的区别

  3. for i, v := range() 这种方式遍历切片底层实现(但1.22版本,v地址会变化,不再赋值同一个元素)

    1. 遍历前取size,遍历时size不变。(所以增删元素时,会存在问题,越界or遍历不完)
    2. i是下标,v是一个单独的变量,循环时,每次将对应下标的元素赋值给v;
    3. v始终是一个变量,v的地址不变;
  4. struct能不能用==比较

    • 不同类型的不能比较
    • slice、map、func无法比较
    • 两struct,任一个,含slice、map、func的struct则无法比较
  5. 不能用==比较:可以用reflect.DeepEqual比较

  6. 空结构体struct{}{}占用空间吗:不占用,可以用unsafe.Sizeof(struct{}{} 算出为0

  7. _的作用

    • import时: import _ "context" ,只执行包下的init函数,但不调用包。
    • 返回值_:忽略返回值
  8. 包中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}

应用场景

  1. trace_id:做链路追踪,不侵入业务
  2. 流程控制:取消控制、超时控制

channel

异常状态操作

对于已经关闭的channel,写和再次close,都会panic;读可以正常读,读完则返回对于零值,并附赠标识

对于nil的channle,关闭会直接panic,读和写都会挂起,如果是main,会直接fatal error

select交互

selecct分为阻塞(不含default)和和非阻塞(含default)

阻塞的,需要等到某一路有数据时唤醒

非阻塞,在有数据的路中随机选一路(不含default),如果都没有数据,则到default。

Map

  1. 非并发安全:map不是并发安全的,并发读写的话,会fatal error且无法被recover
  2. 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中函数传参是值拷贝,哪怕是传的指针,其实也发生了值拷贝,只不过拷贝的是指针,两个指针指向同一个对象。对象的操作函数,本质也是函数第一个参数多了一个对象地址。

所以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面试题

参考


Golang常见问题
https://messenger1th.github.io/2024/07/24/Golang/Golang常见问题/
作者
Epoch
发布于
2024年7月24日
许可协议