Go语言第一课

#1 入门必备

##1.1 Go语言简介

###1.1.1 Go语言的特点

  1. 静态类型(需要指明类型或至少能推导出来)、编译型的开源语言
  2. 脚本化的语法,支持多种编程范式:函数式编程、面向对象
  3. 原生、给力的并发编程支持:原生支持并发和通过函数库支持并发是有明显区别的

##1.2 优势与劣势

###1.2.1 Go语言的优势

  • 脚本化的语法
  • 静态类型+编译型:程序运行速度有保证
  • 原生支持并发编程:有利于服务端软件开发优势
  1. 降低开发、维护成本
  2. 程序可以更好的运行

###1.2.2 Go语言的劣势

  1. 语法糖没有python和ruby多
  2. 目前的程序运行速度不如c
  3. 第三方函数库暂时不像绝对主流的编程语言那么多

##1.3 安装

###1.3.1 下载

https://storage.googleapis.com/golang/go1.4.2
http://golang.org/dl

###1.3.2 安装

1
2
3
$ tar -zxvf go1.4.2.linux-amd64.tar.gz -C /usr/local
$ cd /usr/local/go
$ ./bin/go version

##1.4 linux下的设置

###1.4.1 需要设置什么

  • 4个环境变量:GOROOT、GOPATH、GOBIN、PATH
1
2
3
4
5
6
7
8
9
10
11
12
13
#或/etc/profile
$ vim ~/.bash_profile
#Go语言当前安装目录
export GOROOT=/usr/local/go
#Go语言工作区集合
export GOPATH=~/golib:~/goproject
#Go程序的可执行文件目录
export GOBIN=~/gobin
#方便使用Go语言命令和Go程序的可执行文件,需要追加其值,如:
export PATH=$PATH:$GOROOT/bin:$GOBIN

$ source ~/.bash_profile
$ go version

#2 基本规则

##2.1 工作区和GOPATH

###2.1.1 工作区

