Bootstrap

Golang学习笔记

学习文档链接:Go入门学习

一、环境安装

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

Go官方镜像站:https://golang.google.cn/dl/

Windows下运行下载好的可执行文件即可完成安装。

1、GOROOT和GOPATH

GOROOTGOPATH都是环境变量,其中GOROOT是安装go开发包的路径,而从Go 1.8版本开始,Go开发包在安装完成后会为GOPATH设置一个默认目录,并且在Go1.14及之后的版本中启用了Go Module模式之后,不一定非要将代码写到GOPATH目录下,所以也就不需要自己配置GOPATH了,使用默认的即可。

2、GOPROXY

Go1.14版本之后,都推荐使用go mod模式来管理依赖环境了,也不再强制把代码必须写在GOPATH下面的src目录。

默认的配置:

GOPROXY=https://proxy.golang.org,direct

修改GOPROXY:

go env -w GOPROXY=https://goproxy.cn,direct

3、命令

  • go env用于打印Go语言的环境信息。

  • go run命令可以编译并运行命令源码文件。

  • go get可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。

  • go build命令用于编译我们指定的源码文件或代码包以及它们的依赖包。

  • go install用于编译并安装指定的代码包及它们的依赖包。

  • go clean命令会删除掉执行其它命令时产生的一些文件和目录。

  • go doc命令可以打印附于Go语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的。

  • go test命令用于对Go语言编写的程序进行测试。

  • go list命令的作用是列出指定的代码包的信息。

  • go fix会把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码。

  • go vet是一个用于检查Go语言源码中静态错误的简单工具。

  • go tool pprof命令来交互式的访问概要文件的内容。

二、Hello Go

1、创建一个名为hello的文件夹

2、进入命令行,对项目进行初始化

使用go module模式新建项目时,通过go mod init 项目名命令对项目进行初始化,该命令会在项目的根目录下生成go.mod文件。

go mod init hello

3、在hello目录下新建main.go文件

package main //声明main包,表明当前是一个可执行程序

import "fmt" //导入内置fmt包

func main(){ //main函数,是程序执行的入口
    fmt.Println("Hello Go") //在终端打印Hello Go
}

4、编译

在hello目录下的命令行中执行以下命令:

go build

go build:表示将源代码编译成可执行文件。

go build hello

上述这种写法go编译器会去GOPATH的src目录下查找需要编译的hello项目。

编译得到的可执行文件保存在执行编译命令的当前目录下。可以通过- o参数指定编译后得到的可执行文件的名字。

go build -o first.exe

go run

go run main.go也可以执行程序,该命令本质上也是先编译后执行。

go install

go install表示安装的意思,它先编译源代码得到可执行文件,然后将可执行文件移动到GOPATH的bin目录下。因为境变量中配置了GOPATH下的bin目录,所以可以在任意地方直接执行可执行文件。

跨平台编译

默认go build的可执行文件都是当前操作系统可执行的文件,Go语言支持跨平台编译——在当前平台(例如Windows)下编译其他平台(例如Linux)的可执行文件。

三、go module

go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具。

GO111MODULE

要启用go module支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:offonauto,默认值是auto

  1. GO111MODULE=off禁用模块支持,编译时会从GOPATHvendor文件夹中查找包。
  2. GO111MODULE=on启用模块支持,编译时会忽略GOPATHvendor文件夹,只根据 go.mod下载依赖。
  3. GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持。

简单来说,设置GO111MODULE=on之后就可以使用go module了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。

使用 go module 管理依赖后会在项目根目录下生成两个文件go.modgo.sum

GOPROXY

Go1.11之后设置GOPROXY命令为:

export GOPROXY=https://goproxy.cn

Go1.13之后GOPROXY默认值为https://proxy.golang.org

go env -w GOPROXY=https://goproxy.cn,direct

go mod命令

常用的go mod命令如下:

go mod download    下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit        编辑go.mod文件
go mod graph       打印模块依赖图
go mod init        初始化当前文件夹, 创建go.mod文件
go mod tidy        增加缺少的module,删除无用的module
go mod vendor      将依赖复制到vendor下
go mod verify      校验依赖
go mod why         解释为什么需要依赖

go.mod文件

go.mod文件记录了项目所有的依赖信息,其结构大致如下:

module github.com/Q1mi/studygo/blogger

go 1.16

require (
	github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586
	github.com/gin-gonic/gin v1.4.0
	github.com/go-sql-driver/mysql v1.4.1
	github.com/jmoiron/sqlx v1.2.0
	github.com/satori/go.uuid v1.2.0
	google.golang.org/appengine v1.6.1 // indirect
)

其中,

  • module用来定义包名
  • require用来定义依赖包及版本
  • indirect表示间接引用

依赖的版本

go mod支持语义化版本号,比如go get [email protected],也可以跟git的分支或tag,比如go get foo@master,当然也可以跟git提交哈希,比如go get foo@e3702bed2。关于依赖的版本支持以下几种格式:

gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
gopkg.in/vmihailenco/msgpack.v2 v2.9.1
gopkg.in/yaml.v2 <=v2.2.1
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
latest

replace

在国内访问golang.org/x的各个包都需要翻墙,你可以在go.mod中使用replace替换成github上对应的库。

replace (
	golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
	golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
	golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)

go get

在项目中执行go get命令可以下载依赖包,并且还可以指定下载的版本。

  1. 运行go get -u将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)
  2. 运行go get -u=patch将会升级到最新的修订版本
  3. 运行go get package@version将会升级到指定的版本号version

如果下载所有依赖可以使用go mod download命令。

整理依赖

在代码中删除依赖代码后,相关的依赖库并不会在go.mod文件中自动移除。这种情况下可以使用go mod tidy命令更新go.mod中的依赖关系。

go mod edit

格式化

因为可以手动修改go.mod文件,所以有些时候需要格式化该文件。Go提供了一下命令:

go mod edit -fmt

添加依赖项

go mod edit -require=golang.org/x/text

移除依赖项

如果只是想修改go.mod文件中的内容,那么可以运行go mod edit -droprequire=package path,比如要在go.mod中移除golang.org/x/text包,可以使用如下命令:

go mod edit -droprequire=golang.org/x/text

关于go mod edit的更多用法可以通过go help mod edit查看。

项目中使用go module

已有项目

如果需要对一个已经存在的项目启用go module,可以按照以下步骤操作:

  1. 在项目目录下执行go mod init,生成一个go.mod文件。
  2. 执行go get,查找并记录当前项目的依赖,同时生成一个go.sum记录每个依赖库的版本和哈希值。

新项目

对于一个新创建的项目,可以在项目文件夹下按照以下步骤操作:

  1. 执行go mod init 项目名命令,在当前项目文件夹下创建一个go.mod文件。
  2. 手动编辑go.mod中的require依赖项或执行go get自动发现、维护依赖。

使用go module导入本地包

假设存在两个包:demo和test,其中demo包中会导入test包并使用其方法。

test/test.go

package test

import "fmt"

fun Test(){
    fmt.Println("test.Test()")
}

在同一个项目下

目录结构

demo
├── go.mod
├── main.go
└── test
    └── test.go

1、导入包

demo/go.mod中按如下定义:

module demo

go 1.16

然后再demo/main.go中按如下方式导入test:

package main

import (
    "fmt"
    "demo/test"
)

func main(){
    test.Test()
    fmt.Print("main.main()")
}

不在同一个项目下

├── demo
│   ├── go.mod
│   └── main.go
└── test
    ├── go.mod
    └── test.go

1、导入包

对demo和test进行module初始化(也就是对这两个项目分别进行go mod init),然后在demo/main.go中按如下方式导入:

package main

import (
    "fmt"
    "test"
)

func main(){
    test.Test()
    fmt.Print("main.main()")
}

2、指定test包的路径

因为这两个包不在同一个项目路径下,想要导入本地包,并且这些包也没有发布到远程的github或其他代码仓库地址。这个时候就需要在go.mod文件中使用replace指令。

在调用方demo/go.mod中按如下方式指定使用相对路径来寻找test包:

module demo

go 1.16

require "test" v0.0.0
replace "test" => "../test"

四、Go基础

主要特征

