TOC
CHAT

Go语言入门教程:基本语法

Golang入门语法教程,主要介绍与C、Java、Python等语言的异同。

Go官网下载地址:golang.org/dl

Go官方镜像站(推荐):golang.google.cn/dl

1 变量与常量

标识符命名规则:

  1. 变量名称必须由数字、字母、下划线组成。
  2. 标识符开头不能是数字。
  3. 标识符不能是保留字和关键字。
  4. 变量的名字区分大小写。
  5. 标识符一定要见名思意:变量名称建议用名词,方法名称建议用动词。
  6. 变量命名一般采用驼峰式,当遇到特有名词(缩写或简称,如DNS)时,特有名词根据是否私有全部大写或小写。

Go语言代码风格:

  1. 代码每一行结束后不用写分号(;
  2. 运算符左右建议各加一个空格(var username string = "Asashio"
  3. Go语言程序员推荐使用驼峰式命名:当名字有几个单词组成的时优先使用大小写分隔
  4. 强制的代码风格:左括号必须紧接着语句不换行。可使用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同时声明多个常量时,若省略了值则表示和上一行的值相同。 例如下例中,常量n1n2n3的值都为100

const (
    n1 = 100
    n2
    n3
)

1.3 iota常量计数器

iota是go语言的常量计数器,只能在常量的表达式中使用。

iotaconst关键字出现时将被重置为0const内部的第一行之前), 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"

PrintPrintlnPrintf的区别类似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 整型

整型根据有无符号和长度可分为以下两大类:

  • 有符号整形:int8int16int32int64
  • 无符号整型:uint8uint16uint32uint64

特殊整型:

  • 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语言支持两种浮点型数:float32float64默认为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)是计算机中数据处理的基本单位,常表示为大写B1B = 8bit(位)

一个汉字占用3字节,一个字母占用1字节

Go语言的字符有以下两种:

  1. uint8类型,或byte型:代表ASCII码的一个字符。字符串底层是一个byte数组,所以字符串的长度是byte字节的长度,且可以和[]byte类型相互转换。
  2. 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与其他类型相互转换的函数。可以使用该包中的ItoaFormatFloatFormatBoolFormatInt等函数进行转换:

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包中的ParseIntParseFloatParseBoolstring类型转换成其他类型:

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等语言基本一致:

  1. 算术运算符:+-*/%
  2. 关系运算符:==!=>>=<<=
  3. 逻辑运算符:&&||!
  4. 位运算符:&|^<<>>
  5. 赋值运算符:=+=-=*=/=%=

注:Go语言中仍存在自增(++)和自减(--),但只能独立使用,且不可前缀自增,只可写为i++i--


4 流程控制

此处主要解释Go的流程控制语法与其他语言的区别。

4.1 if else

与C语言的if条件判断语句类似,但无需在条件表达式外加括号。

Go语言中规定与if匹配的左括号{必须与if和条件表达式放在同一行,放在其他位置会触发编译错误。同理,与else ifelse匹配的{亦必须与其写在同一行。

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++
}

还可直接省略条件表达式,实现无限循环。可通过breakgotoreturnpanic语句强制退出循环:

k := 1
for {
    if k <= 10 {
        fmt.Println("ok", k)
    } else {
        break
    }
    k++
}

Go语言中还可以使用for range遍历数组、切片、字符串、map、通道(channel),实现键值循环。通过for range遍历的返回值有以下规则:

  1. 数组、切片、字符串返回索引
  2. map返回
  3. 通道(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语句用于以下几个方面:

  1. 用于循环语句中跳出循环,并开始执行循环之后的语句。(与其他语言相同)
  2. switch中在执行一条case后跳出语句的作用。【可以省略】
  3. 特殊用法:在多重循环中,可以用标号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]]
}

注:

  1. 数组支持==!=操作符,因为内存总是被初始化过的。
  2. [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)可知切片的扩容策略:

  1. 首先判断——如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap) 即为新申请的容量。
  2. 否则判断——如果旧切片的长度小于1024,则最终容量(newcap)为旧容量(old.cap)的2倍,即newcap = doublecap
  3. 否则判断)如果旧切片的长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,直到最终容量大于等于新申请的容量(cap

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,例如intstring类型的处理方式就不一样。

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)对于intfloat64string数组或切片分别提供了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,即比较函数。因此intfloat64string的逆序排序函数如下例所示:

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]

7 map

发表评论