2.3 slice与append

2.3 slice与append #

一、array #

数组:具有固定长度且拥有零个或多个相同数据类型元素的序列****。

由于固定长度,Go中很少直接使用array,更多使用动态长度的slice,而slice底层由array实现。

数组在Go和C中的主要区别:

  • 数组是值。将一个数组赋予另一个数组会复制其所有元素。数组为值的属性很有用,但代价高昂;若你想要C那样的行为和效率,你可以传递一个指向该数组的指针。但这并不是Go的习惯用法,切片才是。
    1
    2
    3
    4
    5
    6
    7
    8
    
    func Sum(a *[3]float64) (sum float64) {
    	for _, v := range *a {
    		sum += v
    	}
    	return
    }
    array := [...]float64{7.0, 8.5, 9.1}
    x := Sum(&array)  // 注意显式的取址操作
    
  • 特别地,若将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。
  • 数组的长度是其类型的一个组成部分。类型 [10]int 和 [20]int 是不同的类型,长度需要在编译阶段确定,所以必须是常量表达式。
    • 在数组字面值中,如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的个数来计算
      1
      2
      
      q := [...]int{1, 2, 3}
      r := [...]int{99: -1} // 前99个元素初始化为零值,最后一个初始化为-1
      
    • 数组元素可通过索引下标来访问,每个元素都被默认初始化为元素类型对应的零值,或使用字面量初始化;
      1
      
      var r [3]int = [3]int{1, 2}
      

数组的可比较性 comparable:如果一个数组的元素类型是可以相互比较的,数组类型相同(包括数组长度一致),则数组是可以相互比较==的,只有当两个数组的所有元素都是相等的时候数组才是相等的;

  • crypto/sha256包的Sum256函数对一个任意的byte slice类型的数据生成一个对应的消息摘要。消息摘要有256bit大小,因此对应[32]byte数组类型。如果两个消息摘要是相同的,那么可以认为两个消息本身也是相同(译注:理论上有HASH码碰撞的情况,但是实际应用可以基本忽略);如果消息摘要不同,那么消息本身必然也是不同的。下面的例子用SHA256算法分别生成“x”和“X”两个信息的摘要:
  • crypto/sha256:两个消息虽然只有一个字符的差异,但是生成的消息摘要则几乎有一半的bit位是不相同的
    1
    2
    3
    4
    5
    
    import "crypto/sha256"
    func main() {
        c1 := sha256.Sum256([]byte("x"))  // 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881, [32]uint8
        c2 := sha256.Sum256([]byte("X"))  // 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015
    }
    

数组作为函数参数传递的机制值传递,而非引用传递。当调用一个函数时,每个传入的参数都会创建一个副本,然后赋值给对应的函数变量**(**所以函数参数变量接收的是一个副本,并不是原始的参数)。

传递大的数组时将变得很低效,并且在函数内部对数组的任何修改都仅影响副本,而不是原始数组。这种情况下。Go把数组和其他的类型都看成值传递不同于其他语言中数组都是隐式的使用引用传递)。

**Go中可以显示的传递一个数组的指针给函数,**这样在函数内部对数组的任何修改都会反应到原始数组上,这样很高效;但由于数组的长度不可变特性,无法为数组添加或者删除元素,除了在SHA256、矩阵变换这类固定长度的情况下,很少直接使用数组;

1
2
3
4
5
6
// 用于给[32]byte类型的数组清零
func zero(ptr *[32]byte) {
    for i := range ptr {
        ptr[i] = 0
    }
}
1
2
3
4
// zero函数更简洁版本:
func zero(ptr *[32]byte) {
    *ptr = [32]byte{}  // 数组字面值[32]byte{}就可以生成一个32字节的数组。而且每个数组的元素都是零值初始化,也就是0。
}

二、pointer #

变量存储值,变量被称为可寻址的值,对应一个保存了变量对应类型值的****内存地址

**一个指针对应变量的内存地址。通过指针可以绕过变量的名字直接读或更新对应变量的值。**通过变量名或表达式访问(如x[i]或x.f),必定能接受&取地址操作。

对一个变量取地址(p := &v)、复制指针,都是为原变量创建了新的别名*p就是变量v的别名。

**指针特别有价值的地方在于我们可以不用变量名的情况下,直接访问一个变量。**但因为此,要找到一个变量的所有访问者并不容易,我们必须知道变量全部的别名(译注:这是Go语言的垃圾回收器所做的工作)。

