Golang 笔记

一.基础部分

Go 语言的设计哲学

简单、显式、组合、并发和面向工程。


go语言的值类型和引用类型?

值类型:int、float、bool、string 和 数组。
值类型的变量直接指向存在内存中的值,值类型的变量的值存储在中。

引用类型:切片(slice)、映射(map)、通道(chan)、接口(interface)和指针(pointer)。
引用类型的变量存储在

调用函数传入结构体时,应该传值还是指针?

go 里面只存在只存在值传递(要么是该值的副本,要么是指针的副本),不存在引用传递。

之所以对于引用类型的传递可以修改原内容数据,是因为在底层默认使用该引用类型的指针进行传递,但是也是使用指针的副本,依旧是值传递。


Go 只有值传递

Go语言中所有函数参数传递都是值传递,这意味着:

  1. 当你传递一个变量给函数时,Go会创建该变量的一个副本传入函数
  2. 这个副本可能是:
    • 原始值的副本(对于基本类型如int、float、struct等)
    • 指针的副本(对于指针类型或引用类型)

为什么能修改"引用类型"?

对于 slice、map、channel 等"引用类型",虽然看起来像是引用传递,但实际上:

  1. 这些类型在底层都是结构体,包含指向实际数据的指针
  2. 当你传递它们时,传递的是这个结构体的副本(值传递)
  3. 但由于副本中的指针指向相同的内存,所以通过副本也能修改原数据

指针也是值传递

即使你显式使用指针:

func modify(p *int) {
   
    *p = 10
}

func main() {
   
    x := 5
    modify(&x)
}

这里传递的是指针&x副本,而不是原始指针本身。但因为副本指针指向相同内存,所以能修改原变量。


与引用传递的区别

真正的引用传递(如C++的&参数)会:
• 直接操作原始变量,不创建副本
• 对参数的修改直接影响调用者

而 Go 的值传递(包括指针副本):
• 总是创建副本
• 只是通过副本中的指针间接访问原数据


副本复制时,是深拷贝还是浅拷贝?

在 Go 语言中,参数传递时的"复制"是浅拷贝(Shallow Copy),而不是深拷贝(Deep Copy)。具体区别如下:


  1. 基本类型(int, float, bool, string 等)

    浅拷贝:直接复制值本身(等同于深拷贝,因为不涉及嵌套结构)

    a := 10
    b := a  // 完全复制值,a 和 b 完全独立
    

  2. 引用类型(slice, map, channel)

    浅拷贝:复制的是底层数据结构的引用(如 slice 的 ptrlencap),但不会复制底层数据

    s1 := []int{
         1, 2, 3}
    s2 := s1  // 复制的是 slice 的 header(ptr, len, cap),但底层数组仍然是同一个
    s2[0] = 99
    fmt.Println(s1) // [99 2 3],因为 s1 和 s2 共享底层数组
    

  1. 结构体(struct)

    浅拷贝:复制结构体的所有字段,但如果字段是指针或引用类型,只会复制指针/引用,不会复制指向的数据

    type Person struct {
         
        Name string
        Friends []string  // 引用类型(slice)
    }
    
    p1 := Person{
         Name: "Alice", Friends: []string{
         "Bob"}}
    p2 := p1  // 浅拷贝,Friends 的底层数组仍然是同一个
    
    p2.Friends[0] = "Charlie"
    fmt.Println(p1.Friends) // ["Charlie"],因为 p1 和 p2 共享 Friends 的底层数组
    

  1. 指针(*T

    浅拷贝:复制的是指针的值(内存地址),不会复制指针指向的数据

    x := 10
    p1 := &x
    p2 := p1  // 复制的是指针的值(地址),p1 和 p2 指向同一个 x
    
    *p2 = 20
    fmt.Println(x) // 20,因为 p1 和 p2 都指向 x
    

总结

类型 复制方式 是否共享底层数据 示例
基本类型 直接复制值(等同于深拷贝) ❌ 不共享 a := 10; b := a
引用类型(slice, map, channel) 浅拷贝(复制引用) ✅ 共享 s1 := []int{1}; s2 := s1; s2[0] = 2
结构体 浅拷贝(字段逐值复制) 如果字段是引用类型,则共享 p2 := p1p1.Friends 是 slice)
指针 浅拷贝(复制地址) ✅ 共享 p2 := p1p1 是指针)