1、Go语言的主要特征

  • 自动立即回收
  • 更丰富的内置类型
  • 函数多返回值
  • 错误处理
  • 匿名函数和闭包
  • 类型和接口
  • 并发编程
  • 反射
  • 语言交互性

2、Go语言命名

1、Go的函数、变量、常量、自定义类型、包(package)的命名方式遵循以下规则:

  • 首字符可以是任意的Unicode字符或者下划线
  • 剩余字符可以是Unicode字符、下划线、数字
  • 字符长度不限

2、Go只有25个关键字

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

3、Go还有37个保留字

Constants:    true  false  iota  nil

Types:    int  int8  int16  int32  int64  
uint  uint8  uint16  uint32  uint64  uintptr
float32  float64  complex128  complex64
bool  byte  rune  string  error

Functions:   make  len  cap  new  append  copy  close  delete
complex  real  imag
panic  recover

4、可见性

  • 声明在函数内部,是函数的本地值,类似private
  • 声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect
  • 声明在函数外部且首字母大写是所有包可见的全局值,类似public

3、Go语言声明

有四种主要声明方式:

  • var声明变量
  • onst声明常量)
  • type声明类型
  • func声明函数

Go的程序是保存在多个.go文件中,文件的第一行就是package XXX声明,用来说明该文件属于哪个包(package),package声明下来就是import声明,再下来是类型,变量,常量,函数的声明。‘

下划线

_是特殊标识符,用来忽略结果。

1、在import中

当导入一个包时,该包下的文件里所有init()函数都会被执行,然而,有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。这个时候就可以使用import引用该包。即使用import _ 包路径只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。

2、在代码中

解释一

下划线意思是忽略这个变量。比如os.Open,返回值为*os.File,error

普通写法:

f,err := os.Open("xxxxxxx")

如果此时不需要知道返回的错误值,就可以用

f, _ := os.Open("xxxxxx") 

如此则忽略了error变量。

解释二

占位符,意思是那个位置本应赋给某个值,但是咱们不需要这个值。所以就把该值赋给下划线,意思是丢掉不要。这样编译器可以更好的优化,任何类型的单个值都可以丢给下划线。这种情况是占位用的,方法返回两个结果,而你只想要一个结果。那另一个就用_ 占位,而如果用变量的话,不使用,编译器是会报错的。

补充

import "database/sql"
import _ "github.com/go-sql-driver/mysql"

第二个import就是不直接使用mysql包,只是执行一下这个包的init函数,把mysql的驱动注册到sql包里,然后程序里就可以使用sql包来访问mysql数据库了。

基本数据类型

在 Go 编程语言中,数据类型用于声明函数和变量。

数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。

Go语言中有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串外,还有数组、切片、结构体、函数、map、通道(channel)等。Go 语言的基本类型和其他语言大同小异。

1、整型

类型描述
uint8无符号 8位整型 (0 到 255)
uint16无符号 16位整型 (0 到 65535)
uint32无符号 32位整型 (0 到 4294967295)
uint64无符号 64位整型 (0 到 18446744073709551615)
int8有符号 8位整型 (-128 到 127)
int16有符号 16位整型 (-32768 到 32767)
int32有符号 32位整型 (-2147483648 到 2147483647)
int64有符号 64位整型 (-9223372036854775808 到 9223372036854775807)

特殊整型

类型描述
uint32位操作系统上就是uint32,64位操作系统上就是uint64
int32位操作系统上就是int32,64位操作系统上就是int64
uintptr无符号整型,用于存放一个指针

注意: 在使用intuint类型时,不能假定它是32位或64位的整型,而是考虑intuint可能在不同平台上的差异。

注意事项:获取对象的长度的内建len()函数返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用int来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用intuint

2、浮点型

类型描述
float32遵循IEEE 754标准,最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32
float64遵循IEEE 754标准,最大范围约为1.8e308,可以使用常量定义:math.MaxFloat64

打印浮点数时,可以使用fmt包配合动词%f

3、复数

在计算机中,复数是由两个浮点数表示的,其中一个表示实部(real),一个表示虚部(imag)

complex64的实部和虚部为32位complex128的实部和虚部为64位,其中 complex128 为复数的默认类型。

复数的值由三部分组成 RE + IMi,其中 RE 是实数部分,IM 是虚数部分,RE 和 IM 均为 float 类型,而最后的 i 是虚数单位。

声明复数的语法格式如下所示:

var name complex128 = complex(x, y)

其中name为复数的变量名,complex128为复数的类型,=后面的complex为Go语言的内置函数用于为复数赋值,xy分别表示构成该复数的两个float64类型的数值,x为实部,y为虚部。

上面的声明语句也可以简写为下面的形式:

name := complex(x, y)

对于一个复数z := complex(x, y),可以通过Go语言的内置函数real(z) 来获得该复数的实部,也就是 x;通过imag(z) 获得该复数的虚部,也就是y

4、布尔型

Go语言中以bool类型进行声明布尔型数据,布尔型数据只有truefalse两个值。

布尔值可以和&&(AND)和||(OR)操作符结合,并且有短路行为,如果运算符左边的值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值,因此下面的表达式总是安全的:

s != "" && s[0] == 'x'

其中s[0]操作如果应用于空字符串将会导致panic异常。

因为&&的优先级比||高(&& 对应逻辑乘法,|| 对应逻辑加法,乘法比加法优先级要高),所以下面的布尔表达式可以不加小括号:

if 'a' <= c && c <= 'z' ||
    'A' <= c && c <= 'Z' ||
    '0' <= c && c <= '9' {
    // ...ASCII字母或数字...
}

布尔值并不会隐式转换为数字值 0 或 1,反之亦然,必须使用 if 语句显式的进行转换:

i := 0
if b {
    i = 1
}

如果需要经常做类似的转换,可以将转换的代码封装成一个函数,如下所示:

// 如果b为真,btoi返回1;如果为假,btoi返回0
func btoi(b bool) int {
    if b {
        return 1
    }
    return 0
}

数字到布尔型的逆转换非常简单,不过为了保持对称,我们也可以封装一个函数:

// itob报告是否为非零。
func itob(i int) bool { 
    return i != 0 
}

注意:

  1. 布尔类型变量的默认值为false
  2. Go 语言中不允许将整型强制转换为布尔型。
  3. 布尔型无法参与数值运算,也无法与其他类型进行转换。

5、字符串

Go 语言里的字符串的内部实现使用UTF-8编码。 字符串的值为双引号"中的内容,可以在Go语言的源码中直接添加非ASCII码字符,例如:

s1 := "hello"
s2 := "Go,你好"

字符串转义符

Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。

转义符含义
\r回车符(返回行首)
\n换行符(直接跳到下一行的同列位置)
\t制表符
\'单引号
\"双引号
\\反斜杠

举个例子,我们要打印一个Windows平台下的一个文件路径:

package main

import "fmt"

func main(){
	fmt.Println("str := \"C:\\Users\\lskj\\Desktop\"")
}

多行字符串

Go语言中要定义一个多行字符串时,就必须使用反引号字符:

s1 := `第一行
第二行
第三行
`
fmt.Println(s1)

反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

字符串的常用操作

方法介绍
len(str)求长度
+或fmt.Sprintf拼接字符串
strings.Split分割
strings.contains判断是否包含
strings.HasPrefix,strings.HasSuffix前缀/后缀判断
strings.Index(),strings.LastIndex()子串出现的位置
strings.Join(a[]string, sep string)join操作

6、byte与rune类型

  • byte类似uint8,一般用于强调数值是一个原始的数据而不是一个小的整数,代表了ASCII码的一个字符。

  • rune通常用于表示一个Unicode码点,代表一个UTF-8字符。当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32

组成每个字符串的元素叫做字符,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号'包裹起来。

字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。

修改字符串

