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
类型数据的通道。
make
与 new
的区别
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 线程创建的高昂成本。
🔥 关键点
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
不能替代正常的流程控制语句(如 return
或 break
)。它的作用仅限于延迟执行某些操作,不能改变程序的逻辑结构。