不仅仅是指针会创建别名,很多其他引用类型也会创建别名,例如slice、map和chan,甚至结构体、数组和接口都会创建所引用变量的别名

对于聚合类型每个成员,如结构体的每个字段、或者是数组的每个元素也都是对应一个变量,因此可以被取地址。

1
2
3
4
5
x := 1          // &x: 读取x变量的内存地址;p指针指向变量、p指针保存了x变量的内存地址,**其数据类型为 *int** (**指向int类型的指针**)
p := &x         // p, of type *int, points to x, *p: 读取p指针指向的变量的值
fmt.Println(*p) // "1"   // 因为*p对应一个变量,所以可以赋值语句更新指针所指向的变量的值。
*p = 2          // equivalent to x = 2
fmt.Println(x)  // "2"

Go中函数中返回局部变量的地址也是安全的。因为指针p依然引用这个变量,Go中的GC使用的简单的标记清除算法的可达性树法不会识别为垃圾变量,但这会导致内存泄露。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// point study
package main

import "fmt"

func main() {
	// 在局部变量地址&v被返回之后依然有效,因为指针p依然引用这个变量。
	var p = f()
	fmt.Println(p)
	fmt.Println(f() == f()) // "false"

}

func f() *int {
	v := 1
	return &v
}

如果将指针作为参数调用函数,可以在函数中通过该指针来更新变量的值。(译注:这是对C语言中++v操作的模拟,这里只是为了说明指针的用法,incr函数模拟的做法并不推荐):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// incr study
package main

import "fmt"

func main() {
	v := 1
	incr(&v)              // v = 2,return 2
	fmt.Println(incr(&v)) // v = 3 return 3
}

func incr(p *int) int {
	*p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
	return *p
}

nil是一个预声明的标识符,表示指针、通道、函数、接口、映射或切片类型的零值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/builtin/builtin.go
// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

// Type is here for the purposes of documentation only. It is a stand-in
// for any Go type, but represents the same type for any given function
// invocation.
// Type只是为了文档目的,它是一个占位符,代表任何Go类型。在给定的函数调用中代表相同的类型。
type Type int

example:echo #

标准库中的flag包的关键技术为大量使用到了指针,它使用命令行参数来设置对应变量的值。

为了说明这一点,在早些的echo版本中,就包含了两个可选的命令行参数:-n用于忽略行尾的换行符,-s sep用于指定分隔字符(默认是空格)。下面这是第四个版本,对应包路径为gopl.io/ch2/echo4。

调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。它有三个属性:第一个是命令行标志参数的名字“n”,然后是该标志参数的默认值(这里是false),最后是该标志参数对应的描述信息。

如果用户在命令行输入了一个无效的标志参数,或者输入-h-help参数,那么将打印所有标志参数的名字、默认值和描述信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Echo4 prints its command-line arguments.
// See page 33.
package main

import (
	"flag"
	"fmt"
	"strings"
)

// 定义命令行参数名、默认值、和描述信息。
var n = flag.Bool("n", false, "omit trailing newline") // *bool类型的指针,默认为false不换行(终端会打印出一个%作为无换行符的标识)
var sep = flag.String("s", " ", "separator")           // *string类型的指针,默认为空格

func main() {
	// 解析命令行参数,更新每个标志参数对应变量的值(之前是默认值)。
	// 解析命令行参数时遇到错误,默认将打印相关的提示信息,然后调用os.Exit(2)终止程序。
	flag.Parse()
	fmt.Print(strings.Join(flag.Args(), *sep)) // 打印命令行参数,*sep 是一个字符串指针,它的值是通过命令行参数 -s 指定的分隔符。

	if !*n { // 如果命令行参数 -n 没有指定,就打印一个换行符
		fmt.Println()
	}
}

三、slice #

slice(切片,/slaɪs/)[]T表示一个拥有相同类型元素的可变长度的序列**,用来访问底层引用和封装的数组**的元素(引用类型);

slice的三个属性:

  • pointer: 指向第一个可以从slice中访问的元素(数组中的任意一个元素)
  • len:slice的元素个数,不能超过cap
  • cap:slice的起始元素到底层数组的最后一个元素间元素的个数 多个slice可以引用同一个底层数组及其任何位置:
1
2
3
4
5
months := [...]string{1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "Augest", 9: "September", 10: "October", 11: "November", 12: "December"}  // **[13]string 模拟slice引用的底层array**

Q2 := months[4:7]    //  **[]string slice,poiner=, len=3, cap=9**Q2[:10] // // panic: runtime error: slice bounds out of range [:10] with capacity 9