要修改字符串,需要先将其转换成[]rune[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。

func changeString() {
    s1 := "hello"
    // 强制类型转换
    byteS1 := []byte(s1)
    byteS1[0] = 'H'
    fmt.Println(string(byteS1))

    s2 := "男生"
    runeS2 := []rune(s2)
    runeS2[0] = '女'
    fmt.Println(string(runeS2))
}

类型转换

Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。

强制类型转换的基本语法如下:

T(表达式)

其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等。

类型B的值 = 类型B(类型A的值)
  • 类型转换只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(将 int16 转换为 int32)。当从一个取值范围较大的类型转换到取值范围较小的类型时(将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况。
  • 浮点数在转换为整型时,会将小数部分去掉,只保留整数部分。

数字字面量语法

数字字面量语法(Number literals syntax)。Go1.13版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字,例如:

v := 0b00101101, 代表二进制的 101101,相当于十进制的 45。 v := 0o377,代表八进制的 377,相当于十进制的 255。 v := 0x1p-2,代表十六进制的 1 除以 2²,也就是 0.25。

而且还允许我们用 _ 来分隔数字,比如说: v := 123_456 表示 v 的值等于 123456。

我们可以借助fmt函数来将一个整数以不同进制形式展示。

package main

import "fmt"

func main(){
    // 十进制
    var a int = 10
    fmt.Printf("%d \n", a)  // 10
    fmt.Printf("%b \n", a)  // 1010  占位符%b表示二进制

    // 八进制  以0开头
    var b int = 077
    fmt.Printf("%o \n", b)  // 77

    // 十六进制  以0x开头
    var c int = 0xff
    fmt.Printf("%x \n", c)  // ff
    fmt.Printf("%X \n", c)  // FF
}

内置类型和函数

1、内置类型

值类型

bool
int(32 or 64), int8, int16, int32, int64
uint(32 or 64), uint8(byte), uint16, uint32, uint64
float32, float64
string
complex64, complex128
array    -- 固定长度的数组

引用类型(指针类型)

slice   -- 序列数组(最常用)
map     -- 映射
chan    -- 管道

2、内置函数

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。

append          -- 用来追加元素到数组、slice中,返回修改后的数组、slice
close           -- 主要用来关闭channel
delete            -- 从map中删除key对应的value
panic            -- 停止常规的goroutine  (panic和recover:用来做错误处理)
recover         -- 允许程序定义goroutine的panic动作
real            -- 返回complex的实部   (complex、real imag:用于创建和操作复数)
imag            -- 返回complex的虚部
make            -- 用来分配内存,返回Type本身(只能应用于slice, map, channel)
new                -- 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针
cap                -- capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
copy            -- 用于复制和连接slice,返回复制的数目
len                -- 来求长度,比如string、array、slice、map、channel ,返回长度
print、println     -- 底层打印函数,在部署环境中建议使用 fmt 包

内置接口error

type error interface { //只要实现了Error()函数,返回值为String的都实现了err接口
	Error()    String
}

init函数与main函数

1、init函数

go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性。

  • init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等
  • 每个包可以拥有多个init函数
  • 包的每个源文件也可以拥有多个init函数
  • 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明)
  • 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序
  • init函数不能被其他函数调用,而是在main函数执行之前,自动被调用

2、main函数

Go语言程序的默认入口函数(主函数):func main(),函数体用{}一对括号包裹。

init函数与main函数的异同

相同点:两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。

不同点

  • init可以应用于任意包中,且可以重复定义多个。
  • main函数只能用于main包中,且只能定义一个。

执行顺序

  • 对同一个go文件的init()调用顺序是从上到下的。

  • 对同一个package中不同文件是按文件名字符串比较从小到大顺序调用各文件中的init()函数。

  • 对于不同的package,如果不相互依赖的话,按照main包中"先import的后调用"的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。

如果init函数中使用了println()或者print(),在执行过程中这两个不会按照想象中的顺序执行。这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用。

变量和常量

1、变量

标准格式

Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。声明变量的一般形式是使用var关键字,行尾无需分号

var 变量名 变量类型

变量初始化:

var 变量名 类型 = 表达式

批量格式

变量声明时可以声明多个变量:

var (
    a string
    b int
    c bool
    d float32
)

简短格式

除 var 关键字外,还可使用更加简短的变量定义和初始化语法。

名字 := 表达式

需要注意的是,简短格式有以下限制:

  • 定义变量,同时显式初始化。
  • 不能提供数据类型。
  • 只能用在函数内部。

var形式声明语句一样,简短变量声明语句也可以用来声明和初始化一组变量。

  • 指定变量类型,如果没有初始化,则变量默认为零值。
  • 将变量的类型省略时,编译器会根据值自行判定变量类型。
  • 如果变量已经使用 var 声明过了,再使用 :=声明变量,就产生编译错误。

匿名变量

在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_表示(_本身就是一个特殊的标识符,被称为空白标识符),例如:

func foo() (int, string) {
    return 10, "Q1mi"
}
func main() {
    x, _ := foo()
    _, y := foo()
    fmt.Println("x=", x)
    fmt.Println("y=", y)
}

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。

  • 函数外的每个语句都必须以关键字开始(varconstfunc等)
  • :=不能使用在函数外
  • _多用于占位,表示忽略值

变量的作用域

根据变量定义位置的不同,可以分为以下三个类型:

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 函数定义中的变量称为形式参数

2、常量

常量的定义格式和变量的声明语法类似:

const 常量名 常量类型 =

常量的值必须是能够在编译时就能够确定的,可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

与变量声明一样,也可以批量声明多个常量:

const (
    pi = 3.1415926
    title = "test"
)

所有常量的运算都可以在编译期完成,这样不仅可以减少运行时的工作,也方便其他代码的编译优化,当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。

常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:lencaprealimagcomplex unsafe.Sizeof

iota

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

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一。

定义一个 Weekday 命名类型,然后为一周的每天定义了一个常量,从周日 0 开始:

type Weekday int
const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

在其它编程语言中,这种类型一般被称为枚举类型。

常见的iota示例

使用_跳过某些值

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

定义数量级 (这里的<<表示左移操作,1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000,也就是十进制的1024。同理2<<2表示将2的二进制表示向左移2位,也就是由10变成了1000,也就是十进制的8。)

const (
    _  = iota
    KB = 1 << (10 * iota)
    MB = 1 << (10 * iota)
    GB = 1 << (10 * iota)
    TB = 1 << (10 * iota)
    PB = 1 << (10 * iota)
)

多个iota定义在一行

const (
    a, b = iota + 1, iota + 2 //1,2
    c, d                      //2,3
    e, f                      //3,4
)

无类型常量

Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型,例如intfloat64,或者是类似time.Duration这样的基础类型,但是许多常量并没有一个明确的基础类型。

编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算,可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串

通过延迟明确常量的具体类型,不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。

例:math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方:

var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

如果math.Pi被确定为特定类型,比如 float64,那么结果精度可能会不一样,同时对于需要float32complex128类型值的地方则需要一个明确的强制类型转换:

const Pi64 float64 = math.Pi
var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)

对于常量面值,不同的写法可能会对应不同的类型。例如 00.00i\u0000虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,truefalse也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。

五、Go容器

数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组。

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

数组的声明语法如下:

var 数组变量名 [元素数量]变量类型
  • 数组变量名:数组声明及使用时的变量名。
  • 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
  • 变量类型:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。

数组的每个元素都可以通过索引下标来访问,索引下标的范围是从 0 开始到数组长度减1(len-1)的位置,内置函数len()可以返回数组中元素的个数,访问越界(下标在合法范围之外),则触发访问越界,会panic。

1、数组的遍历

var a [3]int

// for循环遍历
for i := 0; i < len(a); i++ {
    fmt.Println(a[i])
}

// for range遍历
for index, value := range a {
    fmt.Println(index,value)
}
  • 默认情况下,数组的每个元素都会被初始化为元素类型对应的零值,对于数字类型来说就是 0,同时也可以使用数组字面值语法,用一组值来初始化数组。
  • 在数组的定义中,如果在数组长度的位置出现“…”省略号,则表示数组的长度是根据初始化值的个数来计算。
  • 数组的长度是数组类型的一个组成部分,因此 [3]int 和 [4]int 是两种不同的数组类型,数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

2、多维数组

Go 语言支持多维数组,以下为常用的多维数组声明方式:

var 数组变量名 [元素数量]元素数量]...元素数量] 变量类型

多维数组只有第一层可以使用...来让编译器推导数组长度。

比较两个数组是否相等

