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

使用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.2 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.3 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语言中的数组是值类型。Go语言中数组的另一个特点是占用内存的连续性,数组中的元素被分配到连续的内存地址中,因而索引数组元素的速度非常快。

基本语法:

var a [7]int

var b [3]int
b[0] = 80
b[1] = 100
b[2] = 96

初始化数组时可以使用初始化列表来设置数组元素的值,此时可以使用...让编译器根据初始值的个数自行推断数组的长度:

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

遍历数组有两种方式: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)
}

Go语言同样支持多维数组,注意只有第1层可以使用...来让编译器推导数组长度:

a := [...][2]string{
    {"北京" , "上海"},
    {"广州" , "深圳"},
    {"成都" , "杭州"},
}
for _, v1 := range a {
    for _, v2 := range v1 {
        fmt.Printf("%s\t", v2)
    }
    fmt.Println()
}

在Go语言中,数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。

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()函数动态创建切片(以及map、channel),其中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

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

7.1 Map的初始化与基本使用

Go语言中map的定义语法如下,其中KeyType为键的类型,ValueType为键对应的值的类型:

map[KeyType]ValueType

map类型的变量默认初始值为nil,需要使用make()函数来分配内存,其中可选参数capmap对应的底层数组切片的容量(注意cap()返回的是数组切片分配的空间大小,要获取map的容量应使用len()函数):

make(map[KeyType]ValueType, [cap])

map中的数据都是成对出现的。当声明了一个变量但还未赋值(未初始化)时,Go语言会自动给变量赋默认零值(详见6.1)。

基本使用与其他语言中类似,示例如下:

scoreMap := make(map[string]int, 8)
scoreMap["Teresa"] = 90
scoreMap["Akira"] = 100
fmt.Println(scoreMap)   // map[Akira:100 Teresa:90]
fmt.Println(scoreMap["Akira"])    // 100
fmt.Printf("Type of a: %T\n", scoreMap)   // Type of a: map[string]int

map亦支持在声明时填充元素:

userInfo := map[string]string{
    "username": "hyperplasma",
    "password": "11223344",
}
fmt.Println(userInfo)

7.2 判断某个键是否存在

Go语言中的map索引存在第2个返回值(为bool类型),可用于判断某个键是否存在:

value, ok := mp[key]

如下例所示:

scoreMap := make(map[string]int)
scoreMap["Teresa"] = 90
scoreMap["Akira"] = 100

v, ok := scoreMap["Akira"]
if ok {
    fmt.Println(v)
} else {
    fmt.Println("查无此人")
}

7.3 Map的遍历

Go语言中使用for range遍历map

scoreMap := make(map[string]int)
scoreMap["Teresa"] = 90
scoreMap["Akira"] = 100
scoreMap["KINA"] = 85

for k, v := range scoreMap {
    fmt.Println(k, v)
}

// 只遍历key
for k := range scoreMap {
    fmt.Println(k)
}

map自身是无序的(哈希表),若想按顺序遍历map,可以先取出map中的所有key存入切片,对切片进行排序后,按照排序后的key遍历map

rand.Seed(time.Now().UnixNano())    // 初始化随机数种子

var scoreMap = make(map[string]int, 200)
// 随机生成数据
for i := 0; i < 100; i++ {
    key := fmt.Sprintf("stu%02d", i)
    value := rand.Intn(100)
    scoreMap[key] = value
}

// 取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
    keys = append(keys, key)
}
// 对切片进行排序
sort.Strings(keys)
// 按照排序后的key遍历map
for _, key := range keys {
    fmt.Println(key, scoreMap[key])
}

7.4 delete()删除键值对

使用内建函数delete()map中删除一组键值对,格式如下,其中mp为要删除键值对的map对象,key为待删键值对的键:

delete(mp, key)

如下例所示:

scoreMap := make(map[string]int)
scoreMap["Teresa"] = 90
scoreMap["Akira"] = 100
scoreMap["KINA"] = 85

delete(scoreMap, "KINA")

for k, v := scoreMap {
    fmt.Println(k, v)
}

7.5 Map类型切片与值为切片的Map

如下例所示,切片中的元素可为map类型:

var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
    fmt.Printf("index: %d, value: %v\n", index, value)
}
fmt.Println("")

// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "hyperplasma"
mapSlice[0]["password"] = "11223344"
mapSlice[0]["address"] = "拱墅区"
for index, value := range mapSlice {
    fmt.Printf("index: %d, value: %v\n", index, value)
}

如下所示为值为切片类型的map

var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)

key := "China"
value, ok := sliceMap[key]
if !ok {
    value = make([]string, 0, 2)
}
value = append(value, "Beijing" , "Shanghai" )
sliceMap[key] = value
fmt.Println(sliceMap)

8 函数

函数(Function)是组织好的、可重复使用的、用于执行指定任务的代码块。Go语言中的函数与其他语言非常相似,但又存在诸多重大变化。

8.1 函数的定义与调用

Go语言中支持函数、匿名函数和闭包。

Go语言中定义函数使用func关键字,具体要求如下:

  • 函数名:由字母、数字、下划线组成。与变量名一样,函数名的第一个字母不能为数字。在同一个包内,函数不能重名。
  • 参数:由参数变量及其类型组成,多个参数之间使用,分隔。(可选)
  • 返回值:由返回值变量及其类型组成,亦可只写返回值的类型,多个返回值必须用()包裹,并用,分隔。(可选)
  • 函数体:实现指定功能的代码块。
func intSum(x int, y int) int {
    return x + y
}

与其他语言相同,可以通过函数名()的方式调用函数:

ret := intSum(4, 5)
fmt.Println(ret)

全局变量是定义在函数外部的变量,在程序整个运行周期内都有效。在函数中可以访问到全局变量。

局部变量是函数等语句块内部定义的变量,无法在作用域之外使用。如果局部变量和全局变量重名,优先访问局部变量

8.2 函数的参数与返回值

函数的参数中如果相邻变量的类型相同,则可以省略类型。例如上例中的函数可简写如下:

func intSum(x, y int) int {
    return x + y
}

Go语言中的可变参数通过在参数名后加...来标识(本质上通过切片来实现)。固定参数搭配可变参数使用时,可变参数要放在固定参数的后面:

func intSum2(x int, y ...int) int {
    fmt.Println(x, y)   // y为一个切片
    sum := x
    for _, v := range y {
        sum += v
    }
    return sum
}

ret1 := intSum2(100)
ret2 := intSum2(100, 10)
ret3 := intSum2(100, 10, 20)
ret4 := intSum2(100, 10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4) // 100 110 130 160

Go语言中通过return关键字向外输出返回值,支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。

func calc(x, y int) (int, int) {
    sum := x + y
    sub := x - y
    return sum, sub
}

函数定义时可以给返回值命名,并在函数体中直接使用这些变量。用于返回的return语句可不带参数,或者直接省略:

func calc(x, y int) (sum, sub int) {
    sum = x + y
    sub = x - y
    // return
}

8.3 函数类型与变量

可以使用type关键字来定义函数类型,凡是满足所定义条件的函数都是该类型的函数:

type calculation func(int, int) int

可以声明函数类型的变量并且为该变量赋值:

package main

type calculation func(int, int) int

func add(x, y int) int {
    return x + y
}

func sub(x, y int) int {
    return x - y
}

func main() {
    var c calculation   // 声明一个calculation类型的变量c
    c = add // 把add赋值给c
    fmt.Printf("Type of c: %T\n", c)  // Type of c: main.calculation
    fmt.Println(c(1, 2))    // 像调用add一样调用c

    f := add    // 将函数add赋值给变量f
    fmt.Printf("Type of f: %T\n", f)  // Type of f: func(int, int) int
    fmt.Println(f(10, 20))  // 像调用add一样调用f
}

8.4 高阶函数

高阶函数分为函数作为参数和函数作为返回值两部分。

函数可以作为参数:

func add(x, y int) int {
    return x + y
}

func calc(x, y int, op func(int, int) int) int {
    return op(x, y)
}

func main() {
    ret := calc(10, 20, add)
    fmt.Println(ret)    // 30
}

函数也可以作为返回值:

package main

import (
    "fmt"
)

func add(x, y int) int {
    return x + y
}

func sub(x, y int) int {
    return x - y
}

func do(s string) func(int, int) int {
    switch s {
        case "+":
            return add
        case "-":
            return sub
        default:
            return nil
    }
}

func main() {
    a := do("+")
    fmt.Println(a(10, 20))
}

8.5 匿名函数和闭包

结合8.4高阶函数来深入理解本小节