关键区别

  • 浅拷贝:只复制最外层的值,不会递归复制嵌套的数据(如 slice 的底层数组、map 的哈希表、指针指向的数据)。
  • 深拷贝:会递归复制所有数据,完全独立(Go 默认不提供,需要手动实现或使用 encoding/jsongob 等方式)。

如何实现深拷贝?

  1. 切片:

    如何实现切片的深拷贝,避免共享底层数组?

    方法 1:copy()(推荐)

    s1 := []int{
         1, 2, 3}
    s2 := make([]int, len(s1)) // 先创建新 slice
    copy(s2, s1)              // 复制元素(底层数组不同)
    s2[0] = 99
    fmt.Println(s1) // [1, 2, 3](不受影响)
    fmt.Println(s2) // [99, 2, 3]
    

    方法 2:手动创建新 slice

    s1 := []int{
         1, 2, 3}
    s2 := append([]int{
         }, s1...) // 通过 append 创建新 slice
    s2[0] = 99
    fmt.Println(s1) // [1, 2, 3](不受影响)
    fmt.Println(s2) // [99, 2, 3]
    

    方法 3: json 序列化/反序列化

    // 使用 json 序列化/反序列化(适用于可序列化类型)
    func deepCopy(src, dest interface{
         }) error {
         
        bytes, err := json.Marshal(src)
        if err != nil {
         
            return err
        }
        return json.Unmarshal(bytes, dest)
    }
    
    // 示例
    var s1 = []int{
         1, 2, 3}
    var s2 []int
    deepCopy(s1, &s2)  // s2 现在是完全独立的副本
    
  2. 结构体

    方法 1. 手动实现深拷贝(推荐用于简单结构体)

    type Person struct {
         
        Name    string
        Age     int
        Friends []string
    }
    
    func (p *Person) DeepCopy() *Person {
         
        // 创建新结构体
        newPerson := &Person{
         
            Name: p.Name,
            Age:  p.Age,
        }
        
        // 对引用类型字段进行深拷贝
        if p.Friends != nil {
         
            newPerson.Friends = make([]string, len(p.Friends))
            copy(newPerson.Friends, p.Friends)
        }
        
        return newPerson
    }
    
    // 使用示例
    p1 := &Person{
         
        Name:    "Alice",
        Age:     30,
        Friends: []string{
         "Bob", "Charlie"},
    }
    p2 := p1.DeepCopy()
    p2.Friends[0] = "David"
    fmt.Println(p1.Friends) // ["Bob", "Charlie"]
    fmt.Println(p2.Friends) // ["David", "Charlie"]
    

    方法 2. json 序列化/反序列化

    适用于可JSON序列化的结构体:

    import "encoding/json"
    
    func DeepCopyJSON(src, dst interface{
         }) error {
         
        bytes, err := json.Marshal(src)
        if err != nil {
         
            return err
        }
        return json.Unmarshal(bytes, dst)
    }
    
    // 使用示例
    p1 := &Person{
         
        Name:    "Alice",
        Age:     30,
        Friends: []string{
         "Bob", "Charlie"},
    }
    var p2 Person
    err := DeepCopyJSON(p1, &p2)
    if err != nil {
         
        panic(err)
    }
    

Go 中不可序列化的类型

类型 不可序列化的原因 替代方案
chan 绑定运行时状态,跨进程无意义 传递数据而非通道对象
func 依赖代码段和闭包环境,存在安全风险 传递函数标识符和参数

Go 的设计哲学强调明确性和安全性,因此禁止对这类具有运行时依赖的类型进行序列化。


struct 结构体能不能比较

  • 如果 struct 中含有不能被比较的字段类型,就不能被比较
  • 如果 struct 中所有的字段类型都支持比较,那么就可以被比较。