summer := months[6:9]  // **[]string slice, poiner= , len=3, cap=7**

example:reverse

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//  reverse a slice of ints in place.
func reverse(s []int) {
    for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
        s[i], s[j] = s[j], s[i]
    }
}

a := [...]int{0, 1, 2, 3, 4, 5}
reverse(a[:])
fmt.Println(a) // "[5 4 3 2 1 0]"


// 一种将slice元素循环向左旋转n个元素的方法是三次调用reverse反转函数
// 第一次是反转开头的n个元素,然后是反转剩下的元素,最后是反转整个slice的元素。
//(如果是向右循环旋转,则将第三个函数调用移到第一个调用位置就可以了。)
s := []int{0, 1, 2, 3, 4, 5}
// Rotate s left by two positions.
reverse(s[:2])
reverse(s[2:])
reverse(s)
fmt.Println(s) // "[2 3 4 5 0 1]"

slice不直接支持比较运算符:

  • 原因1:一个slice的元素是间接引用的,当slice声明为[]interface{}时,slice的元素甚至可以是自身。虽然有很多办法处理这种情形,但是没有一个是简单有效的。
  • 原因2:因为slice的元素是间接引用的slice本身的值(不是元素的值)**在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改。而如Go语言中map的key只做简单的浅拷贝,它要求key在整个生命周期内保持不变性(译注:如slice扩容,就会导致其本身的值/地址变化)。而用深度相等判断的话,显然在map的key这种场合不合适。对于像指针或chan之类的引用类型,==相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的,也能临时解决map类型的key问题,但是slice和数组不同的相等测试行为会让人困惑。因此,安全的做法是直接禁止slice之间的比较操作**。 标准库提供了高度优化的bytes.Equal函数来判断两个[]byte是否相等。

对于其他类型的slice,我们必须自己展开每个元素进行比较,运行的时间并不比支持==操作的数组或字符串更多。

slice唯一合法的比较操作是和nil比较,如:

1
if summer == nil { /* ... */ }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func equal(x, y []string) bool {
    if len(x) != len(y) {    // Go语言排错式的风格
        return false
    }
    for i := range x {
        if x[i] != y[i] {   // Go语言排错式的风格
            return false
        }
    }
    return true            // Go语言排错式的风格,正常执行的语句不被if缩进
}

测试一个slice是否是空的,使用len(s) == 0来判断而不应该用s == nil来判断

nil值的slice & 长度和容量为0的slice一个nil值的slice的行为和其它任意0长度的slice一样,所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice(除了文档已经明确说明的地方): 如reverse(nil)也是安全的。除了文档已经明确说明的地方

1
2
3
4
5
6
7
// slice(引用类型)的零值为nil:**一个nil值的slice并没有引用任何底层数组**。一个nil值的slice的长度和容量都是0。
// 非nil值但长度和容量也是0的slice:如**[]int{}或make([]int, 3)[3:]**。
// 与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。
var s []int    // len(s) == 0, s == nil
s = nil        // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{}    // len(s) == 0, s != nil

内置的make函数:创建一个指定元素类型、长度和容量的slice。

容量部分可以省略,在这种情况下,容量将等于长度。

在底层,make创建了一个匿名的数组变量,然后返回一个slice。只有通过返回的slice才能引用底层匿名的数组变量。

1
2
3
4
// 在第一种语句中,slice是整个数组的view。
make([]T, len)
// 在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。
make([]T, len, cap) // same as make([]T, cap)[:len]

**二维切片:**切片的切片

1
2
type Transform [3][3]float64  // 一个 3x3 的数组,其实是包含多个数组的一个数组。
type LinesOfText [][]byte     // 包含多个字节切片的一个切片。

有时必须分配一个二维数组,如在处理像素的扫描行时。

独立地分配每一个切片,一次一行:

1
2
3
4
5
6
// 分配顶层切片。
picture := make([][]uint8, YSize) // 每 y 个单元一行。
// 遍历行,为每一行都分配切片
for i := range picture {
	picture[i] = make([]uint8, XSize)
}

一次分配,对行进行切片:

