liujie
liujie
Published on 2025-02-20 / 23 Visits
0
0

GO 学习(二)

#Go

make

在 Go 语言中,make 是一个用于 初始化切片(slice)、映射(map) 和 通道(channel) 的内建函数。它与 new 函数不同,make 函数不仅分配内存,还初始化数据结构的内部状态,使其可以被使用。

1. 切片(slice)

  • 切片是 Go 中非常常用的数据结构,它是对数组的动态视图。通过 make 创建的切片会自动初始化其底层数组并设置切片的长度和容量。

语法:

make([]T, length, capacity)
  • T:切片元素的类型。

  • length:切片的长度。

  • capacity:切片的容量(可选,如果省略则容量与长度相同)。

例子:

slice := make([]int, 5, 10)
 // 创建一个长度为 5,容量为 10 的切片 
fmt.Println(slice) 
// 输出: [0 0 0 0 0]

这会创建一个 int 类型的切片,初始长度为 5,容量为 10,切片中每个元素默认值为 0

2. 映射(map)

  • map 是 Go 的关联数组(类似 Java 中的 HashMap),通过 make 函数可以初始化一个空的 map

语法:

make(map[K]V, capacity)
  • K:键的类型。

  • V:值的类型。

  • capacity:指定 map 的初始容量(可选)。

例子:

m := make(map[string]int) 
// 创建一个空的映射 
m["age"] = 25 
fmt.Println(m) 
// 输出: map[age:25]

这创建了一个空的 map,其键类型为 string,值类型为 int

3. 通道(channel)

  • channel 是 Go 中的一个并发原语,允许 goroutines 之间进行通信。通过 make 创建通道。

语法:

make(chan T, capacity)
  • T:通道中传输的数据类型。

  • capacity:通道的缓冲区大小(可选,如果省略则创建无缓冲通道)。

例子:

g

// 创建一个容量为 2 的通道 ch <- 1 ch <- 2 fmt.Println(<-ch) // 输出: 1 fmt.Println(<-ch) // 输出: 2

这创建了一个可以容纳 2 个 int 类型数据的通道。

makenew 的区别

  • new 用于分配内存并返回指向类型零值的指针,但不会初始化数据结构。

  • make 用于初始化切片、映射和通道,返回的是数据结构的引用,而不是指针。

举例说明:

使用 new 创建切片:

slicePtr := new([]int) // 创建一个指向切片的指针,值为 nil复制编辑

这里 slicePtr 是一个指向切片的指针,它的值是 nil

使用 make 创建切片:

slice := make([]int, 5) // 创建一个长度为 5 的切片,初始值为 [0 0 0 0 0]

这里 slice 是一个有效的切片,长度为 5,元素默认值为 0

总结

  • make:用于初始化切片、映射和通道,它不仅分配内存,还初始化数据结构的内部状态。

  • new:用于分配内存并返回指向类型零值的指针,但不会初始化数据结构。

  • make 是 Go 中专门用来创建和初始化复杂数据结构的内建函数,而 new 则更为基础,主要用于分配简单类型的内存。

通过理解 make 的功能,可以更好地掌握 Go 中如何处理内存分配和数据结构的初始化。

Select 语句

select 是 Go 中的一个控制结构,类似于 switch 语句。

select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。

select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。

如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。

Go 编程语言中 select 语句的语法如下:

select {
  case <- channel1:
    // 执行的代码
  case value := <- channel2:
    // 执行的代码
  case channel3 <- value:
    // 执行的代码

    // 你可以定义任意数量的 case

  default:
    // 所有通道都没有准备好,执行的代码
}

<-

<- 是一个 channel 操作符,用于在 channel 中接收或发送数据。具体来说,<- 有两种主要的用途:

1. 接收数据:<-channel 用来从一个 channel 中接收数据。

• 例如:msg1 := <-c1 表示从 c1 channel 中接收数据,并将其存储在变量 msg1 中。

2. 发送数据:channel <- value 用来将数据发送到一个 channel 中。

• 例如:c1 <- "one" 表示将字符串 "one" 发送到 c1 channel。

channel

在 Go 语言中,channel(通道)是一种用于在不同的 goroutine 之间进行通信的机制。它允许一个 goroutine 将数据发送到另一个 goroutine,而不需要使用显式的锁或共享内存。通道是 Go 并发模型的核心部分,用于协调不同 goroutine 之间的工作。

可以将 channel 看作是一个管道,数据从一个 goroutine 通过管道流向另一个 goroutine。通过这种方式,goroutine 之间可以安全地共享数据。

主要特点:

1. 类型安全:每个 channel 都有一个类型,表示它能传递的数据类型。例如,chan int 表示只能传递 int 类型数据的 channel。

2. 同步机制:channel 也提供了同步机制。当一个 goroutine 向 channel 发送数据时,它会被阻塞,直到另一个 goroutine 从该 channel 接收到数据为止,反之亦然。这意味着发送和接收操作是同步的,确保数据的正确传递。

3. 无锁并发:与传统的共享内存模型相比,channel 提供了一种通过通信而非共享内存来实现并发的方法,避免了显式的锁。

Channel 的基本用法

1. 创建 Channel:

使用 make(chan T) 来创建一个 channel,T 是数据类型,表示该 channel 能传输的数据类型。

c := make(chan int) // 创建一个传递 int 类型数据的 channel
c <- 42 // 将 42 发送到 c channel 中

示例

package main

import (
    "fmt"
    "time"
)

func main() {

    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}

以上代码执行结果为:

received one
received two

Goroutine

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

goroutine 语法格式:

go 函数名( 参数列表 )

go f(x, y, z)

