golang面试(day1)
本文最后更新于336 天前,其中的信息可能已经过时,如有错误请发送邮件到2647369456@qq.com

map怎么去做并发安全

在Go语言中,map是引用类型,它在并发环境下使用时需要特别注意。由于map不是线程安全的,所以在多个goroutine同时读写同一个map时,可能会导致数据竞争(race condition)和不一致的状态。为了确保map在并发环境下的安全使用,你可以采取以下几种策略:

  1. 使用互斥锁(Mutex)

最直接的方法是使用sync.Mutexsync.RWMutex来保护map的访问。sync.Mutex提供互斥锁,而sync.RWMutex提供读写锁,允许多个读操作同时进行,但写操作会独占锁。

import "sync"

var (
    mu    sync.RWMutex
    myMap = make(map[string]int)
)

func readMap(key string) int {
    mu.RLock()
    defer mu.RUnlock()
    return myMap[key]
}

func writeMap(key string, value int) {
    mu.Lock()
    defer mu.Unlock()
    myMap[key] = value
}
  1. 使用sync.Map

sync.Map是Go标准库提供的一个线程安全的map实现,它专为并发环境设计。sync.Map的读写操作都是原子的,因此不需要额外的锁。


import "sync"

var mySyncMap sync.Map

func readSyncMap(key interface{}) (interface{}, bool) {
    value, ok := mySyncMap.Load(key)
    return value, ok
}

func writeSyncMap(key, value interface{}) {
    mySyncMap.Store(key, value)
}
  1. 使用原子操作

对于简单的场景,如果map只在单个goroutine中写入,而在多个goroutine中读取,可以使用sync/atomic包提供的原子操作来保护读取操作。

import "sync/atomic"

var myMap map[string]int
var myMapCopy atomic.Value

func init() {
    myMap = make(map[string]int)
    myMapCopy.Store(myMap)
}

func readMap(key string) int {
    m := myMapCopy.Load().(map[string]int)
    return m[key]
}

func writeMap(key string, value int) {
    myMap[key] = value
    myMapCopy.Store(myMap)
}
  1. 使用通道(Channel)

在某些情况下,可以使用通道来传递map的更新,从而避免直接在多个goroutine中共享map

var updateChan = make(chan map[string]int)

func updateMap(key string, value int) {
    newMap := make(map[string]int)
    // 复制旧的map
    // 更新newMap
    updateChan <- newMap
}

func main() {
    go func() {
        for newMap := range updateChan {
            // 更新全局map
        }
    }()
}
package main

import "fmt"

type Command struct {
    Key    string
    Value  int
    Result chan<- int
}

var m = make(map[string]int)
var commands = make(chan Command)

func setup() {
    go func() {
        for cmd := range commands {
            if cmd.Result == nil {
                m[cmd.Key] = cmd.Value
            } else {
                cmd.Result <- m[cmd.Key]
            }
        }
    }()
}
func get(key string) int {
    result := make(chan int)
    commands <- Command{Key: key, Result: result}
    return <-result
}
func set(key string, value int) {
    commands <- Command{Key: key, Value: value}
}
func main() {
    setup()
    set("foo", 1)
    v := get("foo")
    fmt.Println(v)
}

外层的协程能捕获子协程的panic吗?

在Go语言中,外层的协程(goroutine)可以捕获其子协程(子goroutine)的panic。这通常通过在子协程中使用deferrecover来实现。recover函数可以停止panic过程,并返回panic传递的值。如果在defer函数中调用recover,则可以捕获并处理panic

package main

import (
    "fmt"
    "time"
)

func main() {
    // 创建一个子协程
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("子协程的panic被捕获:", r)
            }
        }()

        // 引发panic
        panic("发生错误")
    }()

    // 等待一段时间,确保子协程有机会执行
    time.Sleep(1 * time.Second)
}

panic都会被捕获吗?哪些panic不会捕获?