匿名函数即为无函数名的函数,多用于实现回调函数和闭包,需要保存到某个变量或作为自执行函数:

// 将匿名函数保存到变量
add := func(x, y int) {
    fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数

// 自执行函数:匿名函数定义后加"()"直接执行
func(x, y int) {
    fmt.Println(x + y)
}(10, 20)

闭包(Closure)可理解为“定义在一个函数内部的函数”,本质上是将函数内部和函数外部连接起来的桥梁,或者说是函数和其引用环境的组合体(闭包 = 函数 + 引用环境)。

如下例所示,变量f是一个函数且引用了其外部作用域中的x变量,此时f即为一个闭包,在f的生命周期内,变量x一直有效:

func adder() func(int) int {
    var x int
    return func(y int) int {
        x += y
        return x
    }
}

func main() {
    var f = adder()
    fmt.Println(f(10))  // 10
    fmt.Println(f(20))  // 30
    fmt.Println(f(30))  // 60

    f1 := adder()
    fmt.Println(f1(40)) // 40
    fmt.Println(f1(50)) // 90
}

闭包进阶示例1:

func adder2(x int) func(int) int {
    return func(y int) int {
        x += y
        return x
    }
}

func main() {
    var f = adder2(10)
    fmt.Println(f(10))  // 20
    fmt.Println(f(20))  // 40
    fmt.Println(f(30))  // 70

    f1 := adder2(20)
    fmt.Println(f1(40)) // 60
    fmt.Println(f1(50)) // 110
}

闭包进阶示例2:

func makeSuffixFunc(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
}

func main() {
    jpgFunc := makeSuffixFunc(".jpg")
    txtFunc := makeSuffixFunc(".txt")
    fmt.Println(jpgFunc("test"))  // test.jpg
    fmt.Println(txtFunc("test"))  // test.txt
}

闭包进阶示例3(注意f1f2共用引用环境):

func calc(base int) (func(int) int, func(int) int) {
    add := func(i int) int {
        base += i
        return base
    }

    sub := func(i int) int {
        base -= i
        return base
    }
    return add, sub
}

func main() {
    f1, f2 := calc(10)
    fmt.Println(f1(1), f2(2))   // 11 9
    fmt.Println(f1(3), f2(4))   // 12 8
    fmt.Println(f1(5), f2(6))   // 13 7
}

8.6 defer语句

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行(FILO)——先被defer的语句最后被执行,最后被defer的语句最先被执行:

func main() {
    fmt.Println("start")
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
    fmt.Println("end")
}
start
end
3
2
1

由于defer语句延迟调用的特性,defer能非常方便地处理资源释放问题,例如资源清理、文件关闭、解锁及记录时间等。

defer执行时机:在Go语言的函数中return语句在底层并不是原子操作,其分为两步——返回值赋值、RET指令(子程序的返回指令)。defer语句执行的时机就在返回值赋值操作后、RET指令执行前

defer经典案例1:

func f1() int {
    x := 5
    defer func() {
        x++
    }()
    return x
}

func f2() (x int) {
    defer func() {
        x++
    }()
    return 5
}

func f3() (y int) {
    defer func() {
        x++
    }()
    return x
}

func f4() (x int) {
    defer func(x int) {
        x++
    }(x)
    return 5
}

func main() {
    fmt.Println(f1())   // 5
    fmt.Println(f2())   // 6
    fmt.Println(f3())   // 5
    fmt.Println(f4())   // 5
}

defer经典案例2:

func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
}

func main() {
    x := 1
    y := 2
    defer calc("AA", x, calc("A", x, y))
    x = 10
    defer calc("BB", x, calc("B", x, y))
    y = 20
}
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4

8.7 内置函数

Go语言存在诸多内置函数:

  • close():主要用于关闭channel
  • len():求长度,例如stringarrayslicemapchannel
  • new():用于分配值类型,例如intstruct,返回值为指针
  • make():用于分配引用类型,例如chanmapslice
  • append():用于追加元素到数组、slice
  • delete():用于删除map中的键值对
  • panic()recover():用于错误处理

Go语言没有Java、C++等语言中常见的异常机制,但可使用panic/recover模式来处理错误。panic()可以在任何地方引发;recover()必须搭配defer使用,defer一定要在可能引发panic的语句之前定义。