含义:放置Go源码文件的目录
注意:一般情况下,Go源码文件都需要存放到工作区中,但是对于命令文件来说,这不是必须的。
平台相关目录:两个隐藏的Go语言环境变量(GOOSGOARCH
bin目录注意事项:

  1. 当环境变量呢GOBIN已有效设置时,该目录会变的无意义
  2. GOPATH的值中包含多个工作区的路径时,必须设置GOBIN,否则无法成功安装Go程序的可执行文件
1
2
3
4
5
├── bin//用于存放当前工作区中的Go程序的可执行文件
├── pkg//用于存放归档文件(名称以.a为后缀的文件),所有归档文件都会被存放到该目录下的平台相关目录中,同样以代码包为组织形式
└── src//用于存放源码文件,以代码包为组织形式;可以有若干级的子目录
└── main
└── main.go

##2.2 源码文件的分类和含义

##2.2.1 Go源码文件

特点:

  1. 名称以.go为后缀,内容以Go语言代码组织的文件
  2. 多个Go源码文件是需要用代码包组织起来的

分类:

  1. 命令源码文件
  2. 库源码文件
  3. 测试源码文件(辅助源码文件)

####1. 命令源码文件

特点: 声明自己属于main代码包、包含无参数声明和结果声明的main函数
安装行为: 被安装后,相应的可执行文件会被存放到GOBIN指向的目录或<当前工作目录/bin>

注意事项:

  1. 命令源码文件是Go程序的入口,但不建议把程序都写在一个文件中
  2. 同一个代码包中强烈不建议直接包含多个命令源码文件

####2.库源码文件

特点:不具备命令源码文件的那两个特征(普通源码文件)
安装行为:被安装后,相应的归档文件回被存放到<当前工作区目录>/pkg/<平台相关目录>

####3.测试源码文件

特点:

  1. 不具备命令源码文件的那两个特征的源码文件,名称以_test.go为后缀
  2. 其中至少有一个函数的名称以TestBenchmark为前缀,接受一个类型为*testing.T*testing.B的参数
1
2
3
4
5
6
7
8
9
//功能测试函数
func TestFind(t *testing.T){
//省略若干条语句
}

//性能测试函数
func BenchmarkFind(b *testing.B){
//省略若干条语句
}

##2.3 代码包相关知识

作用:

  1. 编译和归档Go程序的最基本单位
  2. 代码划分、集结和依赖的有效组织形式,也是权限控制的辅助手段

说明:一个代码包实际上就是一个由导入路径代表的目录

2.3.1 导入路径:以下目录下的某段子路径

  • <工作区目录>/src
  • <工作区目录>/pkg/<平台相关目录>
    例如:代码包eli.cn,/home/eli/golib/是一个工作区目录
1
/home/eli/golib/src/eli.cn

###2.3.2 代码包的声明

#####语法

  • 每个源码文件必须声明其所属的代码包
  • 同一个代码包中的所有源文件声明的代码包应该是相同的

注意:代码包声明是代码包导入路径的最右子路径

1
2
//对应导入路径:eli.cn/phgtool
package pkgtool

###2.3.3 代码包的导入

语法:代码包导入语句中使用的包名称应该与其导入路径一致

1
2
3
4
5
import(
"flag"
"fmt"
"strings"
)

####2.3.3.1 导入方式

  • 带别名的导入:使用别名来调用导入的包中的成员
1
2
import str "strings"
str.HasPrefix("abc","a")
  • 本地化导入:调用导入的包的成员时可以省略包名
1
2
import . "strings"
HasPrefix("abc","a")
  • 仅仅初始化:仅执行代码包中的初始化函数
1
import _ "strings"

代码包初始化函数:无参数声明和结果声明的init函数
特点:init函数可以被声明在任何文件中,且可以有多个

####2.3.3.2 init函数的执行

  • 同一代码包中的执行情况
    注意:不保证同一包中多个init函数执行顺序
1
2
3
4
5
6
st=>start: 导入代码包
e=>end
op1=>operation: 代码包中的所有全局变量被求值
op2=>operation: 代码包中的所有init函数被执行

st->op1->op2->e
  • 不同代码包之间init的执行
    递归导入的情况:先导入后执行(A导入B导入C)
    C->B->A
    一个文件导入多个包:导入多个包时,不保证多个包之间的执行顺序

#3 命令基础

##3.1 go run命令简介

###3.1.1 go run

用途:线编译文件再运行
使用:只能接受一个命令源码文件以及若干个库源码文件作为参数

###3.1.2 go build、go install

###3.1.3 go get

##3.2 示例来源说明

《Go并发编程实战》示例项目:https://github.com/hyper-carrot/goc2p.git

  • ds命令:显示指定目录的目录结构(goc2p/src/helper/ds/showds.go)
  • pds命令:显示指定代码包的依赖关系(goc2p/src/helper/pds/showpds.go)

##3.3 go run 命令案例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ go run showds.go -p ~/Music
/Users/tonyearth/Music/:
iTunes/
Album Artwork/
Cache/
Cloud/
Cloud Purchases/
Download/
iTunes Library Extras.itdb
iTunes Library Genius.itdb
iTunes Library.itl
iTunes Media/
Automatically Add to iTunes.localized/
iTunes Music Library.xml
sentinel

##3.4-3.5 go run常用标记的使用

参数 含义
-a 强制编译相关代码,不论它们的编译结果是不是最新的,即已经编译过的部分也会被重新编译
-n 打印编译过程中所需运行的命令,但不真正执行它们
-p n 并行编译,其中n为并行的数量
-v 列出被编译的代码包的名称(1.3包含标准库,1.4不包含)
-work 显示编译时创建的临时工作目录的路径,并且不删除它
-x 打印编译过程中所需进行的命令,并执行它们

##3.6-3.7 go build命令简介

用途:用于编译源码文件或代码包
执行特点:

  1. 编译非命令文件(库文件),不会产生任何结果文件
  2. 编译命令文件,会在该命令的执行目录中生成一个可执行文件

参数:

参数 用途
不加任何参数 它会试图把当前目录作为代码包并编译
以代码包的导入路径作为参数时 该代码包及其依赖会被编译
-a 所有涉及到的代码包都会被重新编译(没有该参数只会编译归档文件而不是最新的代码包)
若干源码文件 只有这些文件会被编译

##3.8-3.9 go install 命令简介

用途:用于编译代码包或源码文件
安装:

安装对象 生成
代码包 会在当前工作区的pkg/<平台相关目录>下生成归档文件
命令源码文件 会在当前工作区的bin目录或$GOBIN目录下生成可执行文件

参数:

参数 用途
不追加参数 会试图把当前目录作为代码包并安装
以代码包的导入路径作为参数 该代码包及其依赖会被安装
以命令源码文件及其相关库源码文件作为参数 只有这些文件会被编译并安装

##3.10-3.11 go get简介

用途:从远程代码仓库(如github)上下载并安装代码包
受支持的代码版本控制系统:Git,Mercurial(hg),SVN,Bazaar
放置路径:$GOPATH中包含的第一个工作区的src目录中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//下载
$ go get -x github.com/go-errors/errors

//查看第一个工作区
$ echo $GOPATH
/Users/tonyearth/local/golib:/Users/tonyearth/local/goproject

//查看下载和安装的情况
$ tree -L 3 ~/local/golib/
/Users/tonyearth/local/golib/
├── pkg
│ └── darwin_amd64
│ └── github.com
└── src
└── github.com
└── go-errors

##3.12-3.13 go get常用标记

参数 意义
-d 只执行下载动作,而后进行编译和安装
-fix 在下载代码包后先执行修正动作(修正其中不受支持的旧版本go的代码),而后再进行编译和安装
-u 利用网络来更新已有的代码包及其依赖

##3.14 本章总结

#4 基本数据类型

##4.1 程序实体和关键字

###4.1.1 程序实体

包含:变量、常量、函数、结构体和接口

###4.1.2 标识符

书写规范:可以是任何Unicode编码可以表示的字母字符、数字以及下划线“_”。不过,首字母不能是数字或下划线。
注意:名字首字母为大写的程序实体可以被任何代码包中的代码访问到。而名字首字母为小写的程序实体则只能被同一个代码包中的代码所访问。

###4.1.3 关键字

用途 关键字
程序声明 import, package
程序实体声明和定义 chan,const,func,interface,map,struct,type,var
程序流程控制 go,select,break,case,continue,default, defer, else, fallthroughfor ,goto,if,range,return,switch
1
2
3
4
5
6
7
8
9
10
11
12
13
package main  // 代码包声明语句。

// 代码包导入语句。
import (
"fmt" // 导入代码包fmt。
)

// main函数。
func main() {

// 打印函数调用语句。用于打印输出信息。
fmt.Println("Go语言编程实战")
}

##4.2 变量和常量

比较 变量 常量
关键字 var const
赋值 绝大多数的数据类型的值,包括函数 字面量
声明 可以不赋值 必须赋值
1
2
3
4
5
6
7
8
9
10
11
12
// 注释:普通赋值,由关键字var、变量名称、变量类型、特殊标记=,以及相应的值组成。
// 若只声明不赋值,则去除最后两个组成部分即可。
var num1 int = 1

//或:
var num2, num3 int = 2, 3 // 注释:平行赋值

//或:
var ( // 注释:多行赋值
num4 int = 4
num5 int = 5
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
)

func main() {
var (

num1 int
num2 int
num3 int

)
num1, num2, num3 = 1, 2, 3
// 打印函数调用语句。用于打印上述三个变量的值。
fmt.Println(num1, num2, num3)
}

##4.3 整数类型的命名和宽度

###4.3.1 种类:10种

数据类型 有符号 类型宽度(bit)
int 平台相关
int8 8
int16 16
int32 32
int64 64
uint 平台相关
uint8 8
uint16 16
uint32 32
uint64 64

###4.3.2 有无符号整数的区别

有符号整数:使用最高位的比特(bit)表示整数的正负
无符号整数:使用所有的比特位来表示数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main // 代码包声明语句

// 代码包导入语句
import (
"fmt" // 导入代码包fmt。
)

// main函数
func main() { // 代码块由“{”和“}”包裹。

// 变量声明和赋值语句,由关键字var、变量名num、变量类型uint64、特殊标记=,以及值10组成。
var num uint64 = 65535

// 短变量声明语句,由变量名size、特殊标记:=,以及值(需要你来填写)组成。
size := 8

// 打印函数调用语句。在这里用于描述一个uint64类型的变量所需占用的比特数。
// 这里用到了字符串的格式化函数。
fmt.Printf("类型为 uint64 的整数 %d 需占用的存储空间为 %d 个字节。\n", num, size)
}

##4.4 整数类型值的表示法

###4.4.1 大小

类型宽度(比特) 数值范围(有符号整数) 数值范围(无符号整数)
8 -128~127 0~255
16 -32768~32767 0~65535
32 约-21.47~21.47亿 约0~42.94亿
64 约-922亿亿~922亿亿 约0~1844亿

###4.4.2 进制

1
2
3
var num1 int = 12//十进制
num1 = 014 // 用“0”作为前缀以表明这是8进制表示法。
num1 = 0xC // 用“0x”作为前缀以表明这是16进制表示法。
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
)