在Go语言中,panic通常是由运行时错误引起的,比如数组越界、空指针解引用等。大多数情况下,panic可以通过deferrecover来捕获和处理。然而,并非所有的panic都能被捕获,以下是一些不会被捕获的panic情况:

  1. 非Go语言引发的panic
    如果panic是由非Go语言代码(如C语言库)引发的,那么它可能不会被Go语言的recover捕获。
  2. Go运行时的panic
    Go运行时自身引发的panic,比如runtime.Goexit(),通常不会被recover捕获。
  3. 协程退出时的panic
    当一个协程执行完毕并正常退出时,如果它在退出前没有被deferrecover包围,那么它产生的panic不会被捕获。
  4. 协程被杀死时的panic
    如果一个协程被强制杀死(例如通过os.Exitruntime.Goexit),那么它产生的panic不会被捕获。
  5. 协程被其他协程杀死时的panic
    如果一个协程被另一个协程通过runtime.Goschedruntime.GOMAXPROCSruntime.GC等函数强制调度,那么它产生的panic不会被捕获。
  6. 协程被垃圾回收时的panic
    如果一个协程因为不再被任何变量引用而被垃圾回收,那么它产生的panic不会被捕获。
  7. 协程被操作系统杀死时的panic
    如果一个协程因为操作系统级别的错误(如内存不足)而被杀死,那么它产生的panic不会被捕获。
  8. 协程在select语句中被杀死时的panic
    如果一个协程在select语句中被杀死,那么它产生的panic不会被捕获。
  9. 协程在select语句中等待的通道被关闭时的panic
    如果一个协程在select语句中等待的通道被关闭,那么它产生的panic不会被捕获。
  10. 协程在select语句中等待的通道被关闭时的panic
    如果一个协程在select语句中等待的通道被关闭,那么它产生的panic不会被捕获。

slice和数组的区别?底层结构?

数组(Array)

  • 固定长度:数组的长度在声明时必须指定,并且在使用过程中不能改变。
  • 类型:数组是值类型,这意味着当你将一个数组赋值给另一个变量时,会创建该数组的一个副本。
  • 内存布局:数组在内存中是连续存储的,每个元素占用相同大小的空间。
  • 索引访问:通过索引直接访问数组元素,索引从0开始。
  • 使用场景:当你需要固定数量的元素,并且这些元素的类型和数量在编译时已知时,数组是一个好的选择。

切片(Slice)

  • 动态长度:切片是对数组的抽象,它提供了动态的长度和容量。切片的长度可以在运行时改变。
  • 引用类型:切片是引用类型,当你将一个切片赋值给另一个变量时,两个变量共享同一个底层数组。
  • 内存布局:切片在内存中包含三个部分:指向底层数组的指针、切片的长度和容量。
  • 索引访问:通过索引直接访问切片元素,索引从0开始。
  • 使用场景:当你需要一个可变长度的序列时,切片是一个更灵活的选择。

底层结构

  • 数组:数组在内存中是连续存储的,每个元素占用相同大小的空间。数组的类型包括元素类型和数组长度,例如[3]int表示一个包含3个整数的数组。
  • 切片:切片包含三个部分:
    • 指针:指向底层数组的指针。
    • 长度:切片当前的长度,表示切片中元素的数量。
    • 容量:切片的容量,表示从切片的第一个元素开始,底层数组中可以容纳的元素数量。
s = [指向底层数组的指针, 长度, 容量]

// 数组声明
var arr [3]int
// 切片声明
var s []int

slice扩容机制

Go1.18版本前

新申请的容量如果大于当前容量的两倍,会将新申请的容量直接作为新的容量,如果新申请的容量小于当前容量的两倍,会有一个阈值,即当前切片容量小于1024时,切片会将当前容量的2倍作为新申请的容量,如果大于等于1024,会将当前的容量的1.25倍作为新申请的容量。

源码片段

 newcap := old.cap
 doublecap := newcap + newcap
 if cap > doublecap {
  newcap = cap
 } else {
  if old.cap < 1024 {
   newcap = doublecap
  } else {
   // Check 0 < newcap to detect overflow
   // and prevent an infinite loop.
   for 0 < newcap && newcap < cap {
    newcap += newcap / 4
   }
   // Set newcap to the requested cap when
   // the newcap calculation overflowed.
   if newcap <= 0 {
    newcap = cap
   }
  }
 }

Go 1.18版本后