panic/recover基本使用例如下所示,程序运行期间funcB中引发了panic导致程序崩溃,异常退出。此时可以通过recover将程序恢复回来,继续往后执行:

func funcA() {
    fmt.Println("func A")
}

func funcB() {
    defer func() {
        err := recover()    // 如果程序出现了panic错误 , 可以通过recover恢复过来
        if err != nil {
            fmt.Println("recover in B")
        }
    }()
    panic("panic in B")
}

func funcC() {
    fmt.Println("func C")
}

func main() {
    funcA()
    funcB()
    funcC()
}

deferrecover实现异常处理:

func fn2() {
    defer func() {
        err := recover()
        if err != nil {
            fmt.Println("抛出异常给管理员发送邮件")
            fmt.Println(err)
        }
    }()

    num1 := 10
    num2 := 0
    res := num1 / num2
    fmt.Println("res = ", res)
}

deferpanicrecover抛出异常:

func readFile(fileName string) error {
    if fileName == "main.go" {
        return nil
    }
    return errors.New("读取文件错误")
}

func fn3() {
    defer func() {
        err := recover()
        if err != nil {
            fmt.Println("抛出异常给管理员发送邮件")
        }
    }()

    var err = readFile("xxx.go")
    if err != nil {
        panic(err)
    }
    fmt.Println(" 继续执行" )
}

func main() {
    fn3()
}

9 time包与日期函数

Go语言中的time包提供了用于时间显示和测量的函数。

9.1 time.Now()获取当前时间

可以通过time.Now()函数获取当前的时间对象(Time类)。

时间对象的Year()Month()等方法可以获取单独的年月日时分秒等信息:

now := time.Now()   //获取当前时间
fmt.Printf("Current time: %v\n", now)

year := now.Year()  // 年
month := now.Month()    // 月
day := now.Day()    // 日
hour := now.Hour()  // 时
minute := now.Minute()  // 分钟
second := now.Second()  // 秒
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)

时间对象的Format()方法可以格式化输出日期字符串,格式化模板必须为Go的出生时间——2006年1月2日15点04分Mon Jan

now := time.Now()

// 24 小时制
fmt.Println(now.Format("2006-01-02 15:04:05"))

// 12 小时制
fmt.Println(now.Format("2006-01-02 03:04:05"))
fmt.Println(now.Format("2006/01/02 15:04"))
fmt.Println(now.Format("15:04 2006/01/02"))
fmt.Println(now.Format("2006/01/02"))

9.2 时间戳(Unix Timestamp)

时间对象的Unix()UnixNano()方法可以获取当前的时间戳或纳秒时间戳:

now := time.Now()
timestamp1 := now.Unix()    // 时间戳
timestamp2 := now.UnixNano()    // 纳秒时间戳
fmt.Printf("Current timestamp1: %v\n", timestamp1)
fmt.Printf("Current timestamp2: %v\n", timestamp2)

使用time.Unix()函数可以将时间戳转为时间对象,继而可以进行格式化等操作:

func unixToTime(timestamp int64) {
    timeObj := time.Unix(timestamp, 0)  // 将时间戳转为时间格式
    year := timeObj.Year()  // 年
    month := timeObj.Month()    // 月
    day := timeObj.Day()    // 日
    hour := timeObj.Hour()  // 时
    minute := timeObj.Minute()  // 分
    second := timeObj.Second()  // 秒
    fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
    fmt.Println(timeObj.Format("2006-01-02 03:04:05"))
}

func main() {
    unixToTime(1587880013)
}

使用time.ParseInLocation()函数可以将日期字符串转换成时间戳,所需参数为格式化模板(见前述)、日期字符串、地区(本地为time.Local):

t1 := "2025-03-08 18:20:50"
timeTemplate := "2006-01-02 15:04:05"

stamp, _ := time.ParseInLocation(timeTemplate, t1, time.Local)

fmt.Println(stamp.Unix())   // 1741429250

9.3 时间间隔

time.Durationtime包定义的一个类型,表示两个时间点之间经过的时间(一段时间间隔),以纳秒为单位,可表示的最长时间段约为290年。

time包中定义的时间间隔类型的常量如下:

const (
    Nanosecond Duration = 1
    Microsecond         = 1000 * Nanosecond
    Millisecond         = 1000 * Microsecond
    Second              = 1000 * Millisecond
    Minute              = 60 * Second
    Hour                = 60 * Minute
)

9.4 时间操作函数

