Go小技巧&易错点100例(十六)

2024-04-26 22:29:33 浏览数 (1)

本期看点:

正文开始:

切片的长度和容量

在Go语言中,切片(slice)是一个引用类型,它是对底层数组的抽象表示,提供了动态长度的、灵活的序列类型。切片包含三个重要的属性:指向底层数组的指针、切片的长度以及切片的容量。

  1. 长度(Length) :切片的长度是指切片中当前包含的元素个数。它可以通过内置的len()函数来获取。例如,对于切片s,其长度可以通过len(s)得到。
  2. 容量(Capacity) :切片的容量是指从切片的第一个元素开始,到底层数组中最后一个元素之间的元素个数。换句话说,容量表示在不重新分配底层数组的情况下,切片可以容纳的元素的最大数量。容量可以通过内置的cap()函数来获取。对于切片s,其容量可以通过cap(s)得到。

切片的长度和容量之间有一个重要的关系:切片的长度不会超过其容量。 这意味着,当你尝试向切片添加更多元素时,如果添加后的元素个数超过了切片的容量,Go语言会创建一个新的、更大的底层数组,并将原有元素和新元素复制到新数组中,然后让切片指向这个新的底层数组。这个过程称为切片的扩容。

需要注意的是,切片的长度和容量是可以变化的。当你使用append()函数向切片添加元素时,如果添加后的元素个数没有超过容量,那么切片的长度会增加,但容量保持不变。如果超过了容量,则会发生扩容,切片的长度和容量都会增加。

一个简单的例子:

代码语言:go复制
func SliceLenAndCap1() {
	list := make([]int, 0)
	fmt.Printf("list elements = % v , len = %d , cap = %d n", list, len(list), cap(list))
	newList := append(list, 1, 2, 3)
	fmt.Printf("list elements = % v , len = %d , cap = %d n", newList, len(newList), cap(newList))
	newList2 := append(newList, 4)
	fmt.Printf("list elements = % v , len = %d , cap = %d n", newList2, len(newList2), cap(newList2))

	// 输出:
	//list elements = [] , len = 0 , cap = 0
	//list elements = [1 2 3] , len = 3 , cap = 3
	//list elements = [1 2 3 4] , len = 4 , cap = 6
}

func SliceLenAndCap2() {
	list := []int{1, 2, 3}
	fmt.Printf("list elements = % v , len = %d , cap = %d n", list, len(list), cap(list))
	newList := append(list, 4)
	fmt.Printf("list elements = % v , len = %d , cap = %d n", newList, len(newList), cap(newList))

	// 输出:
	//list elements = [1 2 3] , len = 3 , cap = 3
	//list elements = [1 2 3 4] , len = 4 , cap = 6
}

在Go语言的源码(src/runtime/slice.go)中有growslice这样一个函数解释了切片的扩容原理,当原切片长度小于 1024 时,新的切片长度直接加上 append 元素的个数,容量则会直接 *2,当原切片长度大于等于 1024 时,新的切片长度直接加上 append 元素的个数,容量则会增加 1/4,所以在日常开发中,如果能明确知道切片的长度或者容量时,我们需要在初始化的时候声明,避免切片频繁扩容而带来的花销。

for循环中使用defer

在Go语言中,defer语句用于在函数返回前执行一些清理操作,无论函数是正常返回还是发生了panic。当你在for循环中使用defer时,有几个关键点需要注意:

  1. 延迟执行defer语句中的函数会在包含它的函数返回前被调用,而不是在defer语句被执行时立即调用。这意味着,如果你在for循环中使用了defer,循环体内的代码会先执行完毕,然后才会执行defer中的函数。
  2. 后进先出(LIFO) :多个defer语句在函数中的执行顺序是后进先出。也就是说,最后一个defer语句中的函数会第一个被调用,依此类推。
  3. 参数值:当defer语句被遇到时,会记录其函数和参数,但参数的值是在defer实际执行时才被计算的。这意味着如果defer语句中的函数参数依赖于循环变量,那么这些参数将使用循环结束时的值。

下面是一个例子来演示这些概念:

代码语言:go复制
package main

import "fmt"

func main() {
    for i := 0; i < 5; i   {
        defer fmt.Println(i)
    }
}

在上面的代码中,defer fmt.Println(i)语句会在每次循环迭代时都被记录,但fmt.Println(i)函数本身并不会立即执行。当main函数返回时,这些defer语句会按照后进先出的顺序执行。因为循环变量i在每次迭代时都被更新,所以最终所有的defer语句都会打印出循环结束时的值,即4

如果你希望每个defer打印出它自己被创建时的循环变量值,你可以通过将循环变量作为参数传递给一个匿名函数来捕获其当前值:

代码语言:go复制
package main

import "fmt"

func main() {
    for i := 0; i < 5; i   {
        defer func(x int) {
            fmt.Println(x)
        }(i)
    }
}

在这个修改后的例子中,我们创建了一个匿名函数,它接受一个参数x,并立即使用循环变量i的值来调用它。这样做可以确保每个defer语句捕获并记住它自己的i值,并在稍后打印出来。输出将会是:

代码语言:shell复制
4
3
2
1
0

这展示了defer语句在for循环中是如何按照后进先出的顺序执行的,并且说明了如何捕获循环变量的当前值以便在defer语句执行时使用。

Go语言TrimLeft函数

在Go语言的strings包中,TrimLeft函数用于删除字符串左侧的指定字符集合。它接受两个参数:一个是要处理的字符串,另一个是要删除的字符集合。

下面是strings.TrimLeft的基本用法:

代码语言:go复制
package main

import (
	"fmt"
	"strings"
)

func main() {
	str := "   Hello, World!   "
	// 去除字符串左侧的空格
	trimmed := strings.TrimLeft(str, " ")
	fmt.Println(trimmed) // 输出: "Hello, World!   "

	// 去除字符串左侧的多个特定字符
	strWithPrefix := "!!!Hello, World!!!"
	trimmedWithPrefix := strings.TrimLeft(strWithPrefix, "!")
	fmt.Println(trimmedWithPrefix) // 输出: "Hello, World!!!"

	// 去除字符串左侧的字符集合中的任意字符
	strWithChars := "abcHello, World!abc"
	trimmedWithChars := strings.TrimLeft(strWithChars, "abc")
	fmt.Println(trimmedWithChars) // 输出: "Hello, World!abc"
}

在上面的例子中,我们首先使用strings.TrimLeft来删除字符串左侧的空格。然后,我们删除了一个字符串左侧的多个感叹号字符。最后,我们删除了一个字符串左侧的任何abc字符。

需要注意的是,strings.TrimLeft只删除字符串左侧的字符,而不会对字符串的右侧进行任何操作。如果你想要同时删除字符串两侧的字符,可以使用strings.TrimSpace(仅删除空格)或strings.Trim(删除两侧的指定字符集合)。

另外,如果你想要删除字符串左侧满足某个条件的字符(例如,删除所有数字),你可以使用正则表达式配合regexp包来实现,但strings.TrimLeft本身只支持删除指定的字符集合。

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

0 人点赞