1
2
3
4
5
6
7
8
// 分配顶层切片,和前面一样。
picture := make([][]uint8, YSize) // 每 y 个单元一行。
// 分配一个大的切片来保存所有像素
pixels := make([]uint8, XSize*YSize) // 拥有类型 []uint8,尽管图片是 [][]uint8.
// 遍历行,从剩余像素切片的前面切出每行来。
for i := range picture {
	picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

若切片会增长或收缩,就应该通过独立分配来避免覆盖下一行;若不会,用单次分配来构造对象会更加高效。

四、append #

源码解读 #

核心功能:append是内置函数,用于向切片末尾追加元素。

容量处理:如果切片有足够容量,直接重新切片;如果容量不足,会分配新的底层数组。

**返回值:**返回更新后的切片,必须存储返回值,通常覆盖原变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/builtin/builtin.go
// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, **the destination is resliced to accommodate the
// new elements.** If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//
//	slice = append(slice, elem1, elem2)
//	slice = append(slice, anotherSlice...)
//
// As a special case, it is legal to append a string to a byte slice, like this:
//
//	slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type

工作原理:

  1. 容量充足时:&slice[0]指针地址相同,说明没有重新分配数组
1
2
3
4
5
6
7
// 创建有足够容量的切片
slice := make([]int, 0, 5)  // len=0, cap=5, **&slice[0]: ptr=0xc000018180**

// 追加元素
slice = append(slice, 1, 2, 3)

fmt.Printf("After:  len=%d, cap=%d, ptr=%p\n", len(slice), cap(slice), &slice[0])  // len=3, cap=5, **&slice[0]: ptr=0xc000018180**
  1. 容量不足时:&slice[0]指针地址不同,说明重新分配了数组
1
2
3
4
5
// 创建容量不足的切片
slice := make([]int, 0, 2)  //  len=0, cap=2, **&slice[0]: ptr=0xc000018180**

// 追加超过容量的元素
slice = append(slice, 1, 2, 3, 4, 5)  // After:  len=5, cap=6, **&slice[0]: ptr=0xc00001a0c0**
  1. 陷阱:必须接收返回值 由于通常并不知道某次append调用是否重新分配了内存,不能确认新的slice和原始的slice是否引用的是相同的底层数组空间,不能确认在原先的slice上的操作是否会影响到新的slice。

**因此,通常是将append返回的结果直接赋值给输入的slice变量。**实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。

1
2
3
4
5
6
7
8
slice := []int{1, 2, 3}
    
// 错误:没有接收返回值
// append(slice, 4)  // 编译警告:结果被丢弃

// 正确:接收返回值
slice = append(slice, 4)
fmt.Println(slice)  // [1 2 3 4]

要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作

从这个角度看,slice并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型

1
2
3
4
5
type slice struct {
	ptr    unsafe.Pointer  *// 指向底层数组*
	len    int            *// 长度*
	cap    int            *// 容量*
}

append函数对于理解slice底层是如何工作的非常重要,这里的appendInt专门用于处理[]int类型的slice。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// gopl.io/ch4/append
func appendInt(x []int, y int) []int {
	var z []int
	zlen := len(x) + 1 
	if zlen <= cap(x) { // slice容量充足时
		// There is room to grow.  Extend the slice.
		z = x[:zlen]
	} else { // slice容量不足时
		zcap := zlen
		if zcap < 2*len(x) { // 容量double扩充,避免了多次内存分配, 分摊线性复杂性,确保了添加单个元素的平均时间是一个常数时间。
			zcap = 2 * len(x)
		}
		z = make([]int, zlen, zcap) // 新分配内存
		copy(z, x) // copy内置函数,比通过循环复制元素更方便,返回成功复制的元素的个数
	}
	z[len(x)] = y //  最后将新添加的y元素复制到新扩展的空间z[len(x)],并返回slice。
	return z
}


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var x, y []int
for i := 0; i < 10; i++ {
	y = appendInt(x, i)
	fmt.Printf("i=%d  cap=%d\t y=%v\n", i, cap(y), y)
	x = y
}

/*
//!+output
i=0  cap=1       y=[0]
i=1  cap=2       y=[0 1]
i=2  cap=4       y=[0 1 2]
i=3  cap=4       y=[0 1 2 3] // 容量充足:x包含了[0 1 2]三个元素,但是容量是4。y和x引用着相同的底层数组。
i=4  cap=8       y=[0 1 2 3 4]  // 容量不足:分配一个容量为8的底层数组,将x的4个元素[0 1 2 3]复制到新空间的开头,然后添加新的元素i=4。**y和x是对应不同底层数组的view。**
i=5  cap=8       y=[0 1 2 3 4 5]
i=6  cap=8       y=[0 1 2 3 4 5 6]
i=7  cap=8       y=[0 1 2 3 4 5 6 7]
i=8  cap=16      y=[0 1 2 3 4 5 6 7 8]
i=9  cap=16      y=[0 1 2 3 4 5 6 7 8 9]
//!-output
*/

性能优化 #

  1. 预分配容量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 预分配容量,避免频繁重新分配
slice := make([]int, 0, 1000)
    
fmt.Printf("Initial: len=%d, cap=%d\n", len(slice), cap(slice))

// 追加1000个元素
for i := 0; i < 1000; i++ {
	slice = append(slice, i)
}

fmt.Printf("Final: len=%d, cap=%d\n", len(slice), cap(slice))  // 输出:Final: len=1000, cap=1000,没有重新分配数组

**容量增长策略:**通常是翻倍增长,但具体策略可能因Go版本而异。

1
2
3
4
5
6
7
8
9
slice := make([]int, 0, 1)
    
fmt.Printf("Initial: len=%d, cap=%d\n", len(slice), cap(slice))

// 观察容量增长
for i := 0; i < 20; i++ {
	slice = append(slice, i)
	fmt.Printf("After %d: len=%d, cap=%d\n", i+1, len(slice), cap(slice)). // 1 -> 2 -> 4 -> 8 -> 16 -> 32
}

2. 批量操作,避免循环中的append

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
slice := []int{1, 2, 3}
    
// 批量追加,减少函数调用次数
newElements := []int{4, 5, 6, 7, 8}
slice = append(slice, newElements...)

// 而不是逐个追加
// for _, elem := range newElements {
//     slice = append(slice, elem)
// }


// 不好的做法:在循环中频繁append
var slice []int
for i := 0; i < 1000; i++ {
    slice = append(slice, i)  // 可能多次重新分配
}
    
// 好的做法:预分配容量
slice = make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    slice = append(slice, i)  // 不会重新分配
}