Add:时间+时间间隔。若要获取时间点t - ddDuration),可通过加上该时间间隔负值的方式,即t.Add(-d)

func (t Time) Add(d Duration) Time

Sub:求两个时间之间的差值,返回一个时间段t - u。如果结果超出了Duration可表示的最大值/最小值,则返回最大值/最小值。

func (t Time) Sub(u Time) Duration

Equal:判断两个时间是否相同。与直接t == u不同,本方法会考虑时区的影响,因此不同时区标准的时间亦可正确比较。

func (t Time) Equal(u Time) bool

Before:若t代表的时间点在u之前,返回真;否则返回假。

func (t Time) Before(u Time) bool

After:若t代表的时间点在u之后,返回真;否则返回假。

func (t Time) After(u Time) bool

使用例:

func main() {
    now := time.Now()
    later := now.Add(time.Hour) // 当前时间加1小时后的时间
    fmt.Println(later)
}

9.5 定时器

存在两种方式:

  1. 使用time.NewTicker(Duration)设置定时器:
ticker := time.NewTicker(time.Second)   // 定义一个1秒间隔的定时器
n := 0
for i := range ticker.C {
    fmt.Println(i)  // 每秒都会执行的任务
    n++
    if n > 5 {
        ticker.Stop()
        return
    }
}
  1. 使用time.Sleep(time.Second)实现定时器
for {
    time.Sleep(time.Second)
    fmt.Println("正在定时执行任务")
}

10 指针

指针(Pointer)存储的数据不是一个普通的值,而是另 一个变量的内存地址。Go语言中的指针相比C语言进行了大幅改进,易学易用。

10.1 指针地址和指针类型

每个变量在运行时都有一个地址,该地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行取地址操作。各种值类型(intfloatboolstringarraystruct)都有对应的指针类型,例如*int*int64*string等,取变量指针的语法如下,其中:

  • v:被取地址的变量,类型为T
  • ptr:接收地址的变量,类型为*T,称为T的指针类型,*代表指针。
ptr := &v

如下例所示:

package main

import "fmt"

func main() {
    var a = 10
    var b = &a
    fmt.Printf("a:%d ptr:%p\n", a, &a)    // a:10 ptr:0xc0000100a8 fmt.Printf("b:%v type:%T\n", b, b) // b:0xc0000100a8 type:*int fmt.Println(" 取 b 的地址:" , &b) // 0xc000006028

}

10.2 指针取值与传值

对变量使用&操作符取地址获得该变量的指针后,可对指针使用*进行指针取值操作:

a := 10
b := &a // 获取变量a的指针
fmt.Printf("Type of b: %T\n", b)  // Type of b: *int

c := *b // 指针取值
fmt.Printf("Type of c: %T\n", c)  // Type of c: int
fmt.Printf("Value of c: %v\n", c) // Value of c: 10

与C语言一样,函数参数可以指针传值,如下例所示:

func modify1(x int) {
    x = 100
}

func modify2(x *int) {
    *x = 100
}

func main() {
    a := 10

    modify1(a)
    fmt.Println(a)  // 10

    modify2(&a)
    fmt.Println(a)  // 100
}

10.3 new()与make()

执行以下两段代码会引发panic,是因为在Go语言中对于引用类型的变量,使用时不仅要声明它,还要为它分配内存空间,否则无法存储值。而对于值类型的声明无需分配内存空间,是因为声明时Go语言已默认分配了内存空间:

var a *int
*a = 100
fmt.Println(*a)

