Go语言学习笔记——常用关键字

2024-04-15 16:55:06 浏览数 (1)

一、for 和 range

Go语言提供了for循环和for...range循环两种循环结构。

for...range完成数据迭代,支持字符串、数组、数组指针、切片、字典、通道类型,返回索引、键值数据。

1. 经典循环和范围循环

  • 经典循环: 使用for关键字和条件语句来控制循环的方式。
  • 范围循环: 范围循环是使用for range关键字来迭代可迭代的数据结构的方式。范围循环支持字符串、数组、数组指针、切片、字典、通道类型,返回索引、键值数据。

2. for range 不会出现循环永动机

代码语言:javascript复制
func main() {
 arr := []int{1, 2, 3}
 for _, v := range arr {
     arr = append(arr, v)
 }
 fmt.Println(arr)
}
// 输出: 1 2 3 1 2 3

for range 在遍历数组或者切片时,会先将数组或者切片拷贝到一个中间变量ha, 在赋值的过程中就发生了拷贝, 所以我们遍历的切片已经不是原始的切片变量了, 因此不会出现循环永动机。

3. for k, v := range 中 ,变量v每一次迭代中被复用

循环中使用的这个变量 v 会在每一次迭代被重新赋值而覆盖,赋值时也会触发拷贝.

代码语言:javascript复制
func main() {
 arr := []int{1, 2, 3}
 newArr := []*int{}
 for _, v := range arr {
  newArr = append(newArr, &v)
 }
 for _, v := range newArr {
  fmt.Println(*v)
 }
}

$ go run main.go
3 3 3

4. for range 遍历map时,顺序随机

Go语言中,map引入了随机数保证遍历的随机性,避免使用者依赖固定的遍历顺序。

Go语言中,map的遍历顺序被设计为随机的,主要是出于两个原因:

  1. 避免依赖:如果map的遍历顺序是固定的,那么开发者可能会依赖这个顺序来编写代码,这是一种不好的编程习惯。因为map本质上是一个无序的数据结构,它的设计目标是提供快速的查找,而不是保持元素的顺序。如果需要有序的数据结构,应该使用其他的数据结构,如数组或切片。
  2. 安全性:随机的遍历顺序可以防止某些类型的散列冲突攻击。如果敌手知道map的遍历顺序,他们可能会尝试构造特定的键,以使得散列函数产生冲突,从而导致程序性能下降。通过随机化遍历顺序,这种攻击的可能性被大大降低。

5. for...range遍历通道channel

在Go语言中,for...range可以用于遍历通道(channel)。

这种结构会持续从通道接收值,直到该通道被关闭。它允许我们在不知道通道何时会停止发送数据的情况下,安全地从通道接收数据。

6. 使用for...range时,常见的错误和陷阱

  1. 修改迭代变量:在for...range循环中,迭代变量实际上是原始集合元素的副本,而不是元素本身。这意味着如果你修改了迭代变量,原始集合不会受到影响。这是一个常见的误解,特别是在遍历数组或切片时。
  2. 并发修改:在多个goroutine中使用for...range遍历并修改同一个集合可能会导致数据竞争。你应该避免这种情况,或者使用适当的同步机制(如互斥锁)来保护数据。
  3. 关闭已关闭的通道:如果你试图关闭一个已经关闭的通道,Go会抛出panic。在使用for...range遍历通道时,你需要确保通道只被关闭一次。
  4. 无限循环:如果你在for...range循环中向通道发送数据,但忘记关闭通道,那么循环将永远不会结束,因为for...range会一直等待新的数据。你需要确保在适当的时候关闭通道。
  5. 忽略for...range的第一个返回值:在遍历映射时,for...range会返回两个值:键和值。如果你只需要值,可能会忽略键,这可能会导致意外的结果。例如,for _, v := range mfor v := range m是不同的,前者遍历的是映射的值,后者遍历的是映射的键。
  6. 字符串遍历:当使用for...range遍历字符串时,返回的索引是Unicode字符的起始字节的索引,而不是连续的。如果字符串包含多字节的Unicode字符,这可能会导致混淆。

二、select

1. select 简介

Go语言中select用于处理多个channel的发送和接收操作。

这使得我们可以在一个goroutine中处理多个channel的数据,极大地提高了并发程序的灵活性和效率。

2. select的基本用法

代码语言:javascript复制
select {
case <-ch1:
    // 从ch1接收数据并处理
case ch2 <- value:
    // 向ch2发送数据
default:
    // 没有可用的case时执行
}

3. select关键字的工作原理