新申请的容量如果大于当前容量的两倍,会将新申请的容量直接作为新的容量,如果新申请的容量小于当前容量的两倍,会有一个阈值,即当前切片容量小于256时,切片会将当前容量的2倍作为新申请的容量,如果大于等于256,会将当前的容量的1.25倍+192作为新申请的容量,扩容的时候更加平滑,不会出现从2到1.25的突变。

newcap := old.cap
 doublecap := newcap + newcap
 if cap > doublecap {
  newcap = cap
 } else {
  const threshold = 256
  if old.cap < threshold {
   newcap = doublecap
  } else {
   // Check 0 < newcap to detect overflow
   // and prevent an infinite loop.
   for 0 < newcap && newcap < cap {
    // Transition from growing 2x for small slices
    // to growing 1.25x for large slices. This formula
    // gives a smooth-ish transition between the two.
    newcap += (newcap + 3*threshold) / 4
   }
   // Set newcap to the requested cap when
   // the newcap calculation overflowed.
   if newcap <= 0 {
    newcap = cap
   }
  }
 }

go哪些内置类型是并发安全的?

在Go语言中,内置类型本身并不保证并发安全。并发安全通常是指在多个goroutine(协程)中同时访问和修改数据时,数据的一致性和完整性得到保证。为了实现并发安全,Go提供了多种同步机制,如互斥锁(sync.Mutex)、读写锁(sync.RWMutex)、原子操作(sync/atomic包)等。

然而,Go标准库中确实有一些类型是设计为并发安全的,它们内部使用了适当的同步机制来保证并发访问时的安全性。以下是一些并发安全的内置类型或类型组合:

  1. sync.Map
    sync.Map是一个线程安全的map实现,它提供了并发读写的能力。它使用了特殊的内部机制来避免在读写操作时加锁,从而提高了并发性能。
  2. sync.WaitGroup
    sync.WaitGroup用于等待一组goroutine完成。它内部使用了原子操作来确保计数器的正确性,因此是并发安全的。
  3. sync.Once
    sync.Once用于确保某个函数只被执行一次,即使在多个goroutine中调用。它内部使用了原子操作来保证只执行一次的特性。
  4. sync.Cond
    sync.Cond是一个条件变量,它允许一个或多个goroutine等待,直到某个条件成立。它内部使用了互斥锁来保证条件变量的正确性。
  5. sync.Pool
    sync.Pool是一个临时对象池,它可以在多个goroutine之间共享和重用对象。它内部使用了原子操作和互斥锁来保证对象池的线程安全。
  6. context.Context
    context.Context用于传递请求范围内的值、截止时间、取消信号等。它内部使用了原子操作和通道来保证并发安全。
  7. sync/atomic包中的类型
    sync/atomic包提供了原子操作,这些操作可以用于实现并发安全的计数器、标志等。原子操作是通过底层硬件指令实现的,因此是并发安全的。
  8. channel
    虽然channel不是内置类型,但它是Go语言中用于goroutine间通信的核心机制。通过channel传递数据是线程安全的,因为channel操作(发送、接收、关闭)是原子的。

需要注意的是,即使这些类型是并发安全的,但在使用它们时仍然需要遵循Go语言的并发编程原则,比如避免死锁、确保资源的正确释放等。此外,对于自定义类型,如果需要并发安全,通常需要使用互斥锁、读写锁、原子操作等同步机制来实现。

go的结构体可以嵌套组合吗?

结构体嵌套

你可以将一个结构体作为另一个结构体的字段,这种嵌套的结构体字段称为匿名字段。匿名字段没有字段名,只有类型。在访问嵌套结构体的字段或方法时,可以直接使用类型名作为字段名。

type Address struct {
    City, Province string
}

type Person struct {
    Name    string
    Age     int
    Address Address // 匿名字段
}

func main() {
    person := Person{
        Name: "张三",
        Age:  30,
        Address: Address{
            City:     "北京",
            Province: "北京",
        },
    }

    fmt.Println(person.Name, person.Age, person.City, person.Province)
}

结构体组合

结构体组合是指将一个结构体嵌入到另一个结构体中,同时可以为嵌入的结构体字段指定新的字段名。这允许你重用已有的结构体类型,同时为嵌入的结构体字段提供额外的上下文或修改其行为。

type Employee struct {
    Person // 嵌入Person结构体
    ID     int
    Title  string
}