```go
var userinfo map[string]string
userinfo["username"] = "Akira"
fmt.Println(userinfo)

new()是一种分配内存的函数,函数签名如下:

func new(Type) *Type

实际开发中new()函数不太常用。new返回指定类型的指针,并且该指针对应的值为该类型的零值。

a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false

本节开头的第1段代码示例仅声明了一个指针变量a但未初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可赋值。可按如下方式进行修正:

var a *int
a = new(int)
*a = 100
fmt.Println(*a)

make也是一种分配内存的函数,只用于slicemap以及channel的内存创建,并且返回的就是这三个类型本身而非其指针类型,因为这三种类型均为引用类型(如前后对应小节所述)。 函数签名如下:

func make(t Type, size ...IntegerType) Type

本节开头的第2段代码示例仅声明变量b是一个map类型的变量,需使用make()函数进行初始化操作之后,才能对其进行键值对赋值:

var userinfo map[string]string
userinfo = make(map[string]string)
userinfo["username"] = "Akira"
fmt.Println(userinfo)

11 结构体

Go语言中没有“类”的概念,但与C语言一样有结构体(Struct),且具有更高的扩展性和灵活性。

11.1 自定义类型和类型别名

Go语言中通过type关键词定义结构体、自定义类型以及类型别名。

定义自定义类型的方式如下所示(类似于C语言中的typedef):

type newInt int

Go语言1.9版本新增类型别名的定义,注意类型别名TypeAlias只是Type的别名,本质上TypeAliasType是同一类型。语法格式如下所示(类似于C++中的using):

type TypeAlias = Type

runebyte实际就是类型别名,其底层定义如下:

type byte = uint8
type rune = int32

自定义类型和类型别名的区别如下例打印结果所示:

package main

import "fmt"

type newInt int // 类型定义
type myInt = int    // 类型别名

func main() {
    var a newInt
    var b myInt

    fmt.Printf("Type of a: %T\n", a) // Type of a: main.newInt
    fmt.Printf("Type of b: %T\n", b) // Type of b: int
}

11.2 结构体的定义与初始化

使用typestruct关键字定义结构体,格式如下,其中:

  • 类型名:自定义结构体的名称,在同一个包内不能重复。
  • 字段名:结构体字段名,必须唯一。
  • 字段类型:结构体字段的具体类型。
type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}

结构体类型名与字段名大小写规则:首字母大写表示是公有的,其他包亦可使用;小写表示是私有的,仅当前包可用。

同类型的字段可以写在同一行:

type person struct {
    name, city  string
    age         int8
}

常见的结构体实例化方法:

  1. 可以像声明内置类型一样使用var关键字声明结构体类型:
package main

import "fmt"

type person struct {
    name string
    city string
    age int
}

func main() {
    var p1 person
    p1.name = "Akira"
    p1.city = "杭州"
    p1.age = 108

    fmt.Printf("p1 = %v\n", p1)   // p1 = {Akira 杭州 108}
    fmt.Printf("p1 = %#v\n", p1)  // p1 = main.person{name:"Akira" , city:"杭州" , age:108}
}
  1. 使用new关键字对结构体进行实例化,得到的是结构体指针,注意结构体指针访问成员仍可直接p2.name(实际底层为(*p2).name):
var p2 = new(person)
p2.name = "Akira"
p2.city = "杭州"
p2.age = 108

fmt.Printf("%T\n", p2)    // *main.person
fmt.Printf("p2 = %#v\n", p2)  // p2 = &main.person{name:"Akira" , city:"杭州" , age:108}
  1. 使用&对结构体进行取地址操作,相当于对该结构体类型进行了一次new()实例化操作。
p3 := &person{}
fmt.Printf("%T\n", p3)    // *main.person
fmt.Printf("p3 = %#v\n", p3)  // p3 = &main.person{name:"", city:"", age:0}
  1. 键值对初始化,亦可不指定某些字段初始值(注意最后一个属性的,不可省略):
p4 := person{
    name: "Akira",
    city: "杭州",
    age: 108,
}

fmt.Printf("p4 = %#v\n", p4)  // p4 = main.person{name:"Akira", city:"杭州", age:108}
  1. 结构体指针进行键值对初始化:
p5 := &person{
    name: "Akira",
    city: "杭州",
    age: 108,
}

fmt.Printf("p5 = %#v\n", p5)  // p5 = &main.person{name:"Akira", city:"杭州", age:108}
  1. 简写:使用值的列表初始化。此时必须初始化结构体的所有字段,且初始值的填充顺序必须与字段在结构体中的声明顺序一致。该方式不能和键值初始化方式混用。
p6 := &person{
    "Akira",
    "杭州",
    108,
}

fmt.Printf("p6 = %#v\n", p6)  // p5 = &main.person{name:"Akira", city:"杭州", age:108}

11.3 结构体方法与接收者

在Go语言中,可以给结构体定义方法(Method),即定义了接受者的函数(接受者类似于其他语言的this或self)。定义格式如下,其中:

  • 接收者变量:官方建议使用接收者类型名的第一个小写字母而非self、this之类的命名,例如Person类型的接收者变量应命名为pConnector类型的接收者变量应命名为c等。
  • 接收者类型:方法所属的结构体类型,可以为指针类型或非指针类型。
  • 方法名、参数列表、返回参数:与函数定义完全相同。
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}

两种接收者类型:

  1. 值类型接收者:当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作仅针对副本,无法修改接收者变量本身
  2. 指针类型接收者:即结构体指针,调用方法时可有效修改接收者指针的任意成员变量。该方式非常类似其他语言的this或self。
package main

import "fmt"

type Person struct {
    name string
    age int
}

// 值类型接受者
func (p Person) printInfo() {
    fmt.Printf("姓名: %v 年龄: %v\n", p.name, p.age)
}

//指针类型接收者
func (p *Person) setInfo(name string, age int) {
    p.name = name
    p.age = age
}

func main() {
    p1 := Person{
        name: "Akira",
        age: 108,
    }
    p1.printInfo()
    p1.setInfo("Teresa", 17)
    p1.printInfo()
}

在Go语言中,接收者的类型可以是任何类型,因此不仅仅是结构体,任意类型均可拥有方法。例如基于内置的int类型使用type关键字可以定义新的自定义类型,然后为该自定义类型添加方法:

package main

import "fmt"

type myInt int

func (i myInt) isEven() bool {
    return i%2 == 0
}

func main() {
    var i myInt = 1
    if i.isEven() {
        fmt.Println("i is even")
    }
    fmt.Printf("%#v %T\n", i, i)  // 1 main.myInt
}

注:非本地类型不能定义方法,即不能给其他包的类型定义方法。

11.4 结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有字段名的字段称为匿名字段

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个

type Person struct {
    string
    int
}

func main() {
    p1 := Person{
        "Akira",
        108,
    }

    fmt.Printf("%#v\n", p1)   // main.Person{string:"Akira", int:108}
    fmt.Println(p1.string, p1.int)  // Akira 108
}

11.5 结构体的继承:嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针(或匿名结构体):

package main

import "fmt"

type Address struct {
    Province string
    City string
}

type User struct {
    Name string
    Gender string
    Address Address
}

func main() {
    user1 := User{
        Name: "Teresa",
        Gender: "Female",
        Address: Address{
            Province: "广东",
            City: "深圳",
        },
    }
    fmt.Printf("user1 = %#v\n", user1)    // user1 = main.User{Name:"Teresa", Gender:"Female", Address:main.Address{Province:"广东", City:"深圳"}}
}

嵌套结构体内部可能存在相同的字段名,此时为了避免歧义需要指定具体的内嵌结构体的字段:

package main

import "fmt"

type Address struct {
    Province string
    City string
    CreateTime string
}

type Email struct {
    Account string
    CreateTime string
}

type User struct {
    Name string
    Gender string
    Address
    Email
}

func main() {
    var user2 User
    user2.Name = "KINA"
    user2.Gender = "N/A"
    user2.Address.CreateTime = "2024" // 指定Address结构体中的CreateTime
    user2.Emai.CreateTime = "2025"    // 指定Email结构体中的CreateTime
}

Go语言中的结构体可以通过嵌套匿名结构体实现其他编程语言中的继承

package main

import "fmt"

type Animal struct {
    name string
}

func (a *Animal) run() {
    fmt.Printf("%s can run!\n", a.name)
}

type Dog struct {
    Age int8
    *Animal // 通过嵌套匿名结构体实现继承
}

func (d *Dog) bark() {
    fmt.Printf("%s can bark!\n" , d.name)
}

func main() {
    d1 := &Dog{
        Age: 4,
        Animal: &Animal{    // 注意本例嵌套的是结构体指针
            name: "Hachi",
        },
    }
    d1.bark()
    d1.run()
}

11.6 结构体与JSON的序列化和反序列化

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于阅读和编写,同时也易于机器解析和生成。 RESTful Api接口中返回的数据都是JSON数据。

Go语言JSON的序列化是指把结构体数据转化成JSON格式的字符串,反序列化是指把JSON数据转化成Go语言中的结构体对象。需要用到encoding/json包中的相关函数。

Map和切片的序列化和反序列化详见接口章。

结构体对象转换成JSON(序列化)使用json.Marshal()函数,直接传入结构体对象即可:

package main

import (
    "encoding/json"
    "fmt"
)

type Student struct {
    ID int
    Gender string
    name string // 私有属性不能被json包访问
    Sno string
}

func main() {
    var s1 = Student{
        ID: 1,
        Gender: "男",
        name: "Xiao",
        Sno: "s0001",
    }
    fmt.Printf("%#v\n", s1)

    var s, _ = json.Marshal(s1)
    jsonStr := string(s)
    fmt.Println(jsonStr)
}

JSON转换成结构体对象(反序列化)使用json.Unmarshal()函数,需要传入JSON字符串(需强转为[]byte)和结构体对象的地址:

package main

import (
    "encoding/json"
    "fmt"
)

type Student struct {
    ID int
    Gender string
    Name string
    Sno string
}

func main() {
    var jsonStr = `{"ID":1,"Gender":"男","Name":"Xiao","Sno":"s0001"}`

    var student Student
    err := json.Unmarshal([]byte(jsonStr), &student)
    if err != nil {
        fmt.Printf("unmarshal err=%v\n", err)
    }
    fmt.Printf("反序列化后:student = %#v, student.Name = %v\n", student, student.Name)
}

11.7 结构体标签(Tag)

Tag是结构体的元信息,可以在运行时通过反射机制读取。Tag在结构体字段的后方定义,用一对反引号(\)包裹,由一个或多个键值对组成,键与值使用冒号分隔,值使用双引号("`)包裹。同一个结构体字段可设置多个键值对tag,不同键值对之间用空格分隔。