select会遍历每一个case,检查casechannelI/O操作是否可以立即进行。

  • 如果可以立即进行,则执行该case
  • 如果有多个case都可以进行,则随机选择一个执行。
  • 如果没有case可以立即进行,且存在default分支,则执行default分支;否则,select将阻塞,直到至少有一个case可以进行。

4. select关键字的实际应用场景

  1. 使用select实现多路复用

在并发编程中,我们经常需要同时处理多个channel的数据。使用select,我们可以在一个goroutine中同时监听多个channel,等待channel准备好进行I/O操作。这就是所谓的多路复用。例如:

代码语言:javascript复制
select {
case msg1 := <-ch1:
fmt.Println("Received", msg1)
case msg2 := <-ch2:
fmt.Println("Received", msg2)
}

b. 使用select处理超时

代码语言:javascript复制
select {
case res := <-ch:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("Timeout")
}

c. 使用select实现非阻塞通信

有时,我们希望channel的发送和接收操作不会阻塞当前的goroutine。使用select和default分支,我们可以实现非阻塞的channel操作。例如:

代码语言:javascript复制
select {
case msg := <-ch:
fmt.Println("Received", msg)
default:
fmt.Println("No message received")
}

三、defer

1. defer简介

在Go语言中,defer关键字用于注册延迟调用。这些调用直到return前才被执行,通常用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁等。

defer关键字还可以帮助我们避免在函数执行过程中忘记释放资源或处理错误的问题。

2. defer的基本用法

代码语言:javascript复制
func main() {
    file, err := os.Open("file.go")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    // ...处理文件
}

3. defer 的工作原理

在Go语言中,defer关键字的工作原理是基于栈的。

当Go语言执行到一个defer语句时,不会立刻执行defer后面的函数,而是将其推入到一个栈中。然后在函数返回前,按照后进先出LIFO的顺序执行栈中的函数调用。

注意: defer语句中的函数会在return语句更新返回值变量后再执行。这意味着你可以在defer函数中修改返回值。

以下是一个简单的示例:

代码语言:javascript复制
func deferExample() (result int) {
    defer func() {
        result  
    }()
    return 0
}

在这个例子中,虽然return语句返回的是0,但是由于defer语句在return语句后执行,所以最终函数的返回值会变成1。

4. 使用defer处理错误和异常

我们也可以使用defer配合recover函数来捕获和处理运行时的panic,以实现异常处理。

代码语言:javascript复制
func safeDivide(numerator, denominator int) (result int) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
            result = 0
        }
    }()
    return numerator / denominator
}

在这个例子中,如果denominator为0,那么除法操作会引发panic,但是我们的defer函数会捕获这个panic并恢复程序的执行,防止程序崩溃。

四、makenew

makenew是两个用于内存分配的重要关键字。它们都可以用来创建对象。

1. new简介

new是Go语言中的一个内建函数,用于分配内存。它的函数签名为func new(Type) *Type

new函数接受一个类型作为参数,分配足够的内存来容纳该类型的值,并返回指向该内存的指针。

new关键字的工作原理相对简单。当调用new函数时,它会在堆上为指定类型分配一块内存,这块内存会被初始化为该类型的零值,然后返回一个指向这块内存的指针。这个指针指向的内存被清零,也就是说,对于所有的类型,new函数都返回一个指向零值的指针

代码语言:javascript复制
ptr := new(int)
fmt.Println(*ptr) // 输出:0
*ptr = 100
fmt.Println(*ptr) // 输出:100

2. make 简介

make也是Go语言中的一个内建函数,用于分配并初始化下列对象:

  • 切片
  • 映射
  • 通道

make返回的是初始化的(非零)值,而不是指针。

make的函数签名取决于它创建的类型。例如,对于切片,函数签名为func make([]T, len, cap) []T,其中T是切片的元素类型,len是切片的长度,cap是切片的容量。

make函数的工作原理与new函数有所不同。当调用make函数时,它会分配一块内存,初始化该内存,然后返回一个指向该内存的引用。这个引用不是指向零值的指针,而是指向已初始化的值。

代码语言:javascript复制
// 创建一个长度和容量都为5的切片
slice := make([]int, 5)
fmt.Println(slice) // 输出:[0 0 0 0 0]

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

// 创建一个通道
ch := make(chan int)
go func() {
    ch <- 1
}()
fmt.Println(<-ch) // 输出:1

3. makenew关键字的比较

  1. 返回类型new返回的是指向类型零值的指针,而make返回的是初始化后的(非零)值。
  2. 使用类型new可以用于任何类型,而make只能用于切片、映射和通道。
  3. 零值和初始化new分配的内存被清零,也就是说,对于所有的类型,new函数都返回一个指向零值的指针。而make则返回一个已初始化的值,而不是零值。

0 人点赞