func main() {
// 声明一个整数类型变量并赋值
var num1 int = -0x1000

// 这里用到了字符串格式化函数。其中,%X用于以16进制显示整数类型值,%d用于以10进制显示整数类型值。
fmt.Printf("16进制数 %X 表示的是 %d。\n", num1, -4096)
}

##4.5 浮点数类型

注意:在Go语言里,浮点数的相关部分只能由10进制表示法表示,而不能由8进制表示法或16进制表示法表示。

###4.5.1 分类

浮点类型 宽度
float32 4
float64 8

###4.5.2 表示方式

  • 普通方式:整数部分、小数点“.”和小数部分组成
  • 指数方式:在其中加入指数部分。指数部分由“E”或“e”以及一个带正负号的10进制数组成

###4.5.3 简化形式

说明:比如,37.0可以被简化为37。又比如,0.037可以被简化为.037。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
)

func main() {
// 可以在变量声明并赋值的语句中,省略变量的类型部分。
// 不过别担心,Go语言可以推导出该变量的类型。
var num2 = 5.89E-4

// 这里用到了字符串格式化函数。其中,%E用于以带指数部分的表示法显示浮点数类型值,%f用于以通常的方法显示浮点数类型值。
fmt.Printf("浮点数 %E 表示的是 %f。\n", num2, ( 0.000589 ))
}

##4.6 复数类型

表示方式:复数类型的值一般由浮点数表示的实数部分、加号“+”、浮点数表示的虚数部分,以及小写字母“i”组成。

###4.6.1 分类

复数类型 宽度(bit) 说明
comples64 8 由两个float32类型的值分别表示实数部分和虚数部分
complex128 16 由两个float64类型的值分别表示实数部分和虚数部分

###4.6.2 实例

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)

func main() {
var num3 = 3.7E+1 + 5.98E-2i

// 这里用到了字符串格式化函数。其中,%E用于以带指数部分的表示法显示浮点数类型值,%f用于以通常的方法显示浮点数类型值。
fmt.Printf("浮点数 %E 表示的是 %f。\n", num3, 37.000000+0.059800i)
}

##4.7 byte和rune

###4.7.1 别名类型

别名类型 对应类型 用途
byte unit8 用来8bit的整数或字符
rune int32 用来表示Unicode字符

###4.7.2 rune支持的表示方式

注意:rune类型值可以用多种方式表示,其中第一种最直观通用

表示形式 案例 支持的字符范围
直接使用Unicode字符 '郝' Unicode支持的字符
“\x”加两个十六进制数 '\x41'(表示”A”) ASCLL编码的字符
“\”加三位八进制数 '\101'(表示"A") 编码值在[0,255)内的字符
“\u”加四位十六进制数 \u90DD(表示”郝”) 编码值在[0,65535)内的字符
“\U”加四位十六进制数 \U000090DD(表示”郝”) 所有Unicode字符

###4.7.3 rune支持的转义字符

转义符 Unicode代码点 说明
\a U+0007 告警铃声或蜂鸣声
\b U+008 退格符
\f U+000C 换页符
\n U+000A 换行符
\r U+000D 回车符
\t U+0009 水平制表符
\v U+000b 垂直制表符
\ U+005c 反斜杠
\’ U+0027 单引号。仅在rune值中有效
U+0022 双引号。仅在string值中有效
1
2
3
4
5
6
7
8
9
10
package main
import (
"fmt"
)
func main() {
// 声明一个rune类型变量并赋值
var char1 rune = '赞'
// 这里用到了字符串格式化函数。其中,%c用于显示rune类型值代表的字符。
fmt.Printf("字符 '%c' 的Unicode代码点是 %s。\n", char1, ("U+8D5E"))
}

##4.8 字符串类型

特点:不可变。一旦创建了一个字符串类型的值就不可能再对它本身做任何修改。

###4.8.1 底层实现

  • 一个字符串值却是由若干个字节来表现和存储的
  • 一个字符串(也可以说字符序列)会被Go语言用Unicode编码规范中的UTF-8编码格式编码为字节数组