不可被比较的类型:

  • slice,因为 slice 是引用类型,除非是和nil比较
  • map,和 slice 同理,如果要比较两个 map 只能通过循环遍历实现
  • 函数类型

为什么引用类型不能比较 ?

引用类型,是想去比较还是地址?会有歧义,因此 Go 从语言层面上直接杜绝了引用类型的比较;
当然引用类型可以和 nil 进行比较。


golang 中 make 和 new 的区别?

共同点:

  • 给变量分配内存;
  • make 与 new 对堆栈分配处理是相同的,编译器优先进行逃逸分析,逃逸的才分配到堆上

不同点:

  • 作用变量类型不同,new 给 string、int、数组 分配内存;make给 slice、map、channel 分配内存;
  • 返回类型不一样,new 返回指向变量的指针,make 返回变量本身;
  • new 分配的空间被清零。make 分配空间后,会进行初始化;

for range 的时候它的地址会发生变化么?

for a,b := range c 遍历中, a 和 b 在内存中只会存在一份,即之后每次循环时遍历到的数据都是以值覆盖的方式赋给 a 和 b,a,b 的内存地址始终不变。

由于有这个特性,for 循环里面如果开协程,不要直接把 a 或者 b 的地址传给协程。

解决办法:在每次循环时,创建一个临时变量。


rune 类型

rune 是类型 int32 的别名,在所有方面都等价于它,用来区分字符值跟整数值。

在 Go 语言中,字符可以被分成两种类型处理:

  • 对占 1 个字节的英文类字符,可以使用 byte(或者unit8);
  • 对占 1 ~ 4 个字节的其他字符,可以使用 rune(或者int32),如中文、特殊符号等。
    在这里插入图片描述
s := "Go语言编程"
// byte
fmt.Println([]byte(s)) // 输出:[71 111 232 175 173 232 168 128]
// rune
fmt.Println([]rune(s)) // 输出:[71 111 35821 35328]

获取变量类型?

类型开关(Type Switch)是在运行时检查变量类型的最佳方式。

switch v := variable.(type) {
   
case Type1:
    // 当 variable 的类型是 Type1 时执行的代码
case Type2:
    // 当 variable 的类型是 Type2 时执行的代码
default:
    // 当 variable 的类型不在上述 case 中时执行的代码
}

反射

Golang 的反射(reflection)机制允许程序在运行时获取和操作变量的类型

在这里插入图片描述

package main

import (
    "fmt"
    "reflect"
)

func main() {
   
    author := "draven"
    fmt.Println("TypeOf author:", reflect.TypeOf(author))
    fmt.Println("ValueOf author:", reflect.ValueOf(author))
}

// 结果
// TypeOf author: string
// ValueOf author: draven

反射优点:

  • 反射就是在程序运行的过程中,可以对一个未知类型的数据进行操作的过程
  • 可以减少重复代码

缺点:

  • 反射会消耗性能,使程序运行缓慢

Select 底层

go 的 select 为 golang 提供了多路 IO 复用机制,用于处理多个通道(channel)操作。

以下是针对源码 selectgo 函数的关键部分注释:

1. 初始化阶段

// 将输入的 scase 数组和 order 数组转换为固定大小的切片(最大 65536 个 case)
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))

// 计算总 case 数(发送 + 接收)
ncases := nsends + nrecvs
scases := cas1[:ncases:ncases]          // scase 切片
pollorder := order1[:ncases:ncases]     // 轮询顺序(随机化)
lockorder := order1[ncases:][:ncases:ncases] // 加锁顺序(按 channel 地址排序)

2. 生成轮询顺序(Poll Order)

// 遍历所有 case,过滤掉 channel 为 nil 的 case
for i := range scases {
   
    cas := &scases[i]
    if cas.c == nil {
   
        cas.elem = nil // 允许 GC 回收
        continue
    }
    // 使用随机算法打乱轮询顺序(避免饥饿)
    j := fastrandn(uint32(norder + 1))
    pollorder[norder] = pollorder[j]
    pollorder[j] = uint16(i)
    norder++
}
pollorder = pollorder[:norder] // 最终轮询顺序

