Golang入门语法教程,主要介绍与C、Java、Python等语言的异同。
Go官网下载地址:golang.org/dl
Go官方镜像站(推荐):golang.google.cn/dl
1 变量与常量
标识符命名规则:
- 变量名称必须由数字、字母、下划线组成。
- 标识符开头不能是数字。
- 标识符不能是保留字和关键字。
- 变量的名字区分大小写。
- 标识符一定要见名思意:变量名称建议用名词,方法名称建议用动词。
- 变量命名一般采用驼峰式,当遇到特有名词(缩写或简称,如DNS)时,特有名词根据是否私有全部大写或小写。
Go语言代码风格:
- 代码每一行结束后不用写分号(
;
) - 运算符左右建议各加一个空格(
var username string = "Asashio"
) - Go语言程序员推荐使用驼峰式命名:当名字有几个单词组成的时优先使用大小写分隔
- 强制的代码风格:左括号必须紧接着语句不换行。可使用
go fmt
格式化文档。
1.1 变量的声明与初始化
Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用。
var
声明变量:
var name string
var age int
var isOk bool
一次定义多个变量:
var identifier1, identifier2 type
批量声明变量时指定类型:
var (
a string
b int
c bool
)
a = "Akira"
b = 108
c = true
fmt.Println(a, b, c)
批量声明变量并赋值:
var (
a string = "Akira"
b int = 108
c bool = true
)
fmt.Println(a, b, c)
fmt.Println(a, b, c)
Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如:整型和浮点型变量的默认值为0
。 字符串变量的默认值为空字符串,布尔型变量默认为false
,切片、函数、指针变量的默认为nil
。
可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:
var name string = "Akira"
var age int = 108
一次初始化多个变量并赋值:
var name, age = "zhangsan", 20
类型推导:有时会将变量的类型省略,此时编译器会根据等号右边的值来推导变量的类型完成初始化。
var name = "Akira"
var age = 108
函数外的每个语句都必须以关键字开始(var、const、func 等)
短变量声明法:在函数内部,可以使用更简略的:=
方式声明并初始化变量。短变量只能用于声明局部变量,不能用于全局变量的声明(不能用于函数外)。
package main
import (
"fmt"
)
// 全局变量m
var m = 100
func main() {
n := 10
m := 200 // 此处声明局部变量m
fmt.Println(m, n)
}
使用变量一次声明多个变量,并初始化变量:
m1, m2, m3 := 10, 20, 30
fmt.Println(m1, m2, m3)
匿名变量(Anonymous Variable):在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量。 匿名变量用一个下划线表示。多用于占位,表示忽略值。匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。
func getInfo() (int, string) {
return 10, "张三"
}
func main() {
_, username := getInfo()
fmt.Println(username)
}
1.2 常量
使用const
定义常量:
const pi = 3.1415
const e = 2.7182
多个常量可以一起声明:
const (
pi = 3.1415
e = 2.7182
)
const
同时声明多个常量时,若省略了值则表示和上一行的值相同。 例如下例中,常量n1
、n2
、n3
的值都为100
:
const (
n1 = 100
n2
n3
)
1.3 iota常量计数器
iota
是go语言的常量计数器,只能在常量的表达式中使用。
iota
在const
关键字出现时将被重置为0
(const
内部的第一行之前), const
中每新增一行常量声明将使iota
计数1次(可理解为const
语句块中的行索引)。
每次const
出现时,都会让iota
初始化为0
(自增):
const a = iota // a=0
const (
b = iota //b=0
c //c=1
)
使用_
跳过某些值:
const (
n1 = iota //0
n2 //1
_
n4 //3
)
iota
声明中间插队:
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0
多个iota
定义在一行:
const (
a, b = iota + 1, iota + 2 //1, 2
c, d //2, 3
e, f //3, 4
)
1.4 fmt包、Print、Println、Printf
Go中要打印值需要引入fmt包:
import "fmt"
Print
、Println
、Printf
的区别类似java;可一次打印多个值:
fmt.Println("Hello Golang!", "Akira Hyplus Syma")
fmt.Print("Hello Golang!", "Akira Hyplus Syma")
fmt.Printf("Hello Golang!", "Akira Hyplus Syma")
注释:
/*
Commented
*/
// Also commented
2 数据类型
Go 语言中数据类型分为基本数据类型和复合数据类型
- 基本数据类型:整型、浮点型、布尔型、字符串
- 复合数据类型:数组、切片、结构体、函数、map、通道(Channel)、接口等
2.1 整型
整型根据有无符号和长度可分为以下两大类:
- 有符号整形:
int8
、int16
、int32
、int64
- 无符号整型:
uint8
、uint16
、uint32
、uint64
特殊整型:
uint
:32位操作系统上为uint32
,64位操作系统上为uint6
int
:32位操作系统上为int32
,64位操作系统上为int64
uintptr
:无符号整型,用于存放一个指针
注:实际项目中整数类型、切片、 map的元素数量等均可用int来表示。在涉及到二进制传输时,为了保持文件结构不受不同编译目标平台字节长度的影响,不要使用int和uint。
unsafe.Sizeof()
是unsafe
包的一个函数,可以返回变量占用的字节数:
package main
import (
"fmt"
"unsafe"
)
func main() {
var a int8 = 120
fmt.Printf("%T\n", a)
fmt.Println(unsafe.Sizeof(a))
}
int
不同长度直接转换:
package main
import (
"fmt"
)
func main() {
var num1 int8
num1 = 127
num2 := int32(num1)
fmt.Printf("值:%v,类型:%T", num2, num2) // 值:127,类型:int32
}
数字字面量语法(Number Literals Syntax):在Go1.13版本引入,便于开发者以二进制、八进制或十六进制浮点数的格式定义数字。例如v := 0b00101101
代表二进制的101101
,相当于十进制的45;v := 0o377
代表八进制的377
,相当于十进制的255;v := 0x1p-2
代表十六进制的1 / 2^2
,即0.25。
还可使用_
来分隔数字,例如v := 123_456
等于123456。
package main
import "fmt"
func main() {
// 十进制
var a int
a = 10
fmt.Printf("%d\n", a) // 10
fmt.Printf("%b\n", a) // 1010
// 八进制(以0开头)
var b int
b = 077
fmt.Printf("%o\n", b) // 77
// 十六进制(以0x开头)
var c int
c = 0xff
fmt.Printf("%x\n", c) // ff
fmt.Printf("%X\n", c) // FF
fmt.Printf("%d\n", c) // 255
}
2.2 浮点型
Go语言支持两种浮点型数:float32
和float64
。默认为float64
。
这两种浮点型数据格式遵循IEEE 754标准:float32
的最大范围约为3.4e38
,可以使用常量math.MaxFloat32
定义;float64
的最大范围约为1.8e308
,可以使用常量math.MaxFloat64
定义。
打印浮点数时,可以使用fmt包配合动词%f
来精确小数位数:
package main
import (
"fmt"
"math"
)
func main() {
fmt.Printf("%f\n", math.Pi) // 默认保留6位小数
fmt.Printf("%.2f\n", math.Pi) // 保留2位小数
}
float精度丢失问题:几乎所有的编程语言都会发生精度丢失,这是典型的二进制浮点数精度损失问题,在定长条件下,二进制小数和十进制小数互转可能有精度丢失。可使用使用第三方包(github.com/shopspring/decimal)来解决精度损失问题。
d := 1129.6
fmt.Println((d * 100)) // 打印结果为112959.99999999999
m1, m2 := 8.2, 3.8
fmt.Println(m1 - m2) // 期望为4.4,打印结果为4.399999999999999
可以使用科学计数法表示浮点类型:
num1 := 6.12345e2
num2 := 6.12345E2
num3 := 6.12345E-2
2.3 布尔值
Go语言中以bool
类型进行声明布尔型数据,布尔型数据只有true
(真)和false
(假)两个值。
- 布尔类型变量的默认值为
false
。 - 布尔型无法参与数值运算,也无法与其他类型进行转换。
package main
import (
"fmt"
"unsafe"
)
func main() {
var b = true
fmt.Println(b, "占用字节:", unsafe.Sizeof(b))
}
2.4 字符串
Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型一样。Go语言的字符串是不能直接修改的。
Go语言字符串的内部实现使用UTF-8编码,字符串的值为双引号("
)中的内容。可以在Go语言的源码中直接添加非ASCII码字符。
字符串占位符详见Go语言字符串占位符简介。
字符串转义符与Java等语言基本一致,如下所示:
\r
:回车符\n
:换行符\t
:制表符\'
:单引号('
)\"
:双引号("
)\\
:反斜杠(\
)
package main
import (
"fmt"
)
func main() {
fmt.Println("str := \"c:\\Code\\demo\\go.exe\"")
}
类似于JS,使用反引号字符(`)可以定义多行字符串。反引号间换行将被作为字符串中的换行,所有的转义字符均无效,文本将会原样输出。
s1 := `第1行
第2行
第3行
`
fmt.Println(s1)
字符串常用函数或运算:
len(str)
:获取字符串长度+
或fmt.Sprintf()
:拼接字符串(参考C语言sprintf()
)strings.Split()
:分割字符串strings.contains()
:判断是否包含strings.HasPrefix()
、strings.HasSuffix()
:前缀/后缀判断strings.Index()
、strings.LastIndex()
:子串出现的位置strings.Join(a []string, sep string)
:用指定分隔符拼接一系列字符串
var s0 = "this is str"
fmt.Println(len(s0))
var str1 = "Hello "
var str2 = "Golang"
fmt.Println(str1 + str2)
var str3 = fmt.Sprintf("%v %v", str1, str2)
fmt.Println(str3)
var tel = "123-456-789"
var arr1 = strings.Split(str, "-")
fmt.Println(arr1)
var str = "this is golang"
var flag = strings.Contains(str, "golang")
fmt.Println(flag)
var flag1 = strings.HasPrefix(str, "this")
var flag2 = strings.HasSuffix(str, "go")
fmt.Println(flag1, flag2)
var index1 = strings.Index(str, "is") // 从前往后
var index2 = strings.LastIndex(str, "is") // 从后往前
fmt.Println(index1, index2)
var str4 = "987-654-321"
var arr2 = strings.Split(str4, "-")
var str5 = strings.Join(arr2, "*")
fmt.Println(str5)
2.5 byte和rune
组成字符串的元素称为字符,可以通过遍历字符串获得字符,用单引号('
) 包裹。
package main
import "fmt"
func main() {
a := 'a'
b := '0'
// 当直接打印字符时,输出的是这个字符对应的码值
fmt.Println(a)
fmt.Println(b)
// 如果要输出这个字符, 需要格式化输出
fmt.Printf("%c--%c", a, b) // %c:相应Unicode码点所表示的字符
}
字节(
byte
)是计算机中数据处理的基本单位,常表示为大写B
,1B = 8bit
(位)一个汉字占用3字节,一个字母占用1字节
Go语言的字符有以下两种:
uint8
类型,或byte
型:代表ASCII码的一个字符。字符串底层是一个byte
数组,所以字符串的长度是byte
字节的长度,且可以和[]byte
类型相互转换。rune
类型:代表一个UTF-8字符。一个rune
字符由一个或多个byte
组成。
当需要处理中文、日文或其他复合字符时,需要用到rune
类型。Go使用了特殊的rune
类型来处理Unicode, 让基于Unicode的文本处理更为方便,也可使用byte
型进行默认字符串处理,性能和扩展性都有照顾。
遍历字符串示例如下,因为UTF-8编码中一个中文汉字由3个字节组成(rune
类型实为一个int32
),故不能简单按字节(byte
)去遍历包含中文的字符串,否则会出现如下输出中第一行的结果:
package main
import "fmt"
func main() {
s := "Hello 司马涛则"
for i := 0; i < len(s); i++ { // byte
fmt.Printf("%v(%c) ", s[i], s[i])
}
fmt.Println()
for _, r := range s { // rune
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()
}
72(H) 101(e) 108(l) 108(l) 111(o) 32( ) 229(å) 143() 184(¸) 233(é) 169(©) 172(¬) 230(æ) 182(¶) 155() 229(å) 136() 153()
72(H) 101(e) 108(l) 108(l) 111(o) 32( ) 21496(司) 39532(马) 28059(涛) 21017(则)
修改字符串:先将其转换成[]rune
或[]byte
,完成后再转换为string。无论哪种转换都会重新分配内存,并复制字节数组。
func changeString() {
s1 := "hyplus"
byteS1 := []byte(s1) // 强制类型转换
byteS1[0] = 'p'
fmt.Println(string(byteS1))
s2 := "KINA"
runeS2 := []rune(s2)
runeS2[0] = 'N'
fmt.Println(string(runeS2))
}
2.6 数据类型转换
Go语言中只有强制类型转换,没有隐式类型转换。
数值类型(number,整形和浮点型)之间的相互转换如下所示,转换时建议从低位转换成高位,高位转换成低位时若转换不成功则会溢出。注意在Go语言中数值类型无法和bool
类型进行转换。
var a, b = 3, 4
var c int
// math.Sqrt()接收的参数为float64类型,需要强制转换
c = int(math.Sqrt(float64(a * a + b * b)))
fmt.Println(c)
可以借助sprintf()
把其他类型转换成string
类型,相关字符串占位符详见Go语言字符串占位符简介:
var i int = 20
var f float64 = 12.456
var t bool = true
var b byte = 'a'
var strs string
strs = fmt.Sprintf("%d", i)
fmt.Printf("str type %T, strs = %v\n", strs, strs)
strs = fmt.Sprintf("%f", f)
fmt.Printf("str type %T, strs = %v\n", strs, strs)
strs = fmt.Sprintf("%t", t)
fmt.Printf("str type %T, strs = %v\n", strs, strs)
strs = fmt.Sprintf("%c", b)
fmt.Printf("str type %T, strs = %v\n", strs, strs)
str type string , strs = 20
str type string , strs = 12.456000
str type string , strs = true
str type string , strs = a
strconv
包提供了许多使string与其他类型相互转换的函数。可以使用该包中的Itoa
、FormatFloat
、FormatBool
、FormatInt
等函数进行转换:
package main
import (
"fmt"
"strconv"
)
func main() {
var num1 int = 20
s1 := strconv.Itoa(num1)
fmt.Printf("str type %T ,strs=%v \n", s1, s1)
var num2 float64 = 20.113123
/* strconv.FormatFloat()参数详解
参数1(f):带转换的值
参数2(fmt):格式化类型
'f'(-ddd.dddd)
'b'(-ddddp±ddd,指数为二进制)
'e'(-d.dddde±dd,十进制指数)
'E'(-d.ddddE±dd,十进制指数)
'g'(指数较大时为'e'格式,否则为'f'格式)
'G'(指数较大时为'E'格式,否则为'f'格式)。
参数3(prec): 小数保留精度,-1表示不对小数点格式化
参数4(bitSize):格式化类型
*/
s2 := strconv.FormatFloat(num2, 'f', 2, 64)
fmt.Printf("str type %T ,strs=%v \n", s2, s2)
s3 := strconv.FormatBool(true)
fmt.Printf("str type %T ,strs=%v \n", s3, s3)
var num3 int64 = 20
s4 := strconv.FormatInt(num3, 10) // 参数2(base)为进制
fmt.Printf(" 类型 %T ,strs=%v \n", s4, s4)
}
使用strconv
包中的ParseInt
、ParseFloat
、ParseBool
将string
类型转换成其他类型:
str1 := "1234"
i64, _ := strconv.ParseInt(str1, 10, 64) // 指定进制(base)和格式化类型(bitSize)
fmt.Printf(" 值:%v,类型:%T" , i64, i64)
str2 := "3.1415926535"
v1, _ := strconv.ParseFloat(str2, 32) // 指定格式化类型(bitSize)
v2, _ := strconv.ParseFloat(str2, 64)
fmt.Printf("值:%v,类型:%T\n", v1, v1)
fmt.Printf("值:%v,类型:%T", v2, v2)
b, _ := strconv.ParseBool("true") // 意义不大
fmt.Printf("值:%v,类型:%T", b, b)
通过遍历string
来获取单个字符(rune
类型):
s := "hello Akira"
for _, r := range s { // rune
fmt.Printf("%v(%c) ", r, r)
}
3 运算符
Go语言内置的运算符与Java等语言基本一致:
- 算术运算符:
+
、-
、*
、/
、%
- 关系运算符:
==
、!=
、>
、>=
、<
、<=
- 逻辑运算符:
&&
、||
、!
- 位运算符:
&
、|
、^
、<<
、>>
- 赋值运算符:
=
、+=
、-=
、*=
、/=
、%=
注:Go语言中仍存在自增(++
)和自减(--
),但只能独立使用,且不可前缀自增,只可写为i++
或i--
。
4 流程控制
此处主要解释Go的流程控制语法与其他语言的区别。
4.1 if else
与C语言的if
条件判断语句类似,但无需在条件表达式外加括号。
Go语言中规定与if
匹配的左括号{
必须与if
和条件表达式放在同一行,放在其他位置会触发编译错误。同理,与else if
或else
匹配的{
亦必须与其写在同一行。
score := 65
if score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
可以在if
表达式前添加执行语句,再进行判断。上例的另一种写法如下例所示,此时该变量位于局部作用域:
if score := 65; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
fmt.Println(score) // 报错 undefined: score
4.2 for
Go语言中的所有循环类型均可使用for
关键字来完成(没有while)。
基本使用方式类似Java等语言:
for i := 0; i < 10; i++ {
fmt.Println(i)
}
for
循环的初始语句可以省略,但其后的分号依旧要写(与Java等语言一样),如下所示:
i := 0
for ; i < 10; i++ {
fmt.Println(i)
}
for
循环的初始语句和结束语句均可省略,且无需写分号(此时形同其他语言的while),如下所示:
i := 0
for i < 10 {
fmt.Println(i)
i++
}
还可直接省略条件表达式,实现无限循环。可通过break
、goto
、return
、panic
语句强制退出循环:
k := 1
for {
if k <= 10 {
fmt.Println("ok", k)
} else {
break
}
k++
}
Go语言中还可以使用for range
遍历数组、切片、字符串、map、通道(channel),实现键值循环。通过for range
遍历的返回值有以下规则:
- 数组、切片、字符串返回索引和值。
- map返回键和值。
- 通道(channel)只返回通道内的值。
str := "Hyplus Rising"
for index, val := range str {
fmt.Printf("index=%d, val=%c\n", index, val)
}
for _, val := range str {
fmt.Printf("val=%c\n", val)
}
4.3 switch case
使用switch
语句可方便地对大量值进行条件判断。
【例】判断文件类型:若后缀名为.html
,输出text/html
;若为.css
,输出text/css
;若为.js
,输出text/javascript
Go语言规定每个switch
只能有一个default
分支。与其他语言不同,Go语言中每个case
语句中可以不写break
,不加break
不会出现穿透现象:
extname := ".a"
switch extname {
case ".html":
fmt.Println("text/html")
case ".css":
fmt.Println("text/css")
case ".js":
fmt.Println("text/javascript")
default:
fmt.Println("格式错误")
}
一个分支可以有多个值,多个case
值中间使用英文逗号分隔:
n := 2
switch n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
与if
语句类似,可以在switch
语句的判断变量之前添加执行语句,再执行判断。上例的另一种写法如下例所示,此时该变量位于局部作用域:
switch n := 2; n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
分支还可使用表达式,此时switch
之后无需再跟判断变量。该写法与其他编程语言差异较大,如下例所示:
level := 117
switch {
case level < 65:
fmt.Println("git gud!")
case level >= 65 && level < 85:
fmt.Println("r u stuck?")
case level >= 110:
fmt.Println("noice tri!")
default:
fmt.Println("Gooooo~")
}
fallthrough
可以执行满足条件的case
的下一个case
,实现C语言中switch
的穿透。
s := "a"
switch {
case s == "a":
fmt.Println("AAAAAAAHHHHHH!!!")
fallthrough // 默认只能穿透一层
case s == "b":
fmt.Println("Bingo~")
fallthrough
case s == "c":
fmt.Println("Clowned\\hj")
default:
fmt.Println(",,,")
}
4.4 break、continue、goto
Go语言中break
语句用于以下几个方面:
- 用于循环语句中跳出循环,并开始执行循环之后的语句。(与其他语言相同)
- 在
switch
中在执行一条case
后跳出语句的作用。【可以省略】 - 特殊用法:在多重循环中,可以用标号
label
标出想break
的循环。如下例所示——
package main
import "fmt"
func main() {
lable2:
for i := 0; i < 2; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
break lable2
}
fmt.Printf("i=%v, j=%v\n", i, j)
}
}
}
continue
语句仅限在for
循环内使用,可以结束当前循环,开始下一次的循环迭代过程(与其他语言相同)。在Go语言中有与break
语句类似的特殊用法,在continue
语句后添加标签时,表示开始标签对应的循环,如下例所示:
package main
import "fmt"
func main() {
here:
for i := 0; i < 2; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
continue here
}
fmt.Printf("i=%v, j=%v\n", i, j)
}
}
}
goto
语句通过标签进行代码间的无条件跳转,可以在快速跳出循环、避免重复退出上有一定的帮助,能简化一些代码的实现过程。(与其他语言完全相同)
5 Array(数组)
在Go语言中,数组(Array)是一个长度固定的数据类型,数组的长度是类型的一部分,因此[5]int
与[10]int
是两个不同的类型。Go语言中数组的另一个特点是占用内存的连续性,数组中的元素被分配到连续的内存地址中,因而索引数组元素的速度非常快。
基本语法:
var a [7]int
var b [3]int
b[0] = 80
b[1] = 100
b[2] = 96
5.1 数组的初始化
初始化数组时可以使用初始化列表来设置数组元素的值,此时可以使用...
让编译器根据初始值的个数自行推断数组的长度:
var testArray [3]int // 默认初始化为int类型的零值
var numArray = [3]int{1, 2} // 使用指定的初始值完成初始化
var cityArray = [...]string{"北京", "上海", "杭州"} // 编译器自动推断数组长度
fmt.Println(testArray) // [0 0 0]
fmt.Println(numArray) // [1 2 0]
fmt.Println(cityArray) // [北京 上海 杭州]
fmt.Printf("Type of cityArray: %T\n", cityArray) // Type of cityArray: [3]string
还可以使用指定索引值的方式来初始化数组:
a := [...]int{1: 1, 3: 5}
fmt.Println(a) // [0 1 0 5]
fmt.Printf("Type of a: %T\n", a) // Type of a: [4]int
5.2 数组的遍历
遍历数组有两种方式:for
循环与for range
键值循环
var a = [...]string{"北京", "上海", "杭州"}
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
for index, value := range a {
fmt.Println(index, value)
}
5.3 多维数组
Go语言同样支持多维数组,注意只有第1层可以使用...
来让编译器推导数组长度:
a := [...][2]string{
{"北京" , "上海" },
{"广州" , "深圳" },
{"成都" , "杭州" },
}
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}
数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
func modifyArray(x [3]int) {
x[0] = 100
}
func modifyArray2(x [3][2]int) {
x[2][0] = 100
}
func main() {
a := [3]int{10, 20, 30}
modifyArray(a) // 在函数中修改的是a的副本x
fmt.Println(a) // [10 20 30]
b := [...][2]int{
{1, 1}, {1, 1}, {1, 1},
}
modifyArray2(b) // 在函数中修改的是b的副本x
fmt.Println(b) // [[1 1] [1 1] [1 1]]
}
注:
- 数组支持
==
、!=
操作符,因为内存总是被初始化过的。 [n]*T
表示指针数组,*[n]T
表示数组指针。
6 Slice(切片)
切片(Slice)是一个拥有相同类型元素的可变长度的序列,是基于数组类型的一层封装,支持自动扩容。
切片是引用类型,内部结构包括地址、长度和容量。
6.1 切片的初始化与再切片
切片的声明方式与数组非常相似,只需不指定长度:
var a []int // 声明但未初始化
var b []float64{} // 声明并初始化
var c []string{"Akira", "KINA"}
fmt.Println(a == nil) // true
fmt.Println(b == nil) // false
fmt.Println(b == nil) // false
当声明了一个变量但还未赋值(未初始化)时,Go语言会自动给变量赋默认零值,以下每种类型对应的零值:
- bool:
false
- number:
0
- string:
""
- pointer:
nil
- slice:
nil
- map:
nil
- channel:
nil
- function:
nil
- interface:
nil
因此切片唯一合法的比较操作是和nil
比较。
可以基于数组定义切片,方式类似Python中的切片:
a := [5]int{55, 56, 57, 58, 59}
b := a[1:4] // 基于数组a创建切片,包含元素a[1], a[2], a[3]
fmt.Println(b) // [56 57 58]
fmt.Printf("Type of b: %T\n", b) // Type of b: []int
// 还支持如下方式
c := a[1:] // [56 57 58 59]
d := a[:4] // [55 56 57 58]
e := a[:] // [55 56 57 58 59]
还可以通过切片来得到切片,即再切片(Reslice),注意索引不能超过原数组的长度,否则会出现索引越界的错误:
a := []string{"北京", "上海", "广州", "深圳", "成都", "杭州"}
b := a[1:3]
c := b[1:5]
切片的循环遍历和数组完全一致。
6.2 切片的长度和容量
切片的本质是对底层数组的封装,包含了三个信息:底层数组的指针、切片的长度、切片的容量。
切片的长度(Length)即为它所包含的元素个数。切片的容量(Capacity)为从其第1个元素开始到其底层数组元素末尾的个数。
可以通过使用内置的len()
函数求长度,使用cap()
函数求容量。
a := [...]string{"北京", "上海", "广州", "深圳", "成都", "杭州"}
fmt.Printf("a: %v, type:%T, len:%d, cap:%d\n", a, a, len(a), cap(a)) // a: [北京 上海 广州 深圳 成都 杭州], type:[6]string, len:6, cap:6
b := a[1:3]
fmt.Printf("b: %v, type:%T, len:%d, cap:%d\n", b, b, len(b), cap(b)) // b: [上海 广州], type:[]string, len:2, cap:5
c := b[1:5]
fmt.Printf("c: %v, type:%T, len:%d, cap:%d\n", c, c, len(c), cap(c)) // c: [广州 深圳 成都 杭州], type:[]string, len:4, cap:4
6.3 make()动态构造切片
上述所有案例均为基于数组来创建切片。可以使用内置的make()
函数动态创建切片,其中T
为切片的元素类型,size
为切片中元素的数量,cap
为切片的容量:
make([]T, size, cap)
下例中a
的内部存储空间已经分配了10个,但实际只用了2个。容量并不会影响当前元素的个数,故len(a)
返回2,cap(a)
返回该切片的容量:
a := make([]int, 2, 10)
fmt.Println(a) // [0 0]
fmt.Println(len(a)) // 2
fmt.Println(cap(a)) // 10
注意切片的赋值拷贝:切片是引用数据类型,拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容。
6.4 append()添加元素
Go语言的内建函数append()
可以为切片动态地在末尾添加元素(尾插)。每个切片会指向一个底层数组,该数组的容量够用则添加新增元素。
当底层数组不能容纳新增的元素时,切片会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组会更换。“扩容”操作往往发生在append()
函数调用时,故通常都需要用原变量接收append()
函数的返回值:
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("numSlice: %v, len: %d, cap: %d, ptr: %p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
}
append()
函数支持一次性追加多个元素,还可直接追加数组或切片,只需在其后添加...
:
var citySlice []string
citySlice = append(citySlice, "北京") // 追加一个元素
citySlice = append(citySlice, "上海" , "深圳" , "杭州") // 追加多个元素
a := []string{"成都" , "重庆"}
citySlice = append(citySlice, a...) // 追加切片
fmt.Println(citySlice) // [北京 上海 深圳 杭州 成都 重庆]
s1 := []int{100, 200, 300}
s2 := []int{400, 500, 600}
s3 := append(s1, s2...)
fmt.Println(s3) // [100 200 300 400 500 600]
通过查看源码($GOROOT/src/runtime/slice.go
)可知切片的扩容策略:
- 首先判断——如果新申请容量(
cap
)大于2倍的旧容量(old.cap
),最终容量(newcap
) 即为新申请的容量。 - 否则判断——如果旧切片的长度小于1024,则最终容量(
newcap
)为旧容量(old.cap
)的2倍,即newcap = doublecap
。 - 否则判断)如果旧切片的长度大于等于1024,则最终容量(
newcap
)从旧容量(old.cap
)开始循环增加原来的1/4
,直到最终容量大于等于新申请的容量(cap
)
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,例如int
和string
类型的处理方式就不一样。
6.5 copy()复制切片
注意切片是引用类型。Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,其中srcSlice
为数据来源切片,destSlice
为目标切片:
copy(destSlice, srcSlice []T)
如下例所示:
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) // 使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) // [1 2 3 4 5]
fmt.Println(c) // [1 2 3 4 5]
c[0] = 1000
fmt.Println(a) // [1 2 3 4 5]
fmt.Println(c) // [1000 2 3 4 5]
6.6 从切片中删除元素
Go语言中并没有删除切片元素的专用方法,可以使用切片自身的特性来删除元素:要从切片a
中删除索引为index
的元素,操作方法为a = append(a[:index], a[index+1:]...)
如下例所示:
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
a = append(a[:2], a[3:]...) // 删除索引为2的元素
fmt.Println(a) // [30 31 33 34 35 36 37]
6.7 sort包:对数组和切片进行排序
sort包(文档:golang.org/src/sort)对于int
、float64
和string
数组或切片分别提供了sort.Ints()
、sort.Float64s()
和sort.Strings()
排序函数, 默认升序排序。
intList := []int{2, 4, 3, 5, 7, 6, 9, 8, 1, 0}
float64List := []float64{4.2, 5.9, 12.4, 10.2, 50.7, 99.9, 31.4, 27.81828, 3.14}
stringList := []string{"a", "c", "b", "z", "x", "w", "y", "d", "f", "i"}
sort.Ints(intList)
sort.Float64s(float64List)
sort.Strings(stringList)
fmt.Println(intList) // [0 1 2 3 4 5 6 7 8 9]
fmt.Println(float64List) // [3.14 4.2 5.9 10.2 12.4 27.81828 31.4 50.7 99.9]
fmt.Println(stringList) // [a b c d f i w x y z]
要实现降序排序,可以使用sort.Reverse(slice)
来调换slice.Interface.Less
,即比较函数。因此int
、float64
和string
的逆序排序函数如下例所示:
intList := []int{2, 4, 3, 5, 7, 6, 9, 8, 1, 0}
float64List := []float64{4.2, 5.9, 12.4, 10.2, 50.7, 99.9, 31.4, 27.81828, 3.14}
stringList := []string{"a", "c", "b", "z", "x", "w", "y", "d", "f", "i"}
sort.Sort(sort.Reverse(sort.IntSlice(intList))) sort.Sort(sort.Reverse(sort.Float64Slice(float8List))) sort.Sort(sort.Reverse(sort.StringSlice(stringList)))
fmt.Println(intList) // [9 8 7 6 5 4 3 2 1 0]
fmt.Println(float64List) // [99.9 50.7 31.4 27.81828 12.4 10.2 5.9 4.2 3.14]
fmt.Println(stringList) // [z y x w i f d c b a]