注意严格控制格式,不要添加多余的空格。

序列化例:

package main

import (
    "encoding/json"
    "fmt"
)

type Student struct {
    ID int `json:"id"`  // 通过指定tag实现JSON序列化该字段时的key
    Gender string `json:"gender"`
    Name string
    Sno string
}

func main() {
    var s1 = Student{
        ID: 1,
        Gender: "男",
        Name: "Xiao",
        Sno: "s0001",
    }
    fmt.Printf("%#v\n", s1)

    var s, _ = json.Marshal(s1)
    jsonStr := string(s)
    fmt.Println(jsonStr)
}

反序列化例:

package main

import (
    "encoding/json"
    "fmt"
)

type Student struct {
    ID int `json:"id"`  // 通过指定tag实现JSON序列化该字段时的key
    Gender string `json:"gender"`
    Name string
    Sno string
}

func main() {
    var jsonStr = `{"ID":1,"Gender":"男","Name":"Xiao","Sno":"s0001"}`

    var student Student
    err := json.Unmarshal([]byte(jsonStr), &student)
    if err != nil {
        fmt.Printf("unmarshal err=%v\n", err)
    }
    fmt.Printf("反序列化后:student = %#v, student.Name = %v\n", student, student.Name)
}

11.8 嵌套结构体与JSON的序列化和反序列化