3. 生成加锁顺序(Lock Order)

// 使用堆排序按 channel 地址排序,避免死锁
for i := range lockorder {
   
    j := i
    c := scases[pollorder[i]].c
    for j > 0 && scases[lockorder[(j-1)/2]].c.sortkey() < c.sortkey() {
   
        k := (j - 1) / 2
        lockorder[j] = lockorder[k]
        j = k
    }
    lockorder[j] = pollorder[i]
}
// ...(后续堆排序调整逻辑)

关键点
• 按 channel 内存地址排序,确保全局加锁顺序一致,防止死锁。


4. 加锁所有 channel

sellock(scases, lockorder) // 按 lockorder 顺序加锁

5. 第一轮轮询(检查就绪的 case)

for _, casei := range pollorder {
   
    casi = int(casei)
    cas = &scases[casi]
    c = cas.c
    if casi >= nsends {
    // 接收 case
        if sg := c.sendq.dequeue(); sg != nil {
   
            goto recv  // 有发送者等待,直接接收
        }
        if c.qcount > 0 {
   
            goto bufrecv // 缓冲区有数据,从缓冲区接收
        }
        if c.closed != 0 {
   
            goto rclose // channel 已关闭
        }
    } else {
    // 发送 case
        if c.closed != 0 {
   
            goto sclose // 不能向已关闭 channel 发送
        }
        if sg := c.recvq.dequeue(); sg != nil {
   
            goto send  // 有接收者等待,直接发送
        }
        if c.qcount < c.dataqsiz {
   
            goto bufsend // 缓冲区未满,写入缓冲区
        }
    }
}

逻辑
• 优先检查是否有可直接处理的 case(如已有 goroutine 阻塞在对面操作)。


6. 第二轮处理(阻塞等待)

if !block {
   
    selunlock(scases, lockorder)
    casi = -1 // 非阻塞且无 case 就绪,直接返回
    goto retc
}

// 将当前 goroutine 加入所有 channel 的等待队列
gp := getg()
for _, casei := range lockorder {
   
    casi = int(casei)
    cas = &scases[casi]
    c = cas.c
    sg := acquireSudog() // 创建 sudog 并加入队列
    sg.g = gp
    sg.isSelect = true
    sg.c = c
    if casi < nsends {
   
        c.sendq.enqueue(sg) // 加入发送队列
    } else {
   
        c.recvq.enqueue(sg) // 加入接收队列
    }
}

// 挂起当前 goroutine,等待被唤醒
gopark(selparkcommit, nil, waitReasonSelect, traceBlockSelect, 1)

7. 被唤醒后的处理

// 从 gp.param 获取唤醒的 sudog(即成功的 case)
sg := (*sudog)(gp.param)
casi = -1
cas = nil

// 清理其他未成功的 case 的等待队列
for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {
   
    if sg == sg1 {
   
        casi = int(casei)
        cas = &scases[casi]
    } else {
   
        c := scases[casei].c
        if casei < nsends {
   
            c.sendq.dequeueSudoG(sg1)
        } else {
   
            c.recvq.dequeueSudoG(sg1)
        }
    }
    releaseSudog(sg1) // 释放 sudog
}

8. 返回结果

retc:
    return casi, recvOK // 返回选中的 case 索引和接收状态

关键路径总结

  1. 初始化:转换输入参数,生成轮询和加锁顺序。
  2. 第一轮轮询:检查是否有立即就绪的 case。
  3. 第二轮阻塞:若无就绪 case,将 goroutine 加入所有 channel 的等待队列。
  4. 唤醒处理:被唤醒后确定成功的 case,清理其他队列。
  5. 返回结果:返回执行的 case 索引和状态。

select 的特性

  • 随机选择
    当多个 case 同时可以执行时,select 会随机选择一个执行。这有助于避免某些 case 长时间得不到执行的情况(饥饿问题)。

  • 非阻塞选择
    使用 default 子句可以实现非阻塞的选择。如果没有任何 case 可以立即执行,select 会执行 default 子句,从而避免阻塞。

  • 多路复用
    select 语句允许在一个 goroutine 中同时等待多个通道操作,类似于多路复用器(multiplexer)。