如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,我们可以直接通过较运算符(==!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。

切片

Go 语言切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

切片的内部结构包含地址长度容量。切片一般用于快速地操作一块数据集合。

1、定义切片

声明未指定大小的数组来定义切片

var 变量名 []T

T:表示切片中的元素类型。切片不需要说明长度。

使用make()函数来创建切片

var slice1 []type = make([]type, len)

//可以简写为
slice1 := make([]type, len)

也可以指定容量,其中capacity为可选参数。

make([]T, length, capacity)

这里len是数组的长度并且也是切片的初始长度。

使make()函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

2、切片初始化

s :=[] int {1,2,3 } 

直接初始化切片,[]表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3

//生成切片的格式中,当开始和结束位置都被忽略时,生成的切片将表示和原切片一致的切片,并且生成的切片与原切片在数据内容上也是一致的
s := arr[:]  //表示原有的切片。arr[0:0],表示重置切片,清空拥有的元素。把切片的开始和结束位置都设为0时,生成的切片将变空

//初始化切片 s ,是数组 arr 的引用
s := arr[startIndex:endIndex]
//将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片
//默认 startIndex 时将表示从 arr 的第一个元素开始
s := arr[startIndex:]
//默认 endIndex 时将表示一直到arr的最后一个元素
s := arr[:endIndex]

//通过切片 s 初始化切片 s1
s1 := s[startIndex:endIndex]
//通过内置函数make()初始化切片s,[]int标识为其元素类型为int的切片
s :=make([]int,len,cap)

3、len()和cap()

切片是可索引的,可以由len()获取长度。

切片提供了计算容量的方法cap()可以测量切片最长可以达到多少。

4、空(nil)切片

一个切片在未初始化之前默认为nil,长度为0

切片不能直接比较

切片之间是不能比较的,不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是不能说一个长度和容量都是0的切片一定是nil,例如:

var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

所以判断一个切片是否为空,需使用len(s) == 0来判断,而不应该使用s == nil来判断。

5、切片截取

可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],实例如下:

package main

import "fmt"

func main() {
	/* 创建切片 */
	numbers := []int{0,1,2,3,4,5,6,7,8}
	printSlice(numbers)

	/* 打印原始切片 */
	fmt.Println("numbers ==", numbers)

	/* 打印子切片从索引1(包含) 到索引4(不包含)*/
	fmt.Println("numbers[1:4] ==", numbers[1:4])

	/* 默认下限为 0*/
	fmt.Println("numbers[:3] ==", numbers[:3])

	/* 默认上限为 len(s)*/
	fmt.Println("numbers[4:] ==", numbers[4:])

	numbers1 := make([]int,0,5)
	printSlice(numbers1)

	/* 打印子切片从索引  0(包含) 到索引 2(不包含) */
	number2 := numbers[:2]
	printSlice(number2)

	/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
	number3 := numbers[2:5]
	printSlice(number3)

}

func printSlice(x []int){
	fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

执行以上代码输出结果为:

len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]

6、append()

Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。

package main

import "fmt"

func main() {
	var numbers []int
	printSlice(numbers)

	/* 允许追加空切片 */
	numbers = append(numbers, 0)
	printSlice(numbers)

	/* 向切片添加一个元素 */
	numbers = append(numbers, 1)
	printSlice(numbers)

	/* 同时添加多个元素 */
	numbers = append(numbers, 2,3,4)
	printSlice(numbers)

	s := []int{5,6,7}
	printSlice(s)

	numbers = append(numbers,s...)
	printSlice(numbers)
}