###4.8.2 两种表示方法

表示方法 语法 行为
原生表示法 反引号`把字符序列包裹起来 前者表示的值是所见即所得的(除了回车符)
解释型表示法 则需用双引号“””包裹字符序列 后者所表示的值中的转义符会起作用并在程序编译期间被转义
1
2
3
4
5
6
7
8
9
10
package main
import (
"fmt"
)
func main() {
// 声明一个string类型变量并赋值
var str1 string = "\\\""
// 这里用到了字符串格式化函数。其中,%q用于显示字符串值的表象值并用双引号包裹。
fmt.Printf("用解释型字符串表示法表示的 %q 所代表的是 %s。\n", str1, `\"`))
}

#5 高级数据类型

##5.1 数组类型

数组: 一个数组(Array)就是一个可以容纳若干类型相同的元素的容器。这个容器的大小(即数组的长度)是固定的,且是体现在数组的类型字面量之中的。

###5.1.1 类型声明

关键字:type
语法:type [类型别名] [类型字面量]
用途:为某种类型声明一个别名类型,使我们可以把自定义的别名类型当作该类型来使用。

1
type MyNumbers [3]int

###5.1.2 字面量

类型字面量:就是用于表示某个类型的字面表示(或称标记方法),比如[3]int
值字面量:用于表示某个类型的值的字面表示可被称为值字面量,或简称为字面量。比如之前提到过的3.7E-2就可被称为浮点数字面量。

###5.1.3 数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
var numbers2 [5]int
numbers2[0] = 2
numbers2[3] = numbers2[0] - 3
numbers2[1] = numbers2[2] + 5
numbers2[4] = len(numbers2)
sum := 11
// “==”用于两个值的相等性判断
fmt.Printf("%v\n", (sum == numbers2[0]+numbers2[1]+numbers2[2]+numbers2[3]+numbers2[4]))
}

####5.1.3.1 声明

1
2
//这是一条变量声明语句。它在声明变量的同时为该变量赋值。
var numbers = [3]int{1, 2, 3}

1
2
//在其中的类型字面量中省略代表其长度的数字
var numbers = [...]int{1, 2, 3}

####5.1.3.2 索引

方式:索引表达式由字符串、数组、切片或字典类型的值(或者代表此类值的变量或常量)和由方括号包裹的索引值组成。
注意:

  1. 索引值既不能小于0也不能大于或等于数组值的长度
  2. 索引值的最小有效值总是0,而不是1
1
2
3
numbers[0] // 会得到第一个元素
numbers[1] // 会得到第二个元素
numbers[2] // 会得到第三个元素

####5.1.3.2 修改

1
numbers[1] = 4

####5.1.3.3 len

用途:len是Go语言的内建函数的名称。该函数用于获取字符串、数组、切片、字典或通道类型的值的长度。我们可以在Go语言源码文件中直接使用它。

1
var length = len(numbers)

####5.1.3.4 默认值

规则:如果我们只声明一个数组类型的变量而不为它赋值,那么该变量的值将会是指定长度的、其中各元素均为元素类型的零值(或称默认值)的数组值。

1
var numbers2 [5]int//[5]int{0, 0, 0, 0, 0}

####5.1.3.5 切片表达式

用途:可以在一个数组或切片上进行“切片”操作,获得一个新的切片。
语法:切片表达式一般由字符串、数组或切片的值以及由方括号包裹且由英文冒号“:”分隔的两个正整数组成。这两个正整数分别表示元素下界索引和元素上界索引。

1
2
var numbers3 = [5]int{1, 2, 3, 4, 5}
var slice1 = numbers3[1:4]

##5.2 切片类型

特点:

  1. 被“切下”的部分不包含元素上界索引指向的元素。
  2. 切片表达式的求值结果会是切片类型的,且其元素类型与被“切片”的值的元素类型一致。
  3. 切片类型属于引用类型,默认值nil

###5.2.1 数组与切片

相同点:切片(Slice)与数组一样,也是可以容纳若干类型相同的元素的容器
不同点:

  1. 无法通过切片类型来确定其值的长度。
  2. 不同长度的切片值是有可能属于同一个类型的;不同长度的数组值必定属于不同类型。

联系:每个切片值都会将数组作为其底层数据结构。我们也把这样的数组称为切片的底层数组。

###5.2.2 切片字面量

语法:[]{类型}
特点:

  1. 不包含代表其长度的信息,不同长度的切片值是有可能属于同一个类型的
1
[]int

1
[]string

###5.2.3 切片声明和创建

注意:切片是引用类型,底层是数组,对切片的成员的修改实际上是对底层数组的修改。

####5.2.3.1 声明切片类型

1
type MySlice []int

####5.2.3.2 创建切片类型

注意:作为切片表达式求值结果的切片值的长度总是为元素上界索引与元素下界索引的差值。
方式一:创建

1
var slice3 []int//nil

方式二:切片操作

1
2
var numbers3 = [5]int{1, 2, 3, 4, 5}
var slice1 = numbers3[1:4]

####5.2.3.3 容量

注意:除了长度,切片值以及数组值还有另外一个属性——容量。数组值的容量总是等于其长度。而切片值的容量则往往与其长度不同。

  • cap:获得切片的容量(即为它的第一个元素值在其底层数组中的索引值与该数组长度的差值的绝对值)
1
2
3
4
5
6
7
8
9
package main
import "fmt"
func main() {
var numbers3 = [5]int{1, 2, 3, 4, 5}
slice3 := numbers3[2 : len(numbers3)]
length := 3
capacity := 3
fmt.Printf("%v, %v\n", (length == len(slice3)), (capacity == cap(slice3)))
}

##5.3 切片的更多操作方法

###5.3.1 限制切片容量

