一、程序结构
1.1 命令行参数
os
包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从 os
包的 Args
变量获取;os
包外部使用 os.Args
访问该变量。
var s, sep string
// 第一种循环
for i := 1; i < len(os.Args); i++ {
s += sep + os.Args[i]
fmt.Printf("Args[%d] = %s", i, os.Args[i])
sep = " "
}
// 第二种循环,多使用这种,不容易出错
//for _, arg := range os.Args[1:] {
// s += sep + arg
// sep = " "
//}
fmt.Println(s)
第一种循环常有以下三种形式:
for initialization; condition; post {
// zero or more statements
}
// a traditional "while" loop
for condition {
// ...
}
// a traditional infinite loop
for {
// ...
}
fmt.Printf
函数对一些表达式产生格式化输出,常用的格式如下:
%d 十进制整数
%x, %o, %b 十六进制,八进制,二进制整数。
%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00
%t 布尔:true或false
%c 字符(rune) (Unicode码点)
%s 字符串
%q 带双引号的字符串"abc"或带单引号的字符'c'
%v 变量的自然形式(natural format)
%T 变量的类型
%% 字面上的百分号标志(无操作数)
1.2 命名
如果一个名字是在函数内部定义,那么它就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。
名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母。
在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。因此,在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名,但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。
1.3 声明
Go语言主要有四种类型的声明语句:var
、const
、type
和func
,分别对应变量、常量、类型和函数实体对象的声明。
一个Go语言编写的程序对应一个或多个以.go
为文件后缀名的源文件。每个源文件中以包的声明语句开始,说明该源文件是属于哪个包。包声明语句之后是import
语句导入依赖的其它包,然后是包一级的类型、变量、常量、函数的声明语句,包一级的各种类型的声明语句的顺序无关紧要(译注:函数内部的名字则必须先声明之后才能使用)。
1.4 变量
变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型的 零值(zero value),数值类型是 0
,字符串类型是空字符串 ""
。
下面这些都等价:
s := ""
var s string
var s = ""
var s string = ""
第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为 ""
。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。
1)指针
任何类型的指针的零值都是nil
。如果p
指向某个有效变量,那么p != nil
测试为真。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil
时才相等。
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。不仅仅是指针会创建别名,很多其他引用类型也会创建别名,例如slice
、map
和chan
,甚至结构体、数组和接口都会创建所引用变量的别名。
2)new函数
表达式new(T)
将创建一个T
类型的匿名变量,初始化为T
类型的零值,然后返回变量地址,返回的指针类型为*T
。
p := new(int) // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"
用new
创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(T)
。换言之,new
函数类似是一种语法糖,而不是一个新的基础概念。
3)生命周期
对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
1.5 赋值
元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。
i, j, k = 2, 3, 5
x, y = y, x
a[i], a[j] = a[j], a[i]
赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。可赋值性的规则对于不同类型有着不同要求,于目前我们已经讨论过的类型,它的规则是简单的:类型必须完全匹配,nil
可以赋值给任何指针或引用类型的变量。常量则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。
对于两个值是否可以用==
或!=
进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之亦然。
1.6 类型
一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。
type 类型名字 底层类型
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度
Celsius
和Fahrenheit
分别对应不同的温度单位。它们虽然有着相同的底层类型float64
,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。
类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。
类型转换
对于每一个类型T
,都有一个对应的类型转换操作T(x)
,用于将x
转为T
类型(译注:如果T
是指针类型,可能会需要用小括弧包装T
,比如(*int)(0))
。只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。
1.7 包和文件
Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。一个包的源代码保存在一个或多个以.go
为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径;例如包gopl.io/ch1/helloworld
对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld
。
每个包都对应一个独立的名字空间。例如,在image
包中的Decode
函数和在unicode/utf16
包中的 Decode
函数是不同的。要在外部引用该函数,必须显式使用image.Decode
或utf16.Decode
形式访问。
任何在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。对于导入的包,例如tempconv
导入的fmt
包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt
包,当前包的其它源文件无法访问在当前源文件导入的包。
对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init
初始化函数来简化初始化工作。每个文件都可以包含多个init
初始化函数:
func init() { /* ... */ }
二、基础数据类型
2.1 整型
Go语言同时提供了有符号和无符号类型的整数运算。
- 有符号整数有
int8
、int16
、int32
和int64
,分别对应大小为8
、16
、32
、64bit
,与此对应的是uint8
、uint16
、uint32
和uint64
四种无符号整数类型。 - 有符号和无符号整数
int
和uint
,这两种类型都有同样的大小,32
或64bit
。但是我们不能对此做任何的假设;因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。 - 还有一种无符号的整数类型
uintptr
,没有指定具体的bit大小但是足以容纳指针。uintptr
类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。
int
和int32
也是不同的类型,即使int
的大小也是32bit
,在需要将int
当作int32
类型的地方需要一个显式的类型转换操作,反之亦然。
2.2 浮点数
Go语言提供了两种精度的浮点数,float32
和float64
,浮点相关的运算都在math
包中。
2.3 复数
Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部:
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(-5+10i)"
fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"
如果一个浮点数面值或一个十进制整数面值后面跟着一个i,例如3.141592i或2i,它将构成一个复数的虚部,复数的实部是0:
fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1
2.4 布尔型
一个布尔类型的值只有两种:true和false。
2.5 字符串
文本字符串通常被解释为采用UTF8编码的Unicode码点(rune,int32等价类型)序列,内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目)。第i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节。
s := "hello, world"
fmt.Println(len(s)) // "12"
fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')
fmt.Println(s[0:5]) // "hello"
fmt.Println(s[:5]) // "hello"
fmt.Println(s[7:]) // "world"
fmt.Println(s[:]) // "hello, world"
1)Unicode 和 UTF-8
起初计算机只有一个ASCII字符集,使用7bit(0xxxxxxx)来表示128个字符:包含英文字母的大小写、数字、各种标点符号和设备控制符。随着互联网的发展,计算机被越来越多的国家使用,ASCII字符集显然不够用了。
所以就有了Unicode( http://unicode.org ),它收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,每个符号都分配一个唯一的Unicode码点,Unicode码点对应Go语言中的rune整数类型(译注:rune是int32等价类型)。
Unicode每个符号的码点都设计成int32位,这种方式比较简单统一,但是它会浪费很多存储空间,因为大多数计算机可读的文本是ASCII字符,本来每个ASCII字符只需要8bit或1字节就能表示。
所以又有了UTF8,这是一个将Unicode码点编码为字节序列的变长编码。UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。
0xxxxxxx runes 0-127 (ASCII)
110xxxxx 10xxxxxx 128-2047 (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused)
变长的编码无法直接通过索引来访问第n个字符,但是UTF8编码获得了很多额外的优点。首先UTF8编码比较紧凑,完全兼容ASCII码,并且可以自动同步:它可以通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看(译注:像GBK之类的编码,如果不知道起点位置则可能会出现歧义)。没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节,可以很好地兼容那些使用NUL作为字符串结尾的编程语言。
unicode/utf8
包则提供了用于rune字符序列的UTF8编码和解码的功能。
"世界"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754c"
Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。
2)字符串和Byte切片
标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包,还有一个文件路径的包path。
- strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
- bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。
- strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
- unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。
- path和path/filepath包提供了关于文件路径名更一般的函数操作。
一个字符串是包含只读字节的数组,一旦创建,是不可变的。相比之下,一个字节slice的元素则可以自由地修改。字符串和字节slice之间可以相互转换:
s := "abc"
b := []byte(s)
s2 := string(b)
从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据,但总的来说需要确保在变量b被修改的情况下,原始的s字符串也不会改变。将一个字节slice转换到字符串的string(b)操作则是构造一个字符串拷贝,以确保s2字符串是只读的。
2.6 常量
每种常量的潜在类型都是基础类型:boolean、string或数字。所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。
因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度:
const IPv4Len = 4
// parseIPv4 parses an IPv4 address (d.d.d.d).
func parseIPv4(s string) IP {
var p [IPv4Len]byte
// ...
}
如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如:
const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d) // "1 1 2 2"
1)iota 常量生成器
在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday
Thursday
Friday
Saturday
)
也可以在复杂的常量表达式中使用iota。
type Flags uint
const (
FlagUp Flags = 1 << iota // 1 << 0
FlagBroadcast // 1 << 1
FlagLoopback // 1 << 2
FlagPointToPoint // 1 << 3
FlagMulticast // 1 << 4
)
三、复合数据类型
3.1 数组
数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置,内置的len函数将返回数组中元素的个数。
数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。默认情况下,数组的每个元素都被初始化为元素类型对应的零值。在数组字面值中,如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的个数来计算。
以下是顺序初始化值序列:
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"
p := [...]int{1, 2, 3}
fmt.Printf("%T\n", p) // "[3]int"
也可以指定一个索引和对应值列表的方式初始化:
type Currency int
const (
USD Currency = iota // 美元
EUR // 欧元
GBP // 英镑
RMB // 人民币
)
symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
fmt.Println(RMB, symbol[RMB]) // "3 ¥"
在这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值初始化。
// 定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其它元素都是用0初始化。
r := [...]int{99: -1}
如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。在这个方面,Go语言对待数组的方式和其它很多编程语言不同,其它编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数。
当然,我们可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以直接反馈到调用者。下面的函数用于给[32]byte类型的数组清零:
func zero(ptr *[32]byte) {
for i := range ptr {
ptr[i] = 0
}
}
3.2 Slice
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。
多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。图4.1显示了表示一年中每个月份名字的字符串数组,还有重叠引用了该数组的两个slice。
要注意的是slice类型的变量s和数组类型的变量a的初始化语法的差异。slice和数组的字面值语法很类似,它们都是用花括弧包含一系列的初始化元素,但是对于slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。就像数组字面值一样,slice的字面值也可以按顺序指定初始化值序列,或者是通过索引和元素值指定,或者用两种风格的混合语法初始化。
// 数组这样定义
months := [...]string{1: "January", /* ... */, 12: "December"}
// 两个slice
Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2) // ["April" "May" "June"]
fmt.Println(summer) // ["June" "July" "August"]
// 如果要定义slice,要按照下面方式
months := []string{1: "January", /* ... */, 12: "December"}
因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名。下面的reverse函数在原内存空间将[]int类型的slice反转,而且它可以用于任意长度的slice。
// reverse reverses 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之间不能直接使用==操作符来判断两个slice是否含有全部相等元素(但是可以与nil比较),不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较。
一个零值的slice等于nil。一个nil值的slice并没有底层数组,其长度和容量都是0。也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。
如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断
1)append函数
从下面appendInt函数可以理解append函数的底层工作原理。
func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
if zlen <= cap(x) {
z = x[:zlen]
} else {
zcap := zlen
if zcap < 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x)
}
z[len(x)] = y
return z
}
每次调用appendInt函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间,并返回slice。因此,输入的x和输出的z共享相同的底层数组。如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大(2*len(old_slice),这个扩容算法因不同语言不同)的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素。结果z和输入的x引用的将是不同的底层数组。
2)Slice内存技巧
返回值与参数重用一个slice,共享一个底层数组。下面的nonempty函数将在原有slice内存空间之上返回不包含空字符串的slice,这可以避免分配另一个数组,不过原来的数据将可能会被覆盖。
func nonempty(strings []string) []string {
i := 0
for _, s := range strings {
if s != "" {
strings[i] = s
i++
}
}
return strings[:i]
}
一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:
stack := []type{} // create stack
stack = append(stack, v) // push v
top := stack[len(stack)-1] // top of stack
stack = stack[:len(stack)-1] // pop
要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成:
func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 9 8]
如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:
func remove(slice []int, i int) []int {
slice[i] = slice[len(slice)-1]
return slice[:len(slice)-1]
}
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 9 8]