func printSlice(x []int){
	fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

上述代码执行输出结果为:

len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=3 cap=3 slice=[5 6 7]
len=8 cap=12 slice=[0 1 2 3 4 5 6 7]

通过var声明的零值切片可以在append()函数直接使用,无需初始化。

每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行扩容,此时该切片指向的底层数组就会更换。扩容操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。

  • append()函数将元素追加到切片的最后并返回该切片。

  • 切片的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍

7、copy()

package main

import "fmt"

func main() {
	numbers := []int{0,1,2,3,4,5,6,7}
	printSlice(numbers)

	/* 创建切片 s 是之前切片的两倍容量*/
	s := make([]int, len(numbers), (cap(numbers))*2)

	/* 拷贝 numbers 的内容到 s */
	copy(s,numbers)
	printSlice(s)

	s1 := s
	printSlice(s1)

	s1[0] = 1
	printSlice(s)
	printSlice(s1)
}

func printSlice(x []int){
	fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

上述代码执行结果如下:

len=8 cap=8 slice=[0 1 2 3 4 5 6 7]
len=8 cap=16 slice=[0 1 2 3 4 5 6 7]
len=8 cap=16 slice=[0 1 2 3 4 5 6 7]
len=8 cap=16 slice=[1 1 2 3 4 5 6 7]
len=8 cap=16 slice=[1 1 2 3 4 5 6 7]

由于切片是引用类型,所以s和s1其实都指向了同一块内存地址。修改s1的同时s的值也会发生变化。

8、从切片中删除元素

Go语言中并没有删除切片元素的专用方法,但是可以使用切片本身的特性来删除元素。

删除指定位置元素

代码如下:

package main

import "fmt"

func main() {
	a := []int{0,1,2,3,4,5,6,7}
	printSlice(a)

	//删除索引为0的元素
	numbers = append(a[:0],a[1:]...)
	printSlice(a)
}

func printSlice(x []int){
	fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

上述代码执行结果如下:

len=8 cap=8 slice=[0 1 2 3 4 5 6 7]
len=7 cap=8 slice=[1 2 3 4 5 6 7]

要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

从开头位置删除

删除开头的元素可以直接移动数据指针:

a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素

也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):

a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素

还可以用 copy() 函数来删除开头的元素:

a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素

从中间位置删除

对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成。

a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素

从尾部删除

a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素

Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来。

连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。

map

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。map是一种无序的键值对的集合。map最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

map是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,map是无序的,我们无法决定它的返回顺序,这是因为map是使用 hash 表来实现的。

1、定义map

以使用内建函数 make 也可以使用 map 关键字来定义map:

/* 声明变量,默认 map 初始值是 nil */
var 变量名 map[keyType]valueType

/* 使用 make 函数 */
变量名 := make(map[keyType]valueType)

/* 因map默认初始值为nil,需使用make()函数来分配内存,如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对 */
make(map[keyType]valueType,[cap])  //其中cap表示map的容量,该参数虽然不是必须的,但是应该在初始化map的时候就为其指定一个合适的容量
  • keyType:表示键的类型
  • valueType:表示键对应的值的类型

[keyType] 和 valueType 之间允许有空格

2、判断某个键是否存在

Go中判断map中键是否存在的写法:

value, ok := map[key]
  • value:map中该key对应的值,不存在该key,则value为其对应类型的默认值
  • ok:map中是否存在这个key

3、map的遍历

使用for range遍历map。例:

package main

import "fmt"

func main() {
	language := make(map[string]int)
	language["java"] = 2
	language["golang"] = 3
	language["javascript"] = 1
	for k, v := range language {
		fmt.Println(k, v)
	}
}

如只遍历值,可以使用下面的形式(将不需要的键使用_改为匿名变量形式):

for _, v := range language {
    //...
}

只遍历键时,使用下面的形式(无须将值改为匿名变量形式,忽略值即可):

for k :range language {
    //...
}

注:遍历输出元素的顺序与填充顺序无关。如果需要特定顺序的遍历结果,正确的做法是先排序。

4、map元素的删除

Go语言提供了一个内置函数delete(),用于删除容器内的元素。

使用delete()内建函数从map中删除一组键值对,delete()函数的格式如下:

delete(map,)
  • map为要删除的 map 实例
  • 键为要删除的 map 中键值对的键

清空map中的所有元素

Go语言中并没有为 map 提供任何清空所有元素的函数、方法。清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。

list

列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等。

1、初始化列表

list的初始化有两种方法:分别是使用 New() 函数和 var 关键字声明,两种方法的初始化效果都是一致的。

  • 通过container/list包的New()函数初始化list

    变量名 := list.New()
    
  • 通过var关键字声明初始化list

    var 变量名 list.List
    

列表与切片和ma 不同的是,列表并没有具体元素类型的限制,因此,列表的元素可以是任意类型,这既带来了便利,也引来一些问题,例如给列表中放入了一个interface{}类型的值,取出值后,如果要将interface{}转换为其他类型将会发生宕机。

2、在列表中插入元素

双链表支持从队列前方或后方插入元素,分别对应的方法是PushFrontPushBack

这两个方法都会返回一个*list.Element结构,如果在以后的使用中需要删除插入的元素,则只能通过*list.Element配合Remove() 方法进行删除,这种方法可以让删除更加效率化,同时也是双链表特性之一。

下面代码展示如何给 list 添加元素:

l := list.New() //创建一个列表实例
l.PushBack("fist") //将fist字符串插入到列表的尾部,此时列表是空的,插入后只有一个元素
l.PushFront(67) //将数值67放入列表,此时,列表中已经存在fist元素,67这个元素将被放在fist的前面

列表插入元素的方法如下:

方 法功 能
InsertAfter(v interface {}, mark * Element) * Element在 mark 点之后插入元素,mark 点由其他插入函数提供
InsertBefore(v interface {}, mark * Element) *Element在 mark 点之前插入元素,mark 点由其他插入函数提供
PushBackList(other *List)添加 other 列表元素到尾部
PushFrontList(other *List)添加 other 列表元素到头部

3、从列表中删除元素

列表插入函数的返回值会提供一个*list.Element结构,这个结构记录着列表元素的值以及与其他节点之间的关系等信息,从列表中删除元素时,需要用到这个结构进行快速删除。

package main

import "container/list"

func main() {
	// 创建列表实列
	l := list.New()
	// 尾部添加
	l.PushBack("canon")
	// 头部添加
	l.PushFront(67)
	// 将字符串 fist 插入到列表的尾部,并将这个元素的内部结构保存到 element 变量中
	element := l.PushBack("fist")
	// 使用 element 变量,在 element 的位置后面插入 high 字符串
	l.InsertAfter("high", element)
	// 使用 element 变量,在 element 的位置前面插入 noon 字符串
	l.InsertBefore("noon", element)
	// 移除 element 变量对应的元素
	l.Remove(element)
}

列表元素操作的过程:

操作内容列表元素
l.PushBack(“canon”)canon
l.PushFront(67)67, canon
element := l.PushBack(“fist”)67, canon, fist
l.InsertAfter(“high”, element)67, canon, fist, high
l.InsertBefore(“noon”, element)67, canon, noon, fist, high
l.Remove(element)67, canon, noon, high

4、遍历列表

遍历双链表需要配合Front()函数获取头元素,遍历时只要元素不为空就可以继续进行,每一次遍历都会调用元素的Next()函数,代码如下所示:

package main

import (
	"container/list"
	"fmt"
)

func main() {
	// 创建列表实列
	l := list.New()
	// 尾部添加
	l.PushBack("canon")
	// 头部添加
	l.PushFront(67)
	
    // i:=l.Front() 表示初始赋值,只会在一开始执行一次,每次循环会进行一次 i != nil 语句判断,如果返回 false,表示退出循环,反之则会执行 i = i.Next()
	for i := l.Front(); i != nil; i = i.Next() {
		fmt.Println(i.Value)
	}
}

nil

在Go语言中,布尔类型的零值(初始值)为 false,数值类型的零值为 0,字符串类型的零值为空字符串"",而指针、切片、映射、通道、函数和接口的零值则是nil

nil是Go语言中一个预定义好的标识符,开发者也许会把nil看作其他语言中的 null(NULL),其实这并不是完全正确的,因为Go语言中的nil和其他语言中的null有很多不同点。

  • nil标识符是不能比较的
  • nil不是关键字或保留字
  • nil没有默认类型
  • 不同类型的nil的指针是一样的
  • 不同类型的nil是不能比较的
  • 两个相同类型的nil值也可能无法比较(在Go语言中 map、slice 和 function 类型的 nil 值不能比较,比较两个无法比较类型的值是非法的)
  • nil是map、slice、pointer、channel、func、interface的零值
  • 不同类型的nil值占用的内存大小可能是不一样的

六、流程控制

1、分支结构if else

在Go语言中,关键字 if 是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号{}括起来的代码块,否则就忽略该代码块继续执行后续的代码。

if condition {
    // ...
}

如果存在第二个分支,则可以在上面代码的基础上添加 else 关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行,if 和 else 后的两个代码块是相互独立的分支,只能执行其中一个。

if condition {
    // ...
} else {
    // ...
}

如果存在第三个分支,则可以使用下面这种三个独立分支的形式:

if condition1 {
    // ...
} else if condition2 {
    // d...
}else {
    // ...
}

else if 分支的数量是没有限制的,但是为了代码的可读性,还是不要在 if 后面加入太多的 else if 结构,如果必须使用这种形式,则尽可能把先满足的条件放在前面。

关键字 if 和 else 之后的左大括号{必须和关键字在同一行,如果你使用了 else if 结构,则前段代码块的右大括号}必须和 else if 关键字在同一行,这两条规则都是被编译器强制规定的。

在有些情况下,条件语句两侧的括号是可以被省略的,当条件比较复杂时,则可以使用括号让代码更易读,在使用&&||! 时可以使用括号来提升某个表达式的运算优先级,并提高代码的可读性。

特殊写法

if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,代码如下:

if err := Connect(); err != nil {
    fmt.Println(err)
    return
}

Connect是一个带有返回值的函数,err:=Connect()是一个语句,执行 Connect 后,将错误保存到 err 变量中。

err != nil 才是 if 的判断表达式,当 err 不为空时,打印错误并返回。

这种写法可以将返回值与判断放在一行进行处理,而且返回值的作用范围被限制在 if、else 语句组合中。

在编程中,变量的作用范围越小,所造成的问题可能性越小,每一个变量代表一个状态,有状态的地方,状态就会被修改,函数的局部变量只会影响一个函数的执行,但全局变量可能会影响所有代码的执行状态,因此限制变量的作用范围对代码的稳定性有很大的帮助。

2、循环结构for

与多数语言不同的是,Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构。

初始语句-开始循环时执行的语句

初始语句是在第一次循环前执行的语句,一般使用初始语句执行变量初始化,如果变量在此处被声明,其作用域将被局限在这个 for 的范围内。

初始语句可以被忽略,但是初始语句之后的分号必须要写,代码如下:

step := 2
for ; step > 0; step-- {
    fmt.Println(step)
}

这段代码将 step 放在 for 的前面进行初始化,for 中没有初始语句,此时 step 的作用域就比在初始语句中声明 step 要大。

条件表达式-控制是否循环的开关

每次循环开始前都会计算条件表达式,如果表达式为 true,则循环继续,否则结束循环,条件表达式可以被忽略,忽略条件表达式后默认形成无限循环。

1、结束循环时带可执行语句的无限循环

var i int
for ; ; i++ {
    if i > 10 {
        break
    }
}

2、无线循环

var i int
for {
    if i > 10 {
        break
    }
    i++
}

3、只有一个循环条件的循环

var i int
for i <= 10 {
    i++
}

结束语句-每次循环结束时执行的语句

在结束每次循环前执行的语句,如果循环被 break、goto、return、panic 等语句强制退出,结束语句不会被执行。

3、键值循环for range

for range 结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道(channel),for range 语法上类似于其它语言中的 foreach 语句,一般形式为:

for key, val := range coll {
    ...
}

注:val 始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值。一个字符串是 Unicode 编码的字符(或称之为 rune )集合,因此也可以用它来迭代字符串。

for pos, char := range str {
    ...
}

通过 for range 遍历的返回值有一定的规律:

  • 数组、切片、字符串返回索引和值。
  • map 返回键和值。
  • 通道(channel)只返回通道内的值。

遍历数组、切片-获得索引和值

在遍历代码中,key 和 value 分别代表切片的下标及下标对应的值,下面的代码展示如何遍历切片,数组也是类似的遍历方法:

for key, value := range []int{1, 2, 3, 4} {
    fmt.Printf("key:%d  value:%d\n", key, value)
}

遍历字符串-获得字符

Go语言和其他语言类似,可以通过 for range 的组合,对字符串进行遍历,遍历时,key 和 value 分别代表字符串的索引和字符串中的每一个字符。

m := map[string]int{
    "first": 100,
    "two": 200,
}
for key, value := range m {
    fmt.Println(key, value)
}

注:对 map 遍历时,遍历输出的键值是无序的,如果需要有序的键值对输出,需要对结果进行排序。

遍历通道(channel)-接收通道数据

for range 可以遍历通道(channel),但是通道在遍历时,只输出一个值,即管道内的类型对应的数据。

// 创建一个整型类型的通道
c := make(chan int)

/*
	往通道中推送数据 1、2、3,然后结束并关闭通道
*/
go func() {
    c <- 1
    c <- 2
    c <- 3
    close(c)
}()

// 用for range对通道c进行遍历,其实就是不断地从通道中取数据,直到通道被关闭
for v := range c {
    fmt.Println(v)
}

4、switch

switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上直下逐一测试,直到匹配为止。 Golang switch 分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止。

语法:

switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。

1、一分支多值

当出现多个 case 要放在一起的时:

var a = "mum"
switch a {
case "mum", "daddy":
    fmt.Println("family")
}

不同的 case 表达式使用逗号分隔。

2、分支表达式

case 后不仅仅只是常量,还可以和 if 一样添加表达式,代码如下:

var r int = 11
switch {
case r > 10 && r < 20:
    fmt.Println(r)
}

上述这种情况的 switch 后面不再需要跟判断变量。

5、goto

Go语言中 goto 语句通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助,使用 goto 语句能简化一些代码的实现过程。

使用goto退出多层循环

package main

import "fmt"

func main() {
	for x := 0; x < 10; x++ {
		for y := 0; y < 10; y++ {
			if y == 2 {
				// 跳转到标签
				goto breakHere
			}
		}
	}
	// 手动返回, 避免执行进入标签
	return
	// 标签
breakHere:
	fmt.Println("done")
}

使用 goto 语句后,无须额外的变量就可以快速退出所有的循环。

使用goto集中处理错误

例如重复的错误处理代码,如果后期在这些代码中添加更多的判断,就需要在这些雷同的代码中依次修改,极易造成疏忽和错误。

err := firstCheckError()
if err != nil {
    goto onExit
}

err = secondCheckError()
if err != nil {
    goto onExit
}

fmt.Println("done")
return

onExit:
    fmt.Println(err)
    exitProcess()

6、break

Go语言中 break 语句可以结束 for、switch 和 select 的代码块,另外 break 语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的 for、switch 和 select 的代码块上。

跳出指定循环

package main

import "fmt"

func main() {
// 外层循环的标签
OuterLoop:
	for i := 0; i < 2; i++ {
		for j := 0; j < 5; j++ {
			switch j {
			case 2:
				fmt.Println(i, j)
				// 退出 OuterLoop 对应的循环之外
				break OuterLoop
			case 3:
				fmt.Println(i, j)
				//退出 OuterLoop 对应的循环之外
				break OuterLoop
			}
		}
	}
}

7、continue

Go语言中 continue 语句可以结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用,在 continue 语句后添加标签时,表示开始标签对应的循环。

package main

import "fmt"

func main() {
OuterLoop:
	for i := 0; i < 2; i++ {
		for j := 0; j < 5; j++ {
			switch j {
			case 2:
				fmt.Println(i, j)
				//结束当前循环,开启下一次的外层循环
				continue OuterLoop
			}
		}
	}
}

七、函数

函数是组织好的、可重复使用的、用于执行指定任务的代码块,其可以提高应用的模块性和代码的重复利用率。

Go 语言支持普通函数、匿名函数和闭包,从设计上对函数进行了优化和改进,让函数使用起来更加方便。

1、函数定义

函数声明(定义)包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

func 函数名(形式参数列表)(返回值列表){
    函数体
}

形式参数列表描述了函数的参数名以及参数类型,这些参数作为局部变量,其值由参数调用者提供,返回值列表描述了函数返回值的变量名以及类型,如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。

如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。

2、函数的参数

类型简写

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

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

上面的代码中,intSum函数有两个参数,这两个参数的类型均为int,因此可以省略x的类型,因为y后面有类型说明,x参数也是该类型。

可变参数

可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。

注意:可变参数通常要作为函数的最后一个参数。本质上,函数的可变参数是通过切片来实现的

3、函数的返回值

Go语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数,Go语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误。

Go语言既支持安全指针,也支持多返回值,因此在使用函数进行逻辑编写时更为方便。

一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。

同一种类型返回值

如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。

使用 return 语句返回时,值列表的顺序需要与函数声明的返回值类型一致。

纯类型的返回值对于代码可读性不是很友好,特别是在同类型的返回值出现时,无法区分每个返回参数的意义。

带有变量名的返回值

Go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。

命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。

同一种类型返回值和命名返回值两种形式只能二选一,混用时将会发生编译错误。

4、调用函数

函数在定义后,可以通过调用的方式,让当前代码跳转到被调用的函数中进行执行,调用前的函数局部变量都会被保存起来不会丢失,被调用的函数运行结束后,恢复到调用函数的下一行继续执行代码,之前的局部变量也能继续访问。

函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。

Go语言的函数调用格式如下:

返回值变量列表 = 函数名(参数列表)
  • 函数名:需要调用的函数名。
  • 参数列表:参数变量以逗号分隔,尾部无须以分号结尾。
  • 返回值变量列表:多个返回值使用逗号分隔。

调用有返回值的函数时,可以不接收其返回值。

5、函数变量

把函数作为值保存到变量中

在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中,下面的代码定义了一个函数变量 f,并将一个函数名为 fire() 的函数赋给函数变量 f,这样调用函数变量 f 时,实际调用的就是 fire() 函数,代码如下:

package main

import "fmt"

func main() {
	// 将变量 f 声明为 func() 类型,此时 f 就被俗称为“回调函数”,此时 f 的值为 nil
	var f func()
	// 将 test() 函数作为值,赋给函数变量 f,此时 f 的值为 test() 函数
	f = test
	// 使用函数变量 f 进行函数调用,实际调用的是 test() 函数
	f()
}

func test(){
	fmt.Println("test")
}

6、高阶函数

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

函数作为参数

函数可以作为参数:

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() {
	ret2 := calc(10, 20, add)
	fmt.Println(ret2) //30
}

函数作为返回值

函数也可以作为返回值:

func do(s string) (func(int, int) int, error) {
	switch s {
	case "+":
		return add, nil
	case "-":
		return sub, nil
	default:
		err := errors.New("无法识别的操作符")
		return nil, err
	}
}

7、匿名函数

Go语言支持匿名函数,即在需要使用函数时再定义函数,匿名函数没有函数名只有函数体,函数可以作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式传递,这与C语言的回调函数比较类似,不同的是,Go语言支持随时在代码里定义匿名函数。

匿名函数是指不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成。

匿名函数定义:

func(参数列表)(返回参数列表){
    函数体
}

匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:

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

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

匿名函数多用于实现回调函数和闭包。

8、闭包(Closure)

Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说,函数 + 引用环境 = 闭包

package main

import "fmt"

func main() {
	var f = test()
	fmt.Println(f(1)) //1
	fmt.Println(f(2)) //3
	fmt.Println(f(3)) //6
}

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

变量f是一个函数并且它引用了其外部作用域中的x变量,此时f就是一个闭包。 在f的生命周期内,变量x也一直有效。

在闭包内部修改引用的变量

闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改。例:

// 准备一个字符串
str := "hello Go"

// 创建一个匿名函数
foo := func() {
    // 匿名函数中访问str
    str = "Hello Golang"
}
// 调用匿名函数
foo()

在匿名函数中并没有定义 str,str 的定义在匿名函数之前,此时,str 就被引用到了匿名函数中形成了闭包。

9、defer

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

关键字 defer 的用法类似于面向对象编程语言 JavaC# 的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。

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

使用延迟执行语句在函数退出时释放资源

处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。

defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。

10、处理运行时错误

Go语言的错误处理思想及设计包含以下特征:

  • 一个可能造成错误的函数,需要返回值中返回一个错误接口(error),如果调用是成功的,错误接口将返回 nil,否则返回错误。
  • 在函数调用后需要检查错误,如果发生错误,则进行必要的错误处理。

Go语言没有类似 Java 中的异常处理机制,虽然可以使用 defer、panic、recover 模拟,但官方并不主张这样做,Go语言的设计者认为其他语言的异常机制已被过度使用,上层逻辑需要为函数发生的异常付出太多的资源,同时,如果函数使用者觉得错误处理很麻烦而忽略错误,那么程序将在不可预知的时刻崩溃。

Go语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数,同时,Go语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。

错误接口定义的格式

error 是 Go 系统声明的接口类型,代码如下:

type error interface {
    Error() string
}

所有符合 Error()string 格式的方法,都能实现错误接口,Error() 方法返回错误的具体描述,使用者可以通过这个字符串知道发生了什么错误。

自定义错误

返回错误前,需要定义会产生哪些可能的错误,在Go语言中,使用 errors 包进行错误的定义,格式如下:

var err = errors.New("this is an error")

错误字符串由于相对固定,一般在包作用域声明,应尽量减少在使用时直接使用 errors.New 返回。

1、errors包

// 创建错误对象
func New(text string) error {
    return &errorString{text}
}
// 错误字符串
type errorString struct {
    s string
}
// 返回发生何种错误
func (e *errorString) Error() string {
    return e.s
}

2、在代码中使用错误定义

package main

import (
	"errors"
	"fmt"
)

// 定义除数为0的错误
var errDivisionByZero = errors.New("division by zero")

func div(dividend, divisor int) (int, error) {
	// 判断除数为0的情况并返回
	if divisor == 0 {
		return 0, errDivisionByZero
	}
	// 正常计算,返回空错误
	return dividend / divisor, nil
}
func main() {
	fmt.Println(div(1, 0))
}

11、panic

Go语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。

一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine(可以先理解成线程)中被延迟的函数(defer 机制),随后,程序崩溃并输出日志信息,日志信息包括 panic value 和函数调用的堆栈跟踪信息,panic value 通常是某种错误信息。

对于每个 goroutine,日志信息中都会有与之相对的,发生 panic 时的函数调用堆栈跟踪信息,通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据,因此,在我们填写问题报告时,一般会将宕机和日志信息一并记录。

虽然Go语言的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同,由于 panic 会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,应该使用Go语言提供的错误机制,而不是 panic。

panic()的声明:

func panic(v interface{})    //panic() 的参数可以是任意类型的。

手动触发宕机

Go语言可以在程序中手动触发宕机,让程序崩溃,这样开发者可以及时地发现错误,同时减少可能的损失。

Go语言程序在宕机时,会将堆栈和 goroutine 信息输出到控制台,所以宕机也可以方便地知晓发生错误的位置,那么我们要如何触发宕机呢,示例代码如下所示:

package main

func main() {
    panic("test")
}

代码运行崩溃,输出如下:

panic: test

goroutine 1 [running]:
main.main()
        C:/.../main.go:4 +0x45

在运行的依赖的必备资源缺失时主动触发宕机

regexp 是Go语言的正则表达式包,正则表达式需要编译后才能使用,而且编译必须是成功的,表示正则表达式可用。

编译正则表达式函数有两种,具体如下:

*func Compile(expr string) (Regexp, error)

编译正则表达式,发生错误时返回编译错误同时返回 Regexp 为 nil,该函数适用于在编译错误时获得编译错误进行处理,同时继续后续执行的环境。

*func MustCompile(str string) Regexp

当编译正则表达式发生错误时,使用 panic 触发宕机,该函数适用于直接使用正则表达式而无须处理正则表达式错误的情况。

手动宕机进行报错的方式不是一种偷懒的方式,反而能迅速报错,终止程序继续运行,防止更大的错误产生,不过,如果任何错误都使用宕机处理,也不是一种良好的设计习惯,因此应根据需要来决定是否使用宕机进行报错。

在宕机时触发延迟执行语句

当 panic() 触发的宕机发生时,panic() 后面的代码将不会被运行,但是在 panic() 函数前面已经运行过的 defer 语句依然会在宕机发生时发生作用。

12、recover

Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

通常来说,不应该对进入 panic 宕机的程序做任何处理,但有时,需要我们可以从宕机中恢复,至少我们可以在程序崩溃前,做一些操作,举个例子,当 web 服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭,如果不做任何处理,会使得客户端一直处于等待状态,如果 web 服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。

在其他语言里,宕机往往以异常的形式存在,底层抛出异常,上层逻辑通过 try/catch 机制捕获异常,没有被捕获的严重异常会导致宕机,捕获的异常可以被忽略,让代码继续运行。

Go语言没有异常系统,其使用 panic 触发宕机类似于其他语言的抛出异常,recover 的宕机恢复机制就对应其他语言中的 try/catch 机制。

panic与recover

panic 和 recover 的组合有如下特性:

  • 有 panic 没 recover,程序宕机。
  • 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。

虽然 panic/recover 能模拟其他语言的异常机制,但并不建议在编写普通函数时也经常性使用这种特性。

在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛,直到程序整体崩溃。

如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。

内置函数

内置函数描述
close主要用来关闭channel
len用来求长度,比如string、array、slice、map、channel
new用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make用来分配内存,主要用来分配引用类型,比如chan、map、slice
append用来追加元素到数组、slice中
panic和recover用来做错误处理

八、结构体

1、类型别名与自定义类型

自定义类型

在Go语言中有一些基本的数据类型,如string整型浮点型布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型。

自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:

//将MyInt定义为int类型
type MyInt int

通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

类型别名

类型别名是Go1.9版本添加的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

type TypeAlias = Type

之前见过的runebyte就是类型别名,它们的定义如下:

type byte = uint8
type rune = int32

2、结构体的定义

Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

Go 语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型。Go 语言使用结构体和结构体成员来描述真实世界的实体和实体对应的各种属性。

Go 语言中的类型可以被实例化,使用new&构造的类型实例的类型是类型的指针。

结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:

  • 字段拥有自己的类型和值。
  • 字段名必须唯一。
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型。

使用关键字 type 可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。

结构体的定义格式如下:

type 类型名 struct {
    字段1 字段1类型
    字段2 字段2类型
    …
}
  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • struct{}:表示结构体类型,type 类型名 struct{}可以理解为将 struct{} 结构体定义为类型名的类型。
  • 字段1、字段2……:表示结构体字段名,结构体中的字段名必须唯一。
  • 字段1类型、字段2类型……:表示结构体各个字段的类型。

构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存。

3、结构体实例化

结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段。

实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。

Go语言可以通过多种方式实例化结构体,根据实际需要可以选用不同的写法。

基本实例化

结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化。

基本实例化格式如下:

var 结构体实例 结构体类型

用结构体表示的点结构(Point)的实例化过程请参见下面的代码:

type Point struct {
    X int
    Y int
}
var p Point
p.X = 10
p.Y = 20

在例子中,使用.来访问结构体的成员变量,如p.Xp.Y等,结构体成员变量的赋值方法与普通变量一致。

匿名结构体

在定义一些临时数据结构等场景下还可以使用匿名结构体。

package main

import (
    "fmt"
)

func main() {
    var user struct{Name string; Age int}
    user.Name = "pprof.cn"
    user.Age = 18
    fmt.Printf("%#v\n", user)
}

创建指针类型结构体

Go语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。

结构体实例 := new(结构体类型)

也可以像访问普通结构体一样使用.来访问结构体指针的成员。

取结构体的地址实例化

在Go语言中,对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作,取地址格式如下:

结构体实例 := &结构体类型{}

4、初始化结构体

没有初始化的结构体,其成员变量都是对应其类型的零值。

使用键值对初始化结构体

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。

键值对初始化结构体的书写格式:

结构体实例 := 结构体类型{
    字段1: 字段1的值,
    字段2: 字段2的值,}

键值之间以:分隔,键值对之间以,分隔。

使用多个值的列表初始化结构体

初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:

结构体实例 := 结构体类型{
    字段1的值,
    字段2的值,}
  • 必须初始化结构体的所有字段。
  • 每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  • 键值对与值列表的初始化形式不能混用。

初始化匿名结构体

匿名结构体没有类型名称,无须通过 type 关键字定义就可以直接使用。

匿名结构体的初始化写法由结构体定义和键值对初始化两部分组成,结构体定义时没有结构体类型名,只有字段和类型定义,键值对初始化部分由可选的多个键值对组成,如下格式所示:

ins := struct {
    // 匿名结构体字段定义
    字段1 字段类型1
    字段2 字段类型2}{
    // 字段值初始化
    初始化字段1: 字段1的值,
    初始化字段2: 字段2的值,}

键值对初始化部分是可选的,不初始化成员时,匿名结构体的格式变为:

ins := struct {
    字段1 字段类型1
    字段2 字段类型2}

5、构造函数

Go语言的类型或结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程来模拟实现构造函数。

例如,实现一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

func newPerson(name, city string, age int8) *person {
    return &person{
        name: name,
        city: city,
        age:  age,
    }
}

调用构造函数:

p := newPerson("test","test",18)
fmt.Printf("%#v\n",p)

6、方法和接收者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。

方法的定义格式如下:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}
  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如,Person类型的接收者变量应该命名为 pConnector类型的接收者变量应该命名为c等。
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:具体格式与函数定义相同

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

指针类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。

例如为Person添加一个SetAge方法,来修改实例变量的年龄。

//Person 结构体
type Person struct {
    name string
    age  int8
}

// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
    p.age = newAge
}