用途:限制我们对其底层数组中的更多元素的访问。
语法:在切片操作中加入第3个正整数作为容量

1
2
3
4
5
6
7
var numbers3 = [5]int{1, 2, 3, 4, 5}

//创建切片的同时设置容量上限
var slice1 = numbers3[1:4:4] //{2,3,4}

//尝试将其长度延展得与其容量相同(会引发一个运行时恐慌)
slice1 = slice1[:cap(slice1)]

###5.3.2 扩展切片

方法:append函数
用途:会对切片值进行扩展并返回一个新的切片值
注意:一旦扩展操作超出了被操作的切片值的容量,那么该切片的底层数组就会被自动更换(不影响原本底层数组)

1
2
//slice1 = []int{2,3,4}
slice1 = append(slice1, 6, 7)//[]int{2, 3, 4, 6, 7}

###5.3.3 复制切片

方法:copy函数
用途:该函数接受两个类型相同的切片值作为参数,并会把第二个参数值中的元素复制到第一个参数值中的相应位置(索引值相同)上。
注意:

  1. 这种复制遵循最小复制原则,即:被复制的元素的个数总是等于长度较短的那个参数值的长度。
  2. 与append函数不同,copy函数会直接对其第一个参数值进行修改。
1
2
var slice4 = []int{0, 0, 0, 0, 0, 0, 0}
copy(slice4, slice1) //slice4:[]int{2, 3, 4, 6, 7, 0, 0}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
func main(){
//创建切片
var numbers4 = [...]int{1,2,3,4,5,6,7,8,9,10}
slice5 := numbers4[4:6:8]//{5,6}
fmt.Printf("capality:%v,length:%v\n",cap(slice5), len(slice5))

//扩展切片刀最大容量处
slice5 = slice5[:cap(slice5)]//{5,6,7,8}

//使用append扩展
slice5 = append(slice5, 11, 12, 13)//{5,6,7,8,11,12,13}

//复制切片
slice6 := []int{0,0,0}
copy(slice5,slice6)//{0,0,0,8,11,12,13}

fmt.Printf("%v, %v, %v\n", slice5[2], slice5[3], slice5[4])//0,8,11
}

##5.4 字典类型

字典类型:map[K]T(“K”意为键的类型,而“T”则代表元素(或称值)的类型)
说明:Go语言的字典(Map)类型其实是哈希表(Hash Table)的一个实现。
用途:字典用于存储键-元素对(更通俗的说法是键-值对)的无序集合。
特点:

  1. 同一个字典中的每个键都是唯一的
  2. 字典的键类型必须是可比较的,否则会引起错误。也就是说,它不能是切片、字典或函数类型。
  3. 对于字典值来说,如果其中不存在索引表达式欲取出的键值对,那么就以它的值类型的空值(或称默认值)作为该索引表达式的求值结果。
  4. 字典类型为引用类型,零值为nil

###5.4.1 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1:创建。首先,最左边是类型字面量,右边紧挨着由花括号包裹且有英文逗号分隔的键值对。每个键值对的键和值之间由英文冒号分隔。
mm := map[int]string{1: "a", 2: "b", 3: "c"}

//2.取值。运用索引表达式取出字典中的值
b := mm[2]

//3.赋值。利用索引表达式来赋值
mm[2] = b + "2"

//4.添加
mm[4] = ""

//5.删除。有则删除,无则不做
delete(mm, 4)

###5.4.2 使用多返回值

说明:针对字典的索引表达式可以有两个求值结果
用途:第二个求值结果是bool类型的,它用于表明字典值中是否存在指定的键值对

1
e, ok := mm[5]//变量ok必为false。因为mm中不存在以5为键的键值对。

##5.5 通道类型

说明:通道(Channel)是Go语言中一种非常独特的数据结构。它可用于在不同Goroutine之间传递类型化的数据,并且是并发安全的。
特点:

  1. 并发安全
  2. 无法用字面量来为通道类型的变量赋值(只能通过调用内建函数make来达到目的)
  3. 与切片和字典类型相同,通道类型属于引用类型。它的零值即为nil。

注意:

  1. 对通道值的重复关闭会引发运行时恐慌。这会使程序崩溃。
  2. 在通道值有效的前提下,针对它的发送操作会在通道值已满(其中缓存的数据的个数已等于它的长度)时被阻塞。
  3. 向一个已被关闭的通道值发送数据会引发运行时恐慌
  4. 针对有效通道值的接收操作会在它已空(其中没有缓存任何数据)时被阻塞

###5.5.1 make

说明:第一个参数是代表了将被初始化的值的类型的字面量(比如chan int),而第二个参数则是值的长度。
用途:除了通道类型,make函数也可以被用来初始化切片类型或字典类型的值。
特点:

  1. 通道值的长度应该被称为其缓存的尺寸。换句话说,它代表着通道值中可以暂存的数据的个数。
  2. 暂存在通道值中的数据是先进先出的,即:越早被放入(或称发送)到通道值的数据会越先被取出(或称接收)。
1
2
//初始化一个长度为5且元素类型为int的通道值
make(chan int, 5)

###5.5.2 基本操作

1
2
3
4
5
6
7
8
9
10
11
//1.创建通道
ch1 := make(chan string, 5)

//2.可以使用接收操作符<-向通道值发送数据
ch1 <- "value1"

//3.也可以使用它从通道值接收数据
value := <- ch1

//4.关闭通道
close(ch1)

###5.5.3 多返回值

用途:消除与零值有关的歧义
解释:如果在接收操作进行之前或过程中通道值被关闭了,则接收操作会立即结束并返回一个该通道值的元素类型的零值。

1
2
//这里的变量ok的值同样是bool类型的。它代表了通道值的状态,true代表通道值有效,而false则代表通道值已无效(或称已关闭)
value, ok := <- ch1