example #

旋转slice、反转slice或在slice原有内存空间修改元素。

nonempty:给定一个字符串列表,下面的nonempty函数将在原有slice内存空间之上返回不包含空字符串的列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Nonempty is an example of an in-place slice algorithm.
// nonempty returns a slice holding only the non-empty strings.
// The underlying array is modified during the call.
// 在原有string slice内存空间之上返回不包含空字符串的列表
// 比较微妙的地方是,输入的slice和输出的slice共享一个底层数组。
// 这可以避免分配另一个数组,不过原来的数据将可能会被覆盖
func nonempty(strings []string) []string {
	i := 0
	for _, s := range strings {
		if s != "" {
			strings[i] = s
			i++
		}
	}
	return strings[:i]
}

func main() {
	data := []string{"one", "", "three"}
	fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
	// 这可以避免分配另一个数组,不过原来的数据将可能会被覆盖
	fmt.Printf("%q\n", data) // `["one" "three" "three"]`
	// 因此我们通常会这样使用nonempty函数,和append函数类似
	data = nonempty(data)
	fmt.Printf("%q\n", data) // `["one" "three"]`
}

// nonempty函数也可以使用append函数实现
func nonempty2(strings []string) []string {
	out := strings[:0] // zero-length slice of original
	for _, s := range strings {
		if s != "" {
			out = append(out, s)
		}
	}
	return out
}

stack:一个slice可以用来模拟一个stack

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func main() {
	// 无论如何实现,以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值
	// 事实上很多这类算法都是用来过滤或合并序列中相邻的元素。这种slice用法是比较复杂的技巧,虽然使用到了slice的一些技巧,但是对于某些场合是比较清晰和有效的。
	// 一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:
	var stack = make([]int, 0)
	v := 1
	stack = append(stack, v) // push v
	// stack的顶部位置对应slice的最后一个元素:
	top := stack[len(stack)-1] // top of stack
	// 通过收缩stack可以弹出栈顶的元素
	stack = stack[:len(stack)-1] // pop
	fmt.Println(top)

	s := []int{5, 6, 7, 8, 9}
	fmt.Println(remove(s, 2)) // [5 6 8 9]
	s = []int{5, 6, 7, 8, 9}
	fmt.Println(remove2(s, 2)) // [5 6 9 8]
}

func remove(slice []int, i int) []int {
	// // 要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成:
	copy(slice[i:], slice[i+1:])
	return slice[:len(slice)-1]

}

// 如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:
func remove2(slice []int, i int) []int {
	slice[i] = slice[len(slice)-1]
	return slice[:len(slice)-1]
}