func main() {
    p1 := NewPerson("test", 18)
    fmt.Println(p1.age) // 18
    p1.SetAge(20)
    fmt.Println(p1.age) // 20
}

什么时候使用指针类型的接收者

  • 需要修改接收者中的值
  • 接收者是拷贝代价比较大的大对象
  • 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

值类型的接收者

当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

7、任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。

例如,基于内置的int类型使用type关键字可以定义新的自定义类型,然后为自定义类型添加方法。

//MyInt 将int定义为自定义MyInt类型
type MyInt int

//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
    fmt.Println("Hello, 我是一个int。")
}
func main() {
    var m1 MyInt
    m1.SayHello() //Hello, 我是一个int。
    m1 = 100
    fmt.Printf("%#v  %T\n", m1, m1) //100  main.MyInt
}

非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

8、结构体的匿名字段

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

//Person 结构体Person类型
type Person struct {
    string
    int
}

func main() {
    p1 := Person{
        "test",
        18,
    }
    fmt.Printf("%#v\n", p1)        //main.Person{string:"test", int:18}
    fmt.Println(p1.string, p1.int) //test 18
}

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

9、嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针。

package main

import "fmt"

type A struct {
	ax, ay int
}
type B struct {
	A
	bx, by float32
}

func main() {
	b := B{A{1, 2}, 3.0, 4.0}
	fmt.Println(b.ax, b.ay, b.bx, b.by)
	fmt.Println(b.A)
}