序列化例:

package main

import (
    "encoding/json"
    "fmt"
)

type Student struct {
    ID int
    Gender string
    Name string
}

type Class struct {
    Title string
    Students []Student
}

func main() {
    c := &Class{
        Title: "001",
        Students: make([]Student, 0, 200),
    }

    for i := 0; i < 10; i++ {
        stu := Student{
            Name: fmt.Sprintf("stu%02d", i),
            Gender: "男",
            ID: i,
        }
        c.Students = append(c.Students, stu)
    }

    data, err := json.Marshal(c)
    if err != nil {
        fmt.Println("JSON marshal failed")
        return
    }
    fmt.Printf("json: %s\n", data)
}

反序列化例:

package main

import (
    "encoding/json"
    "fmt"
)

type Student struct {
    ID int
    Gender string
    Name string
}

type Class struct {
    Title string
    Students []Student
}

func main() {
    str := `{"Title":"001","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`

    c1 := &Class{}

    err := json.Unmarshal([]byte(str), c1)
    if err != nil {
        fmt.Println("JSON unmarshal failed!")
        return
    }
    fmt.Printf("%#v\n", c1)
}

12 包

(Package)是多个Go源码的集合,是一种高级的代码复用方案,Go语言中的包可以分为三种:

  1. 系统内置包:Go语言提供的内置包,例如fmtstrconvstringssorterrorstimeencoding/jsonosio等。
  2. 自定义包:开发者自己写的包
  3. 第三方包:自定义包的一种,需要下载安装到本地后才可使用,例如前述的用于解决float精度丢失问题的github.com/shopspring/decimal包。

12.1 包管理工具go.mod

发表评论