开启一个新的 goroutine:
  • go sayHello() 启动一个新 Goroutine 并异步执行 sayHello()

  • main() 函数不会等待 sayHello() 结束,而是直接返回。因此,可能会导致 Goroutine 还没执行完,程序就结束了

  • 这里 time.Sleep(time.Second) 只是为了确保 Goroutine 有时间执行(不推荐这种方式)

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("Hello from Goroutine!")
}

func main() {
    go sayHello() // 启动 Goroutine
    time.Sleep(time.Second) // 等待 Goroutine 执行完成
}

1. WaitGroup

Goroutine 是异步执行的,所以需要同步机制来确保它们完成任务后再退出程序。

sync.WaitGroup 用于等待多个 Goroutine 完成。

使用:sync.WaitGroup

package main

import (
    "fmt"
    "sync"
)

func sayHello(wg *sync.WaitGroup) {
    defer wg.Done() // 任务完成,减少计数
    fmt.Println("Hello from Goroutine!")
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1) // 计数 +1
    go sayHello(&wg) // 启动 Goroutine
    wg.Wait() // 等待所有 Goroutine 结束
}
  • wg.Add(1) 增加计数,表示有 1 个 Goroutine 在执行。

  • wg.Wait() 阻塞 main(),直到 wg.Done() 让计数变回 0,程序才退出。

2. Goroutine 调度模型(GMP)

Go 语言使用 GMP(Goroutine、M、P)调度模型管理 Goroutine,避免 OS 线程创建的高昂成本。

组件

作用

G (Goroutine)

代码运行单元,代表 Go 语言的一个并发任务

P (Processor)

逻辑处理器,管理 Goroutine 队列

M (OS Thread)

操作系统线程,执行 Goroutine

🔥 关键点

  • Go 运行时自动管理 Goroutine 调度,让多个 Goroutine 共享少量 OS 线程。

  • 避免 Goroutine 频繁切换 OS 线程,提高性能。

Buffered Channel:

创建有缓冲的 Channel。

ch := make(chan int, 2)

Context:

用于控制 Goroutine 的生命周期。

context.WithCancel、context.WithTimeout。

Mutex 和 RWMutex:

sync.Mutex 提供互斥锁,用于保护共享资源。

var mu sync.Mutex
mu.Lock()
// critical section
mu.Unlock()

goto

Go 语言的 goto 语句可以无条件地转移到过程中指定的行。

goto 语句通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能。

但是,在结构化程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难。

语法

goto 语法格式如下:

goto label;
..
.
label: statement;

标签

标签(label)是一个标识符,后面跟着一个冒号 (:),用来标记程序中的某个位置。标签本身并不执行任何操作,它主要用于和 goto、break、continue 等控制流语句一起使用,来控制程序的跳转。

package main

import "fmt"

func main() {
    fmt.Println("开始")
    
    // 使用 goto 跳转到 "end" 标签处
    goto end
    
    fmt.Println("这行代码不会执行")
    
end:
    fmt.Println("跳转到标签处")
}

defer

defer 是一个非常重要的关键字,用于延迟执行某些操作。它通常被用来处理资源清理、解锁、关闭文件或网络连接等任务。通过 defer,可以确保这些操作在函数返回之前被执行,从而避免资源泄漏或其他问题。

1. 基本概念

defer 的核心作用是延迟执行某段代码 。具体来说:

  • 使用 defer 修饰的语句会被放入一个栈中。

  • 当包含 defer 的函数即将返回时(无论是正常返回还是因错误提前退出),defer 语句会按照后进先出(LIFO)的顺序依次执行。

package main

import "fmt"

func main() {
    defer fmt.Println("World") // 延迟执行
    fmt.Println("Hello")
}

输出:

Hello

World

2. defer 的执行顺序

多个 defer 语句会按照后进先出(LIFO)的顺序执行。也就是说,最后定义的 defer 会最先执行。

package main

import "fmt"

func main() {
    defer fmt.Println("First defer")
    defer fmt.Println("Second defer")
    defer fmt.Println("Third defer")
    fmt.Println("Start")
}

输出:

Start
Third defer
Second defer
First defer

3. defer 的常见用途

(1) 资源清理

defer 常用于释放资源,例如关闭文件、数据库连接或网络连接等。

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("example.txt")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer file.Close() // 确保文件在函数结束时关闭

    // 文件操作逻辑
    fmt.Println("File opened successfully")
}
(2) 解锁互斥锁

当使用互斥锁(sync.Mutex)时,defer 可以确保在函数结束时释放锁。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    mu.Lock()
    defer mu.Unlock() // 确保锁会在函数结束时释放

    fmt.Println("Critical section")
}
(3) 捕获函数返回值

defer 还可以用于修改函数的返回值,尤其是在匿名函数中。

package main

import "fmt"

func calculate() (result int) {
    defer func() {
        result += 10 // 修改返回值
    }()
    result = 5
    return
}

func main() {
    fmt.Println(calculate()) // 输出 15
}

4. 注意事项

(1) 参数求值时机

defer 语句中涉及的参数会在 defer 定义时立即求值,而不是在执行时求值。

package main

import "fmt"

func main() {
    i := 1
    defer fmt.Println("Deferred value:", i) // 此时 i 的值为 1
    i++
    fmt.Println("Current value:", i)
}

//输出
//Current value: 2
//Deferred value: 1
(2) 性能影响

虽然 defer 非常方便,但如果在一个高频调用的函数中大量使用 defer,可能会对性能产生一定影响。这是因为 defer 的实现需要维护一个栈来存储延迟调用的信息。

(3) 不能用于控制流

defer 不能替代正常的流程控制语句(如 returnbreak)。它的作用仅限于延迟执行某些操作,不能改变程序的逻辑结构。


Comment