结构体嵌套有如下特性:

  • 内嵌的结构体可以直接访问其成员变量

    嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。例如,ins.a.b.c的访问可以简化为ins.c。

  • 内嵌结构体的字段名是它的类型名

    内嵌结构体字段仍然可以使用详细的字段进行一层层访问,内嵌结构体的字段名就是它的类型名。

一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值的情况,编译器在发现可能的赋值歧义时会报错。

嵌套匿名字段

结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字。匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。

//Address 地址结构体
type Address struct {
    Province string
    City     string
}

//User 用户结构体
type User struct {
    Name    string
    Gender  string
    Address //匿名结构体
}

func main() {
    var user2 User
    user2.Name = "test"
    user2.Gender = "男"
    user2.Address.Province = "贵州"    //通过匿名结构体.字段名访问
    user2.City = "贵阳"                //直接访问匿名结构体的字段名
    fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"test", Gender:"男", Address:main.Address{Province:"贵州", City:"贵阳"}}
}

当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。

嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。

10、结构体的继承

Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。

//Animal 动物
type Animal struct {
	name string
}

func (a *Animal) move() {
	fmt.Printf("%s会动!\n", a.name)
}

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

func (d *Dog) wang() {
	fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
	d1 := &Dog{
		Feet: 4,
		Animal: &Animal{ //注意嵌套的是结构体指针
			name: "小黑",
		},
	}
	d1.wang() //小黑会汪汪汪~
	d1.move() //小黑会动!
}