select 的场景

  1. 竞争选举

这个是最常见的使用场景,多个通道,有一个满足条件可以读取,就可以“竞选成功”

    select {
   
    case i := <-ch1:
        fmt.Printf("从ch1读取了数据%d", i)
    case j := <-ch2:
        fmt.Printf("从ch2读取了数据%d", j)
    case m := <- ch3
        fmt.Printf("从ch3读取了数据%d", m)
    ...
    }
  1. 超时处理(保证不阻塞)

因为select是阻塞的,我们有时候就需要搭配超时处理来处理这种情况,超过某一个时间就要进行处理,保证程序不阻塞。

select {
   
    case str := <- ch1
        fmt.Println("receive str", str)
    case <- time.After(time.Second * 5): 
        fmt.Println("timeout!!")
}
  1. 阻塞main函数

有时候我们会让main函数阻塞不退出,如http服务,我们会使用空的select{}来阻塞main goroutine

package main
import (
    "fmt"
    "time"
)

func main()  {
   
    bufChan := make(chan int)
    
    go func() {
   
        for{
   
            bufChan <-1
            time.Sleep(time.Second)
        }
    }()


    go func() {
   
        for{
   
            fmt.Println(<-bufChan)
        }
    }()
     
    select{
   }
}

Golang 中 selectswitch 的区别

selectswitch 是 Go 语言中两个不同的控制结构,虽然它们语法上有些相似,但用途和机制完全不同。

特性 select switch
用途 用于多通道操作 用于条件分支
执行方式 随机选择一个可执行的 case 从上到下顺序匹配第一个 true case
默认行为 default 分支时不会阻塞 default 分支时作为默认情况
适用场景 通道通信 值比较或类型判断
// switch 示例
value := 2
switch value {
   
case 1:
    fmt.Println("Case 1")
case 2:
    fmt.Println("Case 2") // 这个会被执行
case 3:
    fmt.Println("Case 3")
}

// select 示例
ch := make(chan int, 1)
ch <- 1
select {
   
case <-ch:
    fmt.Println("Received from ch") // 可能执行这个
case ch <- 2:
    fmt.Println("Sent to ch") // 也可能执行这个
}

总结:switch 是条件分支结构,而 select 是专门为通道设计的 I/O 多路复用结构。


context 结构是什么样的?context 使用场景和用途?

Go 1.7 标准库引入 context,它是 goroutine 的上下文,包含 goroutine 的运行状态、环境等信息。

Go 的 Context 的数据结构包含 Deadline,Done,Err,Value

// Conetext 包介绍 : 通常context携带截止时间,**和取消信号**,以及其他跨越API边界的值,Context的方法可以被多个协程同时调用。
package context

type Context interface {
   
 // 返回截止的日期,如果无截止日期,ok返回false
 Deadline() (deadline time.Time, ok bool)
// 返回一个channel,当工作已完成或者上下文被取消时关闭。如果是一个不会被取消的上下文,Done会返回nil
// WithCancel方法,会在被调用cancel时,关闭Done
// WithDeadline方法,会在过截止时间时,关闭Done
// WithTimeout方法,会在超时结束时,关闭Done
 Done() <-chan struct{
   }
// Done没有被关闭时,会返回nil
// 如果Done关闭了,将会返回关闭的原因(取消、超时)
Err() error
// 返回与当前上下文关联的键值或nil。如果没有值与键关联,使用相同键连续调用 Value 会返回相同的结果
Value(key interface{
   }) interface{
   }
}

context 主要用来:

  • 在 goroutine 之间传递上下文信息,包括:取消信号、超时时间(context.WithTimeout )、截止时间、k-v 等
  • 上下文控制
  • 多个 goroutine 之间的数据交互等
  • 超时控制:到某个时间点超时,过多久超时
    在这里插入图片描述

Go语言中的单引号、双引号和反引号

  1. 单引号
    单引号表示 rune(int32) 类型,单引号里面是单个字符,对应的值为改字符的ASCII值。