func main() {
    employee := Employee{
        Person: Person{
            Name: "李四",
            Age:  28,
            Address: Address{
                City:     "上海",
                Province: "上海",
            },
        },
        ID:    1001,
        Title: "软件工程师",
    }

    fmt.Println(employee.Name, employee.Age, employee.City, employee.Province, employee.ID, employee.Title)
}

方法继承

当嵌入一个结构体时,嵌入的结构体的方法也会被“继承”。这意味着你可以直接在外部结构体上调用嵌入结构体的方法,就像它们是外部结构体自己的方法一样。

func (p *Person) Greet() {
    fmt.Println("Hello, my name is", p.Name)
}

func main() {
    employee := Employee{
        Person: Person{
            Name: "王五",
            Age:  35,
            Address: Address{
                City:     "广州",
                Province: "广东",
            },
        },
        ID:    1002,
        Title: "产品经理",
    }

    employee.Greet() // 直接调用Person的方法
}

两个结构体可以等值比较吗?

在Go语言中,两个结构体是否可以进行等值比较(使用==!=运算符)取决于该结构体的字段类型。如果一个结构体的所有字段都是可比较的(即它们的类型支持==!=运算符),那么这个结构体也是可比较的。

例如,以下结构体是可比较的,因为它的所有字段都是基本类型,这些类型都支持等值比较:

type Point struct {
    X, Y int
}

type Rectangle struct {
    TopLeft, BottomRight Point
}

// 两个Rectangle实例可以进行等值比较
r1 := Rectangle{Point{0, 0}, Point{1, 1}}
r2 := Rectangle{Point{0, 0}, Point{1, 1}}
fmt.Println(r1 == r2) // 输出:true

然而,如果结构体包含不可比较的字段,比如切片(slice)、映射(map)、函数(function)或包含这些类型的结构体,那么这个结构体就不是可比较的。尝试对这样的结构体进行等值比较会导致编译错误。

type NotComparable struct {
    S []int // 切片类型不可比较
}

// 下面的代码会导致编译错误
// fmt.Println(nc1 == nc2) // 错误:类型NotComparable的值不能使用==比较

对于不可比较的结构体,如果你需要比较它们的内容,你必须手动实现比较逻辑,通常通过定义一个自定义的比较函数来完成。

func (nc NotComparable) Equal(nc2 NotComparable) bool {
    if len(nc.S) != len(nc2.S) {
        return false
    }
    for i, v := range nc.S {
        if v != nc2.S[i] {
            return false
        }
    }
    return true
}

你如何理解interface类型

interface类型是一种特殊的类型,它定义了一组方法的集合,但不实现这些方法。任何其他类型只要实现了interface中定义的所有方法,就被称为实现了这个interface。这种设计允许Go语言实现一种称为“鸭子类型”(Duck Typing)的编程范式,即“如果它看起来像鸭子,叫起来像鸭子,那么它就是鸭子”。

接口的特性

  1. 隐式实现:Go语言中的接口实现是隐式的。不需要显式声明一个类型实现了某个接口,只要该类型实现了接口中定义的所有方法,它就自动实现了该接口。
  2. 多态:接口是Go语言实现多态的关键。通过接口,可以编写出在运行时能够接受多种类型的代码,只要这些类型实现了接口定义的方法。
  3. 组合:接口可以组合其他接口,形成新的接口。这允许创建更复杂的接口,同时保持代码的灵活性和可重用性。
  4. 空接口interface{}是空接口,它不定义任何方法。任何类型都隐式实现了空接口,因此空接口可以存储任何值。

接口的使用

接口在Go语言中非常灵活,可以用于多种场景:

  • 作为函数参数:通过接口作为函数参数,可以编写出接受多种类型输入的通用函数。
  • 作为方法接收者:接口可以作为类型的方法接收者,这允许类型实现接口定义的方法。
  • 作为类型断言和类型切换:通过类型断言和类型切换,可以在运行时检查和转换接口变量的类型。
// 定义一个接口
type Shape interface {
    Area() float64
    Perimeter() float64
}

// 定义一个结构体
type Rectangle struct {
    Width, Height float64
}