11、结构体字段的可见性

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

12、结构体与JSON序列化

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。

//Student 学生
type Student struct {
	ID     int
	Gender string
	Name   string
}

//Class 班级
type Class struct {
	Title    string
	Students []*Student
}

func main() {
	c := &Class{
		Title:    "101",
		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)
	}
	//JSON序列化:结构体-->JSON格式的字符串
	data, err := json.Marshal(c)
	if err != nil {
		fmt.Println("json marshal failed")
		return
	}
	fmt.Printf("json:%s\n", data)
	//JSON反序列化:JSON格式的字符串-->结构体
	str := `{"Title":"101","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)
}

13、结构体标签(Tag)

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

`key1:"value1" key2:"value2"`

结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。

注: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

例如为Student结构体的每个字段定义json序列化时使用的Tag:

package main

import (
	"encoding/json"
	"fmt"
)

//Student 学生
type Student struct {
	ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
	Gender string //json序列化是默认使用字段名作为key
	name   string //私有不能被json包访问
}

func main() {
	s1 := Student{
		ID:     1,
		Gender: "男",
		name:   "张三",
	}
	data, err := json.Marshal(s1)
	if err != nil {
		fmt.Println("json marshal failed!")
		return
	}
	fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}

14、垃圾回收和SetFinalizer

Go语言自带垃圾回收机制(GC)。GC 通过独立的进程执行,它会搜索不再使用的变量,并将其释放。需要注意的是,GC 在运行时会占用机器资源。

GC 是自动进行的,如果要手动进行 GC,可以使用 runtime.GC() 函数,显式的执行 GC。显式的进行 GC 只在某些特殊的情况下才有用,比如当内存资源不足时调用 runtime.GC() ,这样会立即释放一大片内存,但是会造成程序短时间的性能下降。

finalizer(终止器)是与对象关联的一个函数,通过 runtime.SetFinalizer 来设置,如果某个对象定义了 finalizer,当它被 GC 时候,这个 finalizer 就会被调用,以完成一些特定的任务,例如发信号或者写日志等。

在Go语言中 SetFinalizer 函数是这样定义的:

func SetFinalizer(x, f interface{})
  • 参数 x 必须是一个指向通过 new 申请的对象的指针,或者通过对复合字面值取址得到的指针。
  • 参数 f 必须是一个函数,它接受单个可以直接用 x 类型值赋值的参数,也可以有任意个被忽略的返回值。

SetFinalizer 函数可以将 x 的终止器设置为 f,当垃圾收集器发现 x 不能再直接或间接访问时,它会清理 x 并调用 f(x)。

另外,x 的终止器会在 x 不能直接或间接访问后的任意时间被调用执行,不保证终止器会在程序退出前执行,因此一般终止器只用于在长期运行的程序中释放关联到某对象的非内存资源。例如,当一个程序丢弃一个 os.File 对象时没有调用其 Close 方法,该 os.File 对象可以使用终止器去关闭对应的操作系统文件描述符。

终止器会按依赖顺序执行:如果 A 指向 B,两者都有终止器,且 A 和 B 没有其它关联,那么只有 A 的终止器执行完成,并且 A 被释放后,B 的终止器才可以执行。

如果 *x 的大小为 0 字节,也不保证终止器会执行。

此外,我们也可以使用SetFinalizer(x, nil)来清理绑定到 x 上的终止器。

终止器只有在对象被 GC 时,才会被执行。其他情况下,都不会被执行,即使程序正常结束或者发生错误。

九、接口

接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

在Go语言中接口(interface)是一种类型,一种抽象的类型。

interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)

1、接口声明

type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2}
  • 接口类型名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

2、实现接口的条件

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

  • 接口的方法与实现接口的类型方法格式一致。
  • 接口中所有方法均被实现。

3、类型与接口的关系

一个类型实现多个接口

个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。

多个类型实现同一接口

一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。也就是说,使用者并不关心某个接口的方法是通过一个类型完全实现的,还是通过多个结构嵌入到一个结构体中拼凑起来共同实现的。

4、空接口

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

;