###5.5.4 案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
ch2 := make(chan string, 1)
// 下面就是传说中的通过启用一个Goroutine来并发的执行代码块的方法。
// 关键字 go 后跟的就是需要被并发执行的代码块,它由一个匿名函数代表。
// 对于 go 关键字以及函数编写方法,我们后面再做专门介绍。
// 在这里,我们只要知道在花括号中的就是将要被并发执行的代码就可以了。
go func() {
ch2 <- "已到达"
}()
var value string = "数据"
value = value + <-ch2
fmt.Println(value)
}

##5.6 通道的更多种类

##5.6.1 按照缓存数据数量划分

非缓冲意味着:发送方在向通道值发送数据的时候会立即被阻塞,直到有某一个接收方已从该通道值中接收了这条数据。

分类 缓存数据数量 案例 特点
缓冲通道 >=1 make(chan string,1) 可以缓存N个数据
非缓冲通道 0 make(chan int, 0) 不会缓存任何数据

###5.6.2 按照数据在通道中的传输方向划分

单向通道的用途:主要作用是约束程序对通道值的使用方式。比如:

  1. 调用一个函数时给予它一个发送通道作为参数,以此来约束它只能向该通道发送数据
  2. 一个函数将一个接收通道作为结果返回,以此来约束调用该函数的代码只能从这个通道中接收数据

注意:

  1. 可以将双向通道类型复制给当想通道,反之则不行
  2. 在初始化一个通道值的时候不能指定它为单向。但是,在编写类型声明的时候,我们却是可以这样做的。
通道类型 声明相应类型 特点
双向通道 make(chan int, 3) 默认情况下,通道都是双向的,即双向通道。
接收通道 type Receiver <-chan int 类型Receiver代表了一个只可从中接收数据的单向通道类型
发送通道 type Sender chan<- int 类型Sender代表了一个只可以向其中发送数据的单向通道类型
1
2
3
4
5
6
7
8
9
10
11
12
//声明一个接受通道类型
type Receiver <-chan int

//声明一个发送通道类型
type Sender chan<- int

//创建一个双向通道
var myChannel = make(chan int, 3)

//将双向通道赋值给单项通道
var sender Sender = myChannel
var receiver Receiver = myChannel

###5.6.3 案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

package main

import (
"fmt"
"time"
)

type Sender chan<- int

type Receiver <-chan int

func main() {
var myChannel = make(chan int, (0))//非缓冲通道
var number = 6
//向发送通道中丢数据
go func() {
var sender Sender = myChannel
sender <- number//如果数据不被重通道中取出就会被堵塞
fmt.Println("Sent!")
}()

//从接收通道中读数据
go func() {
var receiver Receiver = myChannel //会被堵塞直到发现有数据存在
fmt.Println("Received!", <-receiver)
}()
// 让main函数执行结束的时间延迟1秒,
// 以使上面两个代码块有机会被执行。
time.Sleep(time.Second)
}

#6 高级数据类型2

##6.1 函数

特点:

  1. 可以把函数作为值来传递和使用
  2. 它接受若干输入(参数),并经过一些步骤(语句)的执行之后再返回输出(结果)
  3. 可以返回多个结果
  4. 零值为nil

###6.1.1 函数类型字面量

组成:由关键字func、由圆括号包裹参数声明列表、空格以及可以由圆括号包裹的结果声明列表组成
语法:func(input1 string ,input2 string) string

  1. 参数声明列表和结果声明列表中的单个参数声明之间是由英文逗号分隔的
  2. 每个参数声明和结果声明由名称、空格和类型组成
  3. 参数声明列表和结果声明列表中的参数名称是可以被统一省略的
  4. 结果声明列表在只有一个无名称的结果声明时还可以省略括号
1
2
//函数类型声明
type MyFunc func(input1 string ,input2 string) string

###6.1.2 函数(函数值)

语法:先写关键字func和函数名称,后跟参数声明列表和结果声明列表,最后是由花括号包裹的语句列表
技巧:如果结果声明是带名称的,那么它就相当于一个已被声明但未被显式赋值的变量。我们可以为它赋值且在return语句中省略掉需要返回的结果值。
函数实例:函数myFunc是函数类型MyFunc的一个实现。实际上,只要一个函数的参数声明列表和结果声明列表中的数据类型的顺序和名称与某一个函数类型完全一致,前者就是后者的一个实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1创建函数实例
func myFunc(part1 string, part2 string) (result string) {
result = part1 + part2
return
}
//2.声明对应上面实例的函数类型
type MyFunc func(input1 string ,input2 string) string

//3.创建响应类型的引用
var splice MyFunc// 等价于var splice func(string, string) string

//4.将函数作为值复制给相应类型的变量
splice = myFunc

//5.调用表达式
splice("1", "2")

###6.1.3 匿名函数

说明:匿名函数就是不带名称的函数值。
特点:

  1. 匿名函数直接由函数类型字面量和由花括号包裹的语句列表组成。
  2. 这里的函数类型字面量中的参数名称是不能被忽略的。
1
2
3
var splice = func(part1 string, part2 string) string {
return part1 + part2
}

###6.1.4 立即执行的匿名函数

说明:既然我们可以在代表函数的变量上实施调用表达式,那么在匿名函数上肯定也是可行的。因为它们的本质是相同的。

1
2
3
var result = func(part1 string, part2 string) string {
return part1 + part2
}("1", "2")

###6.1.5 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"fmt"
"strconv"
"sync/atomic"
)

// 员工ID生成器
type EmployeeIdGenerator func(company string, department string, sn uint32) string

// 默认公司名称
var company = "Gophers"

// 序列号
var sn uint32