// Rectangle实现Shape接口
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// 使用接口
func printShapeInfo(s Shape) {
    fmt.Println("Area:", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}

func main() {
    rect := Rectangle{Width: 3, Height: 4}
    printShapeInfo(rect) // 输出矩形的面积和周长
}

1.18版本后interface有什么增强?

Go 1.18版本引入了泛型(Generics)的特性,这是Go语言自发布以来最大的语言特性更新之一。接口在Go 1.18中并没有直接的增强,但泛型的引入对接口的使用方式产生了间接影响,使得接口的使用更加灵活和强大。

泛型与接口

泛型允许开发者编写可以适用于多种类型参数的函数和类型,而不需要为每种类型编写重复的代码。接口在泛型中扮演了重要的角色,因为它们定义了类型必须满足的契约,而泛型则允许这些契约被应用于多种不同的类型。

接口的间接增强

虽然Go 1.18没有直接增强接口,但泛型的引入间接增强了接口的使用场景:

  1. 更灵活的类型约束:通过泛型,接口可以作为类型约束,定义泛型函数或类型必须满足的条件。这使得接口可以用于更广泛的场景,包括那些需要处理多种类型数据的场景。
  2. 类型参数的接口约束:在泛型中,类型参数可以被约束为必须实现某个接口。这允许编写出更加通用的代码,同时保持类型安全。
  3. 类型参数的组合:泛型允许类型参数组合多个接口约束,这为编写复杂的类型系统提供了更大的灵活性。
// 定义一个接口
type Number interface {
    ~int | ~float64 | ~float32
}

// 使用泛型和接口定义一个函数
func Add[T Number](a, b T) T {
    return a + b
}

func main() {
    // Add函数可以接受任何实现了Number接口的类型
    fmt.Println(Add(1, 2))       // 输出:3
    fmt.Println(Add(3.5, 4.5))   // 输出:8
}
type IfTrue interface {
    ~bool | []string | any
}

func If[T IfTrue](b bool, trueVal, falseVal T) T {
    if b {
        return trueVal
    }
    return falseVal
}

接口类型的等值比较取决于接口持有的具体值。如果接口持有的值是可比较的(比如基本类型如intstring等),那么接口之间可以进行等值比较。如果接口持有的值是不可比较的(比如切片、映射、函数等),那么尝试对这些接口进行等值比较会导致编译错误。

interface可以进行等值比较吗?

可比较的接口

当接口持有的值是可比较的时,可以直接使用==!=运算符进行比较:

package main

import (
    "fmt"
)

// 定义一个接口
type MyInterface interface {
    DoSomething()
}

// 定义一个结构体
type SomeStruct struct {
    Number int
}

// SomeStruct实现了MyInterface接口
func (s SomeStruct) DoSomething() {
    fmt.Println("SomeStruct doing something")
}

// 定义一个函数
func SomeFunction() {
    fmt.Println("SomeFunction doing something")
}

func main() {
    var i1 MyInterface = SomeStruct{Number: 10}
    var i2 MyInterface = SomeStruct{Number: 10}
    var i3 MyInterface = SomeFunction

    // i1和i2是相同的类型,且它们持有的具体值是可比较的
    fmt.Println(i1 == i2) // 输出:true

    // i1和i3是不同的类型,不能进行等值比较
    // fmt.Println(i1 == i3) // 编译错误:invalid operation: i1 == i3 (struct containing DoSomething method cannot be compared)
}

不可比较的接口

当接口持有的值是不可比较的时,尝试进行等值比较会导致编译错误:

type MyInterface interface {
    DoSomething()
}

var i1 MyInterface = someStruct{}
var i2 MyInterface = someOtherStruct{}

// 如果someStruct和someOtherStruct的DoSomething方法实现不同
// 那么尝试比较i1和i2会导致编译错误
fmt.Println(i1 == i2) // 编译错误:invalid operation: i1 == i2 (struct containing DoSomething method cannot be compared)

空接口

空接口interface{}可以持有任何类型的值,因此它是否可以进行等值比较取决于它持有的值是否可比较:

var i1 interface{} = 10
var i2 interface{} = 10

fmt.Println(i1 == i2) // 输出:true

var i3 interface{} = []int{1, 2, 3}
var i4 interface{} = []int{1, 2, 3}

// fmt.Println(i3 == i4) // 编译错误:invalid opera
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