func main() {
   
    a := 'A'
    fmt.Println(a)
}

// 输出:
// 65
  1. 双引号
    双引号里面可以是字符串和转义字符,如\n、\r等,对应 go 语言中的 string 类型。
func main() {
   
    a := "Hello golang\nI am random_wz."
    fmt.Println(a)
}

// 输出:
// Hello golang
// I am random_wz.
  1. 反引号
    多行内容,不支持转义。
func main() {
   
    a := `Hello golang\n:
I am random_wz.
Good.`
    fmt.Println(a)
}

// 输出:
// Hello golang\n:
// I am random_wz.
// Good.

// 可以看到 `\n` 并没有被转义,而是被直接作为字符串输出。

Go 语言触发 panic 的情况?

在 Go 语言中,panic 是一种用于处理程序无法继续执行的严重错误的机制。

当程序遇到无法恢复的错误时,会触发 panic,导致程序立即停止当前函数的执行,并开始逐层向上回溯调用栈,执行每个函数的 defer 语句,最后退出程序。

以下是 Go 中常见的触发 panic 的情况:

  1. 数组或切片越界访问
    • 当访问数组或切片的索引超出其范围时,会触发 panic
    arr := [3]int{
         1, 2, 3}
    fmt.Println(arr[5]) // panic: runtime error: index out of range [5] with length 3
    

  1. 空指针解引用
    • 当解引用一个 nil 指针时,会触发 panic
    var ptr *int
    fmt.Println(*ptr) // panic: runtime error: invalid memory address or nil pointer dereference
    

  1. 向已关闭的通道发送数据
    • 当向一个已关闭的通道发送数据时,会触发 panic
    ch := make(chan int)
    close(ch)
    ch <- 1 // panic: send on closed channel
    

  1. 类型断言失败
    • 当类型断言失败且未使用 ok 接收返回值时,会触发 panic
    var i interface{
         } = "hello"
    num := i.(int) // panic: interface conversion: interface {} is string, not int
    

  1. 除零操作
    • 当进行整数除零操作时,会触发 panic
    a := 10
    b := 0
    fmt.Println(a / b) // panic: runtime error: integer divide by zero
    

  1. 递归调用栈溢出
    • 当递归调用过深,导致调用栈溢出时,会触发 panic
    func infiniteRecursion() {
         
        infiniteRecursion()
    }
    infiniteRecursion() // panic: runtime error: stack overflow
    

  1. 手动调用 panic
    • 开发者可以手动调用 panic 来中止程序执行。
    panic("something went wrong") // panic: something went wrong
    

  1. 使用未初始化的 map
    • 当向一个未初始化的 map 插入数据时,会触发 panic
    var m map[string]int
    m["key"] = 1 // panic: assignment to entry in nil map
    

  1. 调用 sync.WaitGroupDone 方法次数过多
    • 当调用 sync.WaitGroupDone 方法次数超过 Add 方法设置的值时,会触发 panic
    var wg sync.WaitGroup
    wg.Add(1)
    wg.Done()
    wg.Done() // panic: sync: negative WaitGroup counter
    

  1. 并发读写 map
  • 当多个 goroutine 并发读写 map 时,会触发 panic
m := make(map[string]int)
go func() {
   
    m["key"] = 1
}()
fmt.Println(m["key"]) // 可能触发 panic: concurrent map read and map write

  1. 调用 close 关闭 nil 通道
  • 当尝试关闭一个 nil 通道时,会触发 panic
var ch chan int
close(ch) // panic: close of nil channel

  1. 使用 sync.Mutex 未正确加锁
  • 当尝试解锁一个未加锁的 sync.Mutex 时,会触发 panic
var mu sync.Mutex
mu.Unlock() // panic: sync: unlock of unlocked mutex

Go 语言中字符串拼接的方法

Go 语言中有多种字符串拼接方式,各有优缺点和适用场景。以下是主要的字符串拼接方法:

  1. 使用 + 运算符

    最简单的拼接方式:

    str1 := "Hello"
    str2 := "World"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值