// 生成员工ID
func generateId(generator EmployeeIdGenerator, department string) (string, bool) {
// 这是一条 if 语句,我们会在下一章讲解它。
// 若员工ID生成器不可用,则无法生成员工ID,应直接返回。
if generator == nil {
return "", false
}
// 使用代码包 sync/atomic 中提供的原子操作函数可以保证并发安全。
newSn := atomic.AddUint32(&sn, 1)
return generator(company, department, newSn), true
}

// 字符串类型和数值类型不可直接拼接,所以提供这样一个函数作为辅助。
func appendSn(firstPart string, sn uint32) string {
return firstPart + strconv.FormatUint(uint64(sn), 10)
}

func main() {
var generator EmployeeIdGenerator
generator = func(company string, department string, sn uint32) string{return appendSn(company+"-"+department+"-", sn)}//Gophers-RD-1 true
fmt.Println(generateId(generator, "RD"))
}

##6.2 结构体和方法

用途:可以封装属性和操作。前者即是结构体类型中的字段,而后者则是结构体类型所拥有的方法。
结构体类型字面量:

  1. 结构体类型的字面量由关键字type、类型名称、关键字struct,以及由花括号包裹的若干字段声明组成
  2. 每个字段声明独占一行并由字段名称(可选)和字段类型组成
  3. 结构体类型属于值类型。它的零值并不是nil,而是其中字段的值均为相应类型的零值的值。
1
2
3
4
5
type Person struct {
Name string
Gender string
Age uint8
}

结构体字面量:

  1. 由其类型的名称和由花括号包裹的若干键值对组成
  2. 这里的键是其类型中的某个字段的名称(注意,它不是字符串字面量),而对应的值则是欲赋给该字段的那个值
  3. 如果这里的键值对的顺序与其类型中的字段声明完全相同的话,可以统一省略掉所有字段的名称(只对它的部分字段赋值,甚至不对它的任何字段赋值的情况除外)
  4. 未被显式赋值的字段的值则为其类型的零值
1
2
3
Person{Name: "Robert", Gender: "Male", Age: 33}   
//或
Person{"Robert", "Male", 33}

###6.2.1 匿名结构体

定义:与代表函数值的字面量类似,我们在编写一个结构体值的字面量时不需要先拟好其类型。这样的结构体字面量被称为匿名结构体。
用途:在内部临时创建一个结构以封装数据,而不必正式为其声明相关规则
语法:在编写匿名结构体的时候需要先写明其类型特征(包含若干字段声明),再写出它的值初始化部分
注意:匿名结构体是不可能拥有方法的

1
2
3
4
5
p := struct {
Name string
Gender string
Age uint8
}{"Robert", "Male", 33}

###6.2.2 方法

定义:就是一种特殊的函数。它可以依附于某个自定义类型。
语法:方法的特殊在于它的声明包含了一个接收者声明。这里的接收者指代它所依附的那个类型。
接收者声明:

  • 组成:其中的内容由两部分组成。第一部分是代表它依附的那个类型的值的标识符。第二部分是它依附的那个类型的名称
  • 语法:后者表明了依附关系,而前者则使得在该方法中的代码可以使用到该类型的值(也称为当前值,是个指针类型)
  • 接受者:代表当前值的那个标识符可被称为接收者标识符,或简称为接收者。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1. 创建结构体类型
type Person struct {
Name string
Gender string
Age uint8
}

//2.为结构体类型添加方法
//在关键字func和名称Grow之间的那个圆括号及其包含的内容就是接收者声明
//person为接收者
func (person *Person) Grow() {
person.Age++
}

//3.创建结构体实例
p := Person{"Robert", "Male", 33}

//4.调用结构体方法
p.Grow() //p的Age字段的值变为34

###6.2.3 结构体和面向对象

继承:结构体类型(以及任何类型)之间都不可能存在继承关系
模仿继承:通过在结构体类型的声明中添加匿名字段(或称嵌入类型)

###6.2.4 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type Person struct {
Name string
Gender string
Age uint8
Address string
}
func (person *Person) Move(address string) (result string){
result = person.Address
person.Address = address
return
}
func main() {
p := Person{"Robert", "Male", 33, "Beijing"}
oldAddress := p.Move("San Francisco")
fmt.Printf("%s moved from %s to %s.\n", p.Name, oldAddress, p.Address)
}

##6.3 接口

定义:一个接口类型总是代表着某一种类型(即所有实现它的类型)的行为
语法:一个接口类型的声明通常会包含关键字type、类型名称、关键字interface以及由花括号包裹的若干方法声明

###6.3.1 接口类型中的方法

声明:只包括方法名称、参数声明列表和结果声明列表(参数的名称和结果的名称都可以被省略)
实现一个接口中的方法:具有与该方法相同的声明并且添加了实现部分(由花括号包裹的若干条语句
注意:相同的方法声明意味着完全一致的名称、参数类型列表和结果类型列表。其中,参数类型列表即为参数声明列表中除去参数名称的部分。一致的参数类型列表意味着其长度以及顺序的完全相同。对于结果类型列表也是如此。

###6.3.2 实现接口

无侵入式:无需在一个数据类型中声明它实现了哪个接口(只要满足了“方法集合为其超集”的条件)

###6.3.3 类型转换

说明:Go语言的类型转换规则定义了是否能够以及怎样可以把一个类型的值转换另一个类型的值
空接口:所谓空接口类型即是不包含任何方法声明的接口类型,用interface{}表示,常简称为空接口
类型转换表达式:在类型字面量后跟由圆括号包裹的值(或能够代表它的变量、常量或表达式),意为将后者转换为前者类型的值
注意:Go语言中的包含预定义的任何数据类型都可以被看做是空接口的实现

1
2
3
4
5
p := Person{"Robert", "Male", 33, "Beijing"}

//把表达式&p的求值结果转换成了一个空接口类型的值,并由变量v代表
//表达式&p(&是取址操作符)的求值结果是一个*Person类型的值,即p的指针
v := interface{}(&p)

###6.3.4 类型断言

用途:判断一个类型是否是某个接口的实现
返回值:类型断言表达式的求值结果可以有两个。第一个结果是被转换后的那个目标类型的值,而第二个结果则是转换操作成功与否的标志

1
2
//ok代表了一个bool类型的值
h, ok := v.(Animal)

###6.3.5 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"
//1.定义接口
type Animal interface {
Grow()
Move(string) string
}

//2.实现接口
type Cat struct{
name string
age uint16
where string
}
func (cat *Cat) Grow(){
cat.age ++
}
func (cat *Cat) Move(newPlace string) (oldPlace string){
oldPlace = cat.where
cat.where = newPlace
return
}

func main() {
myCat := Cat{"Little C", 2, "In the house"}
animal, ok := interface{}(&myCat).(Animal)
fmt.Printf("%v, %v\n", ok, animal)
}

##6.4 指针

###6.4.1 *&

作为地址操作符

地址操作符 作用对象 返回值
* 指针 取值(取出指针指向的那个值)
& 取址(取出指向改值的指针指)

###6.4.2 *[基底类型]

场景:出现在一个类型之前,当出现在一个类型之前(如Person和[3]string)时就不能被看做是操作符了,而应该被视为一个符号
用途:如此组合而成的标识符所表达的含义是作为第二部分的那个类型的指针类型。
基底类型:可以把其中的第二部分所代表的类型称为基底类型

###6.4.3 指针方法和值方法

指针方法:只要一个方法的接收者类型是其所属类型的指针类型而不是该类型本身,那么我就可以称该方法为一个指针方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Person struct {
Name string
Gender string
Age uint8
Address string
}

//Grow方法和Move方法都是Person类型的指针方法
func (person *Person) Grow() {
person.Age++
}

func (person *Person) Move(newAddress string) string {
old := person.Address
person.Address = newAddress
return old
}

值方法:如果一个方法的接收者类型就是其所属的类型本身,那么我们就可以把它叫做值方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1.为结构体添加值方法
//Person类型的Grow方法的接收者标识符person代表的是p的值的一个拷贝,而不是p的值
func (person Person) Grow() {
person.Age++
}

//2.创建该结构体实例
p := Person{"Robert", "Male", 33, "Beijing"}

//3.调用值方法
//在调用Grow方法的时候,Go语言会将p的值复制一份并将其作为此次调用的当前值。
//因此,Grow方法中的person.Age++语句的执行会使这个副本的Age字段的值变为34,而p的Age字段的值却依然是33
p.Grow()
fmt.Printf("%v\n", p)

###6.4.4 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

type MyInt struct {
n int
}

func (myInt *MyInt) Increase() {
myInt.n++
}

func (myInt *MyInt) Decrease() {
myInt.n--
}

func main() {
mi := MyInt{}
mi.Increase()
mi.Increase()
mi.Decrease()
mi.Decrease()
mi.Increase()
fmt.Printf("%v\n", mi.n == 1)
}

##6.5 指针(续)

###隐藏规则

###6.5.1 指针类型和其基底类型所拥有的方法

原理: 一个指针类型拥有以它以及以它的基底类型为接收者类型的所有方法,而它的基底类型却只拥有以它本身为接收者类型的方法
导致:拥有指针方法Grow和Move的指针类型*Person是接口类型Animal的实现类型,但是它的基底类型Person却不是

###6.5.2 基础类型可以调用指针方法吗?

可以:如果Go语言发现我们调用的Grow方法是bp的指针方法,那么它会把该调用表达式视为(&bp).Grow()

###6.5.3 案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"
//1.创建接口
type Pet interface {
Name() string
Age() uint8
}

//2.实现接口
type Dog struct{
name string
age uint8
}

//3.实现方法
func (dog Dog) Name() string{
return dog.name
}
func (dog Dog) Age() uint8{
return dog.age
}

func main() {
myDog := Dog{"Little D", 3}
_, ok1 := interface{}(&myDog).(Pet)
_, ok2 := interface{}(myDog).(Pet)
fmt.Printf("%v, %v\n", ok1, ok2)
}

#7 基本流程控制

##7.1 if语句

###7.1.1 简单if语句

1
2
3
4
5
6
7
8
var number = 0
if 100 > number {
number += 3
} else if 100 < number {
number -= 2
} else {
fmt.Println("OK!")
}

###7.1.2 包含变量初始化过程的if语句

短变量声明语句:在声明变量number的同时为它赋值。
if语句的初始化语句:

语法:应被放置在if关键字和条件表达式之间,并与前者由空格分隔、与后者由英文分号;分隔
作用域:仅在这条if语句所代表的代码块中

标识符的重声明:只要对同一个标识符的两次声明各自所在的代码块之间存在包含的关系,就会形成对该标识符的重声明。导致的结果就是标识符的遮蔽。

1
2
3
4
5
6
7
if number := 4; 100 > number {
number += 3
} else if 100 < number {
number -= 2
} else {
fmt.Println("OK!")
}

###7.1.3 案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
var number int = 5
if number += 4; 10 > number {
number := 0
number += 3
fmt.Print(number)
} else if 10 < number {
number -= 2
fmt.Print(number)
}
fmt.Println(number)
}

##7.2 switch语句

switch表达式:switch语句中要被判定的那个表达式
注意:case表达式结果类型需要与switch表达式的结果类型一致
分类:每一个case可以携带一个表达式或一个类型说明符,据此分两类

分类 case携带的代码
表达式switch语句 表达式
类型switch语句 类型说明符

##7.3 for语句

##7.4 select语句