uncategorized

LeetCode动态规划题解

Chapter1:Golang开篇

终于等到你!Go语言——让你用写Python代码的开发效率编写C语言代码

Golang方向

  1. 区块链研发工程师

  2. Go服务器端/游戏软件工程师

  3. Golang分布式/云计算软件工程师(消息系统)

Golang应用领域

  1. 区块链应用(分布式账本技术,去中心化,每个人均可参与数据库记录)

  2. 后端服务器应用(美团后台流量支撑程序-排序推荐搜索)

  3. 云计算/云服务后台应用(盛大云CDN-内容分发网络,京东消息推送云服务,分布式文件系统)

Go语言特点

简介:Go语言保证了既能达到静态编译语言的安全和性能,由达到动态语言开发维护的高效率,Go=C +Python。

1、从C语言中继承了很多理念,包括表达式语法,控制结构,基础数据类型,调用参数传值,指针等,也保留和C语言一样的编译执行方式及弱化的指针。举例:

1
2
3
4
//go语言指针的使用特点
func testPtr(num *int) {
*num = 20
}

2、引入包的概念,用于组织程序结构,Go语言的一个文件都要归属于一个包,而不能单独存在

3、垃圾回收机制,内存自动回收,不需开发人员管理

4、天然并发(重要特点)

​ 从语言层面支持并发,实现简单

goroutine,轻量级线程,可实现大并发处理,高效利用多核

​ 基于CPS并发模型实现

5、吸收了管道通信机制,形成Go语言特有的管道channel,通过管道channel可以实现不同的goroute之间的相互通信

6、函数可以返回多个值。举例:

1
2
3
4
5
6
7
//写一个函数,实现同时返回和,差
//go函数支持返回多个值
func getSumAndSub(n1 int, n2 int) (int, int) {
sum := n1 + n2 //go语句后面不要带分号
sub := n1 - n2
return sum, sub
}

7、新的创新:比如切片、延时执行defer(回收资源)等

-w839

goroutine特点

Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go 语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用CPU性能。开启一个goroutine的消耗非常小(大约2KB的内存),你可以轻松创建数百万个goroutine

  1. goroutine具有可增长的分段堆栈。这意味着它们只在需要时才会使用更多内存。
  2. goroutine的启动时间比线程快。
  3. goroutine原生支持利用channel安全地进行通信。
  4. goroutine共享数据结构时无需使用互斥锁。

Chapter2:搭建Go开发环境

下载安装

下载地址

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

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

Windows平台和Mac平台推荐下载可执行文件版,Linux平台下载压缩文件版。

Linux下安装

我们在版本选择页面选择并下载好go1.11.5.linux-amd64.tar.gz文件:

1
wget https://dl.google.com/go/go1.11.5.linux-amd64.tar.gz

将下载好的文件解压到/usr/local目录下:

1
2
mkdir -p /usr/local/go  # 创建目录
tar -C /usr/local/go zxvf go1.11.5.linux-amd64.tar.gz. # 解压

如果提示没有权限,加上sudo以root用户的身份再运行。执行完就可以在/usr/local/下看到go目录了。

配置环境变量: Linux下有两个文件可以配置环境变量,其中/etc/profile是对所有用户生效的;$HOME/.profile是对当前用户生效的,根据自己的情况自行选择一个文件打开,添加如下两行代码,保存退出。

1
2
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin

修改/etc/profile后要重启生效,修改$HOME/.profile后使用source命令加载$HOME/.profile文件即可生效。 检查:

1
2
~ go version
go version go1.11.5 linux/amd64

Go配置环境变量

GOROOTGOPATH都是环境变量,其中GOROOT是我们安装go开发包的路径,而从Go 1.8版本开始,Go开发包在安装完成后会为GOPATH设置一个默认目录,参见下表。

平台 GOPATH默认值 举例
Windows %USERPROFILE%/go C:\Users\用户名\go
Unix $HOME/go /home/用户名/go
  • GOROOT:SDK的安装路径
  • Path:添加SDK的/bin目录
  • GOPATH:工作目录,go项目工作路径

注意:Go语言1.14版本之后推荐使用go modules管理以来,也不再需要把代码写在GOPATH目录下了

GOPROXY

Go1.14版本之后,都推荐使用go mod模式来管理依赖环境了,也不再强制我们把代码必须写在GOPATH下面的src目录了,你可以在你电脑的任意位置编写go代码。

默认GoPROXY配置是:GOPROXY=https://proxy.golang.org,direct,由于国内访问不到https://proxy.golang.org,所以我们需要换一个PROXY,这里推荐使用https://goproxy.iohttps://goproxy.cn

可以执行下面的命令修改GOPROXY:

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

Go语言快速入门

开发目录结构

goproject/src/go_code/project01/main or package

开发步骤

  1. 编写Go代码
  2. 通过go build命令对该go文件进行编译,生成可执行文件
  3. 在终端执行可执行文件
  4. 注意:通过go run命令可以直接运行go程序,生产环境都是先编译再执行

Go执行流程分析

  • .go源文件 –>go build –> 可执行文件 –> 结果
  • .go源文件 –> go run –> 结果

两种执行流程方式的区别:

  1. 如果先编译生成可执行文件,那么可以将该可执行文件拷贝到没有go开发环境的机器上,仍然可以运行
  2. 如果是直接go run .go源代码,那么如果在另一台机器上运行,需要go开发环境
  3. 在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件变大了很多

go install

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

编译和运行说明

编译

有了go源文件,通过编译器将其编译成机器可以识别的二进制码文件

在该源文件目录下,通过go buildhello.go文件进行编译。可以指定生成的可执行文件名,在windows下必须是.exe后缀

1
2
// -o指定编译后的可执行文件名
go build -o myhello.exe hello.go

如果程序没有错误,编译没有任何提示,反之编译时会在错误的那行报错

运行

直接运行编译完的可执行文件

go run 运行源代码

跨平台编译

默认我们go build的可执行文件都是当前操作系统可执行的文件,如果我想在windows下编译一个linux下可执行文件,那需要怎么做呢?

只需要指定目标操作系统的平台和处理器架构即可:

1
2
3
SET CGO_ENABLED=0  // 禁用CGO
SET GOOS=linux // 目标平台是linux
SET GOARCH=amd64 // 目标处理器架构是amd64

使用了cgo的代码是不支持跨平台编译的

然后再执行go build命令,得到的就是能够在Linux平台运行的可执行文件了。

Mac 下编译 Linux 和 Windows平台 64位 可执行程序:

1
2
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build

Linux 下编译 Mac 和 Windows 平台64位可执行程序:

1
2
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build

Windows下编译Mac平台64位可执行程序:

1
2
3
4
SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build

Go语言开发注意事项

  1. Go源文件以go为扩展名
  2. Go应用程序的执行入口是main()函数
  3. Go语言严格区分大小写
  4. Go方法由一条条语句构成,每个语句后不需要分号(Go语言会在每行后自动加分号),这也体现出Golang的简洁性
  5. Go编译器是一行一行进行编译的,因为我们一行就写一条语句,不能把多条语句写在同一行,否则报错
  6. Go语言定义的变量或者import的包没有使用到,代码不能编译通过
  7. 大括号是成对出现的,缺一不可

Go语言转义字符

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

注释与格式化

Go官方更推荐行注释

1
2
行注释://...
块注释:/*...*/

正确的缩进和空白

  1. 使用tabshift tab
  2. 使用gofmt来进行格式化
  3. 运算符两边各加一个空格
1
2
3
4
// 格式化输出,但不保存
gofmt main.go
// 格式化并保存
gofmt -w main.go

行长约定:一行最长不超过80个字符,超过请用换行展示,逗号连接

Golang官方指南

Golang官网:golang.org

Golang中文网:studygolang.com/pkgdoc

Dos常用命令

目录操作

查看当前目录:dir

切换目录:cd

新建目录:md

删除空目录:rd

强制删除所有:rd /q/s test q-安静模式,s-递进删除

文件操作

新建或追加内容文件:echo hello > d:test100\abc100\abc.txt

复制或移动文件:copy old.txt d:test100\new.txt

move old.txt d:test100\

删除文件:del *.txt

其他

清屏:cls

退出dos:exit

Chapter3:Golang变量和常量

标识符与关键字

标识符

在编程语言中标识符就是程序员定义的具有特殊意义的词,比如变量名、常量名、函数名等等。 Go语言中标识符由字母数字和_(下划线)组成,并且只能以字母和_开头。 举几个例子:abc, _, _123, a123

关键字

关键字是指编程语言中预先定义好的具有特殊含义的标识符。 关键字和保留字都不建议用作变量名。

Go语言中有25个关键字:

1
2
3
4
5
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

此外,Go语言中还有37个保留字。

1
2
3
4
5
6
7
8
9
10
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

变量介绍

概念:变量相当于内存中一个数据存储空间的表示,可以将变量看做一个房间的门牌号

变量来历:程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。

变量使用:声明变量 -> 赋值 -> 使用

Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。

1
2
3
4
5
6
7
package main  // 声明 main 包,表明当前是一个可执行程序
import "fmt" // 导入内置 fmt 包
func main() { // main函数,是程序执行的入口
var i int
i = 10
fmt.Println("i = ",i)
}

变量使用注意事项

变量=变量名+值+数据类型,变量三要素

变量表示内存中的一个存储区域,该区域有自己的名称(变量名)和类型(数据类型)

Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用

变量声明

标准声明

Go语言的变量声明格式为:

1
var 变量名 变量类型

变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。 举个例子:

1
2
3
var name string
var age int
var isOk bool

批量声明

每声明一个变量就需要写var关键字会比较繁琐,go语言中还支持批量变量声明:

1
2
3
4
5
6
var (
a string
b int
c bool
d float32
)

变量初始化

Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil

当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:

1
var 变量名 类型 = 表达式

举个栗子:

1
2
var name string = "Q1mi"
var age int = 18

或者一次初始化多个变量

1
var name, age = "Q1mi", 20

类型推导

有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。

1
2
var name = "Q1mi"
var age = 18

短变量声明

在函数内部,可以使用更简略的 := 方式声明并初始化变量。

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

import (
"fmt"
)
// 全局变量m
var m = 100

func main() {
n := 10
m := 200 // 此处声明局部变量m
fmt.Println(m, n)
}

多变量声明

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"
// 定义全局变量
var g1 = 100
var g2 = 200
var g3 = 300
// 上面的声明方式,可以改写成一次性声明
var (
g4 = 400
g5 = 500
g6 = 600
)
func main() {
// 一次性声明多个变量
var n1, n2, n3 int
fmt.Println("n1=", n1, "n2=", n2, "n3=", n3)
// 一次性声明多个类型
var n, name, num = 100, "Tom", 888
fmt.Println("n=", n, "neme is", name, "num=", num)
// 类型推导
n4, name1, num1 := 100, "Tom", 888
fmt.Println("n4=", n4, "neme1 is", name1, "num1=", num1)
// 输出全局变量
fmt.Println(g1, g2, g3, g4, g5, g6)
}

匿名变量

在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_表示,例如:

1
2
3
4
5
6
7
8
9
func foo() (int, string) {
return 10, "Q1mi"
}
func main() {
x, _ := foo()
_, y := foo()
fmt.Println("x=", x)
fmt.Println("y=", y)
}

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua等编程语言里,匿名变量也被叫做哑元变量。)

注意事项

  1. 函数外的每个语句都必须以关键字开始(var、const、func等)
  2. :=不能使用在函数外。
  3. _多用于占位,表示忽略值。

补充

作用域的数据值可以在同一类型范围内不断变化

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
func main() {
var i int = 10
i = 20
i = 30
fmt.Println(i)
i = 1.2 //constant 1.2 truncated to integer,原因是不能改变数据类型
fmt.Println(i)
}

变量在一个作用域内(一个函数或者代码块内)不能重命名

1
2
3
4
5
6
7
package main
import "fmt"
func main() {
var i int = 10
fmt.Println(i)
var i := 40
}

判断变量类型

1
2
3
4
5
6
7
8
9
10
11
12
13
// 方式一
package main
import (
"fmt"
)
func main() {

v1 := "123456"
v2 := 12

fmt.Printf("v1 type:%T\n", v1)
fmt.Printf("v2 type:%T\n", v2)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 方式二
package main
import (
"fmt"
"reflect"
)
func main() {
v1 := "123456"
v2 := 12

// reflect
fmt.Println("v1 type:", reflect.TypeOf(v1))
fmt.Println("v2 type:", reflect.TypeOf(v2))
}

常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。

1
2
const pi = 3.1415
const e = 2.7182

声明了pie这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。

多个常量也可以一起声明:

1
2
3
4
const (
pi = 3.1415
e = 2.7182
)

const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。 例如:

1
2
3
4
5
const (
n1 = 100
n2
n3
)

上面示例中,常量n1n2n3的值都是100。

iota

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

iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。

举个栗子:

1
2
3
4
5
6
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)

常见的iota示例

使用_跳过某些值

1
2
3
4
5
6
const (
n1 = iota //0
n2 //1
_
n4 //3
)

iota声明中间插队

1
2
3
4
5
6
7
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。)

1
2
3
4
5
6
7
8
const (
_ = iota
KB = 1 << (10 * iota)
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)

多个iota定义在一行

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

变量的数据类型

基本数据类型

整型

一个字节是八位,八位占存储空间一字节(1byte = 8bitbit计算机中最小存储单位,byte计算机中基本存储单元)

有符号第一位表示正负号,剩下七位表示数,那么int8范围相当于-2^7 ~ 2^7-1,无符号第一位也表示数,那么uint8范围相当于0 ~ 2^8-1

int8,int16,int32,int64(有符号),uint8,uint16,uint32,uint64(无符号)

rune等同于int32-2^31 ~ 2^31-1),byte等同于uint80 ~ 2^8-1),int16对应C语言中的short型,int64对应C语言中的long

1
2
3
4
5
6
7
8
9
10
package main
import (
"fmt"
"unsafe"
)
func main() {
var i int = 10
fmt.Printf("i变量类型:%T,i占用字节大小:%d", i, unsafe.Sizeof(i))
}
// i变量类型:int,i占用字节大小:8

​ Golang中整形变量在使用时,遵守保小不保大原则,避免存储空间浪费

1
2
// 例如年龄,byte范围在(0,255)
var age byte = 90
类型 描述
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)

特殊整型

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

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

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

浮点型

Go语言支持两种浮点型数:float32float64。这两种浮点型数据格式遵循IEEE 754标准: float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64

  • float32,单精度,占用四字节
  • float64,双精度,占用八字节

浮点数存储=符号位+指数位+尾数位,浮点数的使用可能会丢失尾数造成精度损失

打印浮点数时,可以使用fmt包配合动词%f,代码如下:

1
2
3
4
5
6
7
8
9
package main
import (
"fmt"
"math"
)
func main() {
fmt.Printf("%f\n", math.Pi)
fmt.Printf("%.2f\n", math.Pi)
}

复数

complex64complex128

1
2
3
4
5
6
var c1 complex64
c1 = 1 + 2i
var c2 complex128
c2 = 2 + 3i
fmt.Println(c1)
fmt.Println(c2)

布尔型

Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)false(假)两个值。

注意:

  1. 布尔类型变量的默认值为false
  2. Go 语言中不允许将整型强制转换为布尔型
  3. 布尔型无法参与数值运算,也无法与其他类型进行转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"fmt"
"unsafe"
)
// 演示golang中布尔类型
func main() {
var a = false
fmt.Println("a=", a) // a= false
// 注意事项:
// 1.bool类型占用存储空间是一个字节
fmt.Println("a 占用空间:", unsafe.Sizeof(a)) // a 占用空间: 1
// 2.bool类型只能取true和false
a = 1 // 错误
}

字符型(没有专门的字符型,使用byte来保存单个字母字符)

​ Golang中没有专门的字符类型,如果要存储单个字符(字母),一般使用byte来保存

​ 字符串就是一串固定长度的字符连接起来的字符序列,Go的字符串是由单个字节连接起来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"
// 演示golang中字符类型使用
func main() {
var c1 byte = 'a'
var c2 byte = '0'
// 1.当我们直接输出byte值,就是输出了对应的字符码值
fmt.Println("c1 = ", c1, "\nc2 = ", c2)
// c1 = 97 c2 = 48
// 2.如果我们希望输出对应字符,需要格式化输出
fmt.Printf("c1 = %c,c2 = %c", c1, c2)
// c1 = a,c2 = 0
//3.溢出情况
//var c3 byte = '北'
//fmt.Printf("c3 = %c", c3)
// constant 21271 overflows byte
//4.换个类型
var c3 int = '北'
fmt.Printf("c3 = %c,c3对应码值=%d", c3, c3)
//c3 = 北,c3对应码值=21271
}

​ 对上面代码说明:

  1. ​ 如果我们保存的字符在ASCII表的,比如[0-1,a-z,A-Z…]可以直接保存到byte
  2. ​ 如果我们保存的字符对应码值均大于255,这时我们可以考虑使用int类型保存
  3. ​ 如果我们需要按照字符的方式输出,这时我们需要格式化输出,即fmt.Printf(“%c”, c)

字符类型使用细节

  1. 字符常量使用单引号(‘’)括起来的单个字符。例如:var c1 byte = ‘a’,var c2 int = ‘中’,var c3 byte = ‘1’

  2. Go中允许使用转义字符’\‘来将其后的字符转变为特殊字符型常量。例如:var c4 char = ‘\n’

  3. Go语言的字符使用UTF-8编码,英文字母占1个字节,汉字占3个字节

  4. 在Go中,字符的本质是一个整数,直接输出时,是该字符对应的UTF-8编码的码值

  5. 可以直接给某个变量赋一个数字,然后按格式化输出%c,会输出该数字对应的Unicode字符

    1
    2
    3
    var c4 int = 22269
    fmt.Printf("c4 = %c\n",c4)
    // c4 = 国
  6. 字符类型是可以进行运算的,相当于一个整数,因为他都有Unicode码

1
2
var c5 = 10 + 'a'	// 10+97=107
fmt.Printf("c5 = %c\n",c5)

字符类型本质探讨

  1. 字符存储到计算机中,需要将字符对应的码值(整数)找出来
  2. 字符和码值的对应关系是通过字符编码表决定的
  3. Go语言的编码都统一成了utf-8,很统一,没有编码乱码困扰

字符串

基本介绍:字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用utf-8编码表示Unicode文本,统一用双引号包裹,多行字符串用反引号包裹,字符用单引号包裹

注意事项和使用细节

  1. Go语言的字符串的字节使用utf-8编码,golang统一使用utf-8,不会出现乱码
  2. 字符串一旦赋值了,就不能修改了。在Go中字符串是不可变的
  3. 字符串的两种表示形式,普通双引号括起来,反引号按原样输出
  4. 字符串拼接方式
  5. 多行字符串使用
  6. Go语言中字符串一定是用双引号包裹的,单引号包裹的是字符
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
package main
import (
"fmt"
)
// 演示golang中string类型
func main() {
var address = "北京东城区永定门"
fmt.Println("address=", address)
// 字符串一旦赋值,字符串就不可修改
//address[0] = '南'
// 字符串两种表示形式:1、双引号,会识别转义字符。2、反引号,以原生形式输出,包括换行特殊字符
str := `"abc\nbc"`
fmt.Println("str=", str) // str= "abc\nbc"
// 多行字符串
s1 := `第一行
第二行
第三行
`
fmt.Println(s1)
// 字符串拼接方式
var str1 = "hello" + "world"
str1 += "haha"
fmt.Println(str1)
// 当一个拼接操作很长时间,可以分行写,但是注意,需要将+保留在上一行,因为Go会在行尾加分号
var str2 = "my " + "name " + "is " +
"Kevin," + "yeah"
fmt.Println(str2)
}

字符串的常用操作

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

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

1
2
3
4
5
6
7
8
9
10
11
12
func changeString() {
s1 := "big"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))

s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2))
}

byte和rune类型

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

1
2
var a := '中'
var b := 'x'

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

  1. uint8类型,或者叫 byte 型,代表了ASCII码的一个字符
  2. rune类型,代表一个 UTF-8字符

当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32

Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。

1
2
3
4
5
6
7
8
9
10
11
12
// 遍历字符串
func traversalString() {
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()
}

输出:

1
2
104(h) 101(e) 108(l) 108(l) 111(o) 230(æ) 178(²) 153() 230(æ) 178(²) 179(³) 
104(h) 101(e) 108(l) 108(l) 111(o) 27801(沙) 27827(河)

因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果。

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

基本数据类型的默认值

go中数据类型都有默认值,没有赋值是保留默认值

数据类型 默认值
整形 0
浮点型 0
字符串 “”
布尔型 false

cookies:Printf中的%v按照变量原始值输出

基本数据类型的转换

Golang和Java/C不同,Go在不同类型的 变脸之间赋值时需要显示转换,也就是Golang中数据类型不能自动转换

基本语法

表达式T(v)是将值v转换为类型T

T:数据类型,如int32,float64等

v:就是需要转换的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
)
// 演示golang中基本类型转换(同类型间不同精度也需要显示转换)
func main() {
var n int32 = 100
// 希望将n转换为float
var n1 float64 = float64(n)
// 高精度->低精度
var n2 int8 = int8(n)
// 低精度->高精度
var n3 int64 = int64(n)
fmt.Printf("n=%v n1=%v n2=%v n3=%v\n", n, n1, n2, n3)
fmt.Printf("n type is %T", n)
}

细节说明

  1. Go中,数据类型的转换可以是从表示范围小到表示范围大,也可以从范围大到范围小
  2. 被转换的是变量存储的数据,变量本身数据类型并没有变化
  3. 在转换中,比如将int64转成int8,编译时不会报错,只是转换的结果是按溢出处理,和我们希望的结果不一样
1
2
3
4
5
6
7
8
9
package main
import (
"fmt"
)
// 在转换中,比如将int64转成int8[-128-127],编译时不会报错
// 只是转换的结果按溢出处理,和希望的结果不一样
var num1 int64 = 9999999
var num2 int8 = int8(num1)
fmt.Println("num2=",num2) // 63

举个栗子:计算直角三角形的斜边长时使用math包的Sqrt()函数,该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制类型转换为float64类型

1
2
3
4
5
6
7
func sqrtDemo() {
var a, b = 3, 4
var c int
// math.Sqrt()接收的参数是float64类型,需要强制转换
c = int(math.Sqrt(float64(a*a + b*b)))
fmt.Println(c)
}

cookies:如果我们没有使用到一个包,但又不想删去,前面加一个 _,表示忽略

1
2
3
import (
_ "fmt"
)

基本数据类型和string的转换

基本数据类型转string

方式1:fmt.Sprintf(“%参数”,表达式)

方式2:strconv包函数

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
40
41
42
43
44
45
package main
import (
"fmt"
"strconv"
)
// 演示golang中基本类型与string的转换
func main() {
var num1 int = 99
var num2 float64 = 23.456
var b bool = true
var mychar byte = 'h'
var str string // 空str
// 使用第一种方式转换,fmt.Sprintf
str = fmt.Sprintf("%d", num1)
fmt.Printf("str type %T,str=%q\n", str, str)

str = fmt.Sprintf("%f", num2)
fmt.Printf("str type %T,str=%q\n", str, str)

str = fmt.Sprintf("%t", b)
fmt.Printf("str type %T,str=%q\n", str, str)

str = fmt.Sprintf("%c", mychar)
fmt.Printf("str type %T,str=%q\n", str, str)
fmt.Println("=======================================")
// 使用第二种方式转换,strconv
var num3 int = 99
var num4 float64 = 23.456
var b2 bool = true
str = strconv.FormatInt(int64(num3), 10)
fmt.Printf("str type %T,str=%q\n", str, str)

// f表示格式,10表示小数位保留10位,64表示小数是float64
str = strconv.FormatFloat(num4, 'f', 10, 64)
fmt.Printf("str type %T,str=%q\n", str, str)

str = strconv.FormatBool(b2)
fmt.Printf("str type %T,str=%q\n", str, str)

// strconv包中有一个函数Itoa
var num5 int64 = 4567
var str string
str = strconv.Itoa(int(num5))
fmt.Printf("str type %T str=%q\n", str, str)
}

string类型转基本数据类型

使用strconv包函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
import (
"fmt"
"strconv"
)
// 演示golang中基本类型与string的转换
func main() {
var str string = "true"
var str2 string = "12345"
var str3 string = "123.45"

var b bool
// ParseBool返回一个error,忽略掉
b, _ = strconv.ParseBool(str)
fmt.Printf("b type %T b=%v\n", b, b)

var num6 int64
num6, _ = strconv.ParseInt(str2, 10, 64)
fmt.Printf("num6 type %T num6=%v\n", num6, num6)

var num7 float64
num7, _ = strconv.ParseFloat(str3, 64)
fmt.Printf("num7 type %T num7=%v\n", num7, num7)
}

注意事项:将string转换成基本类型时,要确保string类型能够转成有效的数据,比如我们可以把”123”转换成整数,但是不能把”hello”转换成整数,如果这样做,Golang直接将其转换成0,转换成bool的就变成false。

派生/复杂类型

指针(Pointer)

基本介绍
  1. 基本数据类型,变量存的就是值,也叫值类型
  2. 获取变量的地址,用&,比如:var n int,获取n的地址:&n
  3. 指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值,例如:var ptr *int = &n
  4. 获取指针类型所指向的值,使用*,比如:var ptr *int,使用 *ptr获取ptr指向的值

image-20200302221059387

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"fmt"
)
// 演示golang中指针类型
func main() {
// 基本数据类型在内存布局
var i int = 10
// i的地址:&i
fmt.Println("i 地址:", &i)
// ptr是指针变量,ptr类型是 *int,ptr本身的值是&i
var ptr *int = &i
fmt.Printf("ptr=%v\n", ptr)
fmt.Println("ptr 地址:", &ptr)
// ptr存储的值是i地址,指向的值是i,
fmt.Printf("ptr 指向的值=%v", *ptr)
// 修改i的值
*ptr = 2 // i的值会改变
fmt.Println("i=", i)
}
错误示范
1
2
3
4
5
6
7
8
func main() {
var a int = 300
var ptr1 *int = a // 错误,指针变量存储的是地址,改成&a
var ptr2 *float32 = &a // 错误,类型不匹配,ptr2接收的地址也应该是int类型
var b *int // 空指针
*b = 100 // 给空指针赋值报错
fmt.Println(*a)
}

指针细节说明

  1. 所有的值类型,都有对应的指针类型,形式为*数据类型
  2. 值类型包括:基本数据类型intfloatboolstring,数组和结构体struct
new

new()函数申请一个内存地址,返回的是类型指针,语法如下:

1
func new(Type) *Type
  • Type表示类型,new函数只接受一个参数,这个参数是一个类型
  • *Type表示类型指针,new函数返回一个指向该类型内存地址的指针

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值

1
2
3
4
5
6
7
8
func main() {
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
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
func main() {
var b *int // 空指针,地址是nil
var a = new(int) // new()函数申请一个内存地址
fmt.Println(a)
fmt.Println(*a)
*a = 100
fmt.Println(*a)
}
// 0xc00000a0b8
// 0
// 100
make

make也是用于内存分配的,区别于new,他只用于slice,map,channel的内存创建,而且他返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型本身就是引用类型,所以就没必要返回他们的指针。make函数的语法如下:

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

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作

1
2
3
4
// 构造切片
var a = make([]int,5,10)
// 构造map
b := make(map[string]int, 10)

new与make的区别

  1. 二者都是用来做内存分配的
  2. make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身
  3. 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针

值类型和引用类型

值类型:基本数据类型intfloatboolstring,数组和结构体struct

引用类型:指针,slice切片,map,管道channelinterface等都是引用类型

值类型和引用类型使用特点

值类型:变量直接存储值,内存通常在栈中分配

引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址的数据空间就成为一个垃圾,由GC来回收

image-20200304203943183

标识符的命名规范

标识符概念

Go对各种变量、方法、函数等命名时使用的字符序列称为标识符,凡是可以起名字的地方都叫标识符 var num int

标识符命名规则

  1. 由英文字母、数字、下划线组成
  2. 数字不可以开头
  3. golang中严格区分大小写
  4. 标识符不能包含空格
  5. 下划线在Go中是一个特殊的标识符,称为空标识符。可以代表任何其他的标识符,但是他对应的值会被忽略(比如,忽略某个返回值)。所以仅能作为占位符使用,不能作为表示符使用
  6. 不能以系统保留关键字作为标识符,比如breakif

标识符命名注意事项

  1. 包名:保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,不要和标准库冲突
  2. 变量名、函数名、常量名采用驼峰法
  3. 如果变量名、函数名、常量名首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用(注:可以简单地理解成,首字母大写是共有的,首字母小写是私有的),golang中没有publicprivate等关键字

image-20200304212419648

数组(Array)

数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。 基本语法:

1
2
// 定义一个长度为3元素类型为int的数组a
var a [3]int
数组定义
1
var 数组变量名 [元素数量]T

比如:var a [5]int, 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。 [5]int[10]int是不同的类型。

1
2
3
var a [3]int
var b [4]int
a = b //不可以这样做,因为此时a和b是不同的类型

数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会panic。

数组的初始化

数组的初始化也有很多方式

方法一

初始化数组时可以使用初始化列表来设置数组元素的值

1
2
3
4
5
6
7
8
func main() {
var testArray [3]int //数组会初始化为int类型的零值
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2 0]
fmt.Println(cityArray) //[北京 上海 深圳]
}

方法二

按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,例如:

1
2
3
4
5
6
7
8
9
10
func main() {
var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2]
fmt.Printf("type of numArray:%T\n", numArray) //type of numArray:[2]int
fmt.Println(cityArray) //[北京 上海 深圳]
fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string
}

方法三

我们还可以使用指定索引值的方式来初始化数组,例如:

1
2
3
4
5
func main() {
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
}
数组的遍历

遍历数组有以下两种方法:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
var a = [...]string{"北京", "上海", "深圳"}
// 方法1:for循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}

// 方法2:for range遍历
for index, value := range a {
fmt.Println(index, value)
}
}
多维数组

Go语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。

二维数组的定义

1
2
3
4
5
6
7
8
9
func main() {
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"}
}
fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]]
fmt.Println(a[2][1]) //支持索引取值:重庆
}

二维数组的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}
}
// 北京 上海
// 广州 深圳
// 成都 重庆

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

1
2
3
4
5
6
7
8
9
10
11
12
//支持的写法
a := [...][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
//不支持多维数组的内层使用...
b := [3][...]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
数组是值类型

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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) //在modify中修改的是a的副本x
fmt.Println(a) //[10 20 30]
b := [3][2]int{
{1, 1},
{1, 1},
{1, 1},
}
modifyArray2(b) //在modify中修改的是b的副本x
fmt.Println(b) //[[1 1] [1 1] [1 1]]
}

注意:

  1. 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的
  2. [n]*T表示指针数组,*[n]T表示数组指针

练习:

找出数组中和为指定值的两个元素的下标,比如从数组[1, 3, 5, 7, 8]中找出和为8的两个元素的下标分别为(0,3)(1,2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
)
//找出数组中和为指定值的两个元素的下标,比如从数组[1, 3, 5, 7, 8]中找出和为8的两个元素的下标分别为(0,3)和(1,2)。
func main() {
var numArray = [...]int{1, 3, 5, 7, 8}
for index, value := range numArray {
for i := index + 1; i < len(numArray); i++ {
if value+numArray[i] == 8 {
fmt.Printf("(%v,%v)\n", index, i)
break
}
}
}
}

结构体(struct)

切片(slice)

因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容

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

切片的定义

声明切片类型的基本语法如下:

1
var name []T

其中,

  • name:表示变量名
  • T:表示切片中的元素类型

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
// 声明切片类型
var a []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
fmt.Println(a) //[]
fmt.Println(b) //[]
fmt.Println(c) //[false true]
// Go语言的空nil相当于没有开辟内存空间
fmt.Println(a == nil) //true
fmt.Println(b == nil) //false
fmt.Println(c == nil) //false
// fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和nil比较
}
切片的长度和容量

切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量

注意:

  • 切片指向了一个底层的数组
  • 切片的长度就是它元素的个数
  • 切片的容量是底层数组从切片的第一个元素到最后一个元素的数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
var s1 []int
var s2 = []int{1, 2, 4, 7}
fmt.Printf("s1 type:%T\n", s1)
fmt.Printf("s2 type:%T\n", s2)
fmt.Println(s1 == nil)
fmt.Println(s2 == nil)
fmt.Printf("len(s1):%v,cap(s1):%v\n", len(s1), cap(s1))
fmt.Printf("len(s2):%v,cap(s2):%v\n", len(s2), cap(s2))
}
切片表达式

切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。

切片的底层就是一个数组,所以可以基于数组通过切片表达式得到切片。 切片表达式中的lowhigh表示一个索引范围(左闭右开),也就是下面代码中从数组a中选出1<=索引值<4的元素组成切片s,得到的切片长度=high-low,容量等于底层数组从切片的第一个元素到最后一个元素的数量。

1
2
3
4
5
6
7
8
package main
import "fmt"
func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // s := a[low:high]
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
}
//s:[2 3] len(s):2 cap(s):4

为了方便起见,可以省略切片表达式中的任何索引。省略了low则默认为0;省略了high则默认为切片操作数的长度(和python相同):

1
2
3
a[2:]  // 等同于 a[2:len(a)]
a[:3] // 等同于 a[0:3]
a[:] // 等同于 a[0:len(a)]

对于数组或字符串,如果0 <= low <= high <= len(a),则索引合法,否则就会索引越界(out of range)。

切片再切片

对切片再执行切片表达式时,索引的上限是切片的容量cap(s),而不是长度,即0 <= low <= high <= cap(s)常量索引必须是非负的,并且可以用int类型的值表示;对于数组或常量字符串,常量索引也必须在有效范围内。如果lowhigh两个指标都是常数,它们必须满足low <= high。如果索引在运行时超出范围,就会发生运行时panic

对切片再切片时,操作的还是底层数组,二次切片的索引0代表的值是一次切片的初始元素。若一次切片是array[1:],那么二次切片的数据则从array[1]开始,索引从0开始。举个栗子:a := [5]int{1,2,3,4,5}s:=a[1:3],s:[2,3]s1:=s[:4],s1:[2,3,4,5]

1
2
3
4
5
6
7
8
9
func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // [2 3]
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
s2 := s[3:4] // 索引的上限是cap(s)而不是len(s)
fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
}
// s:[2 3] len(s):2 cap(s):4
// s2:[5] len(s2):1 cap(s2):1

完整切片表达式

对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:

1
a[low : high : max]

上面的代码会构造与简单切片表达式a[low: high]相同类型、相同长度和元素的切片。另外,它会将得到的结果切片的容量设置为max-low。在完整切片表达式中只有第一个索引值(low)可以省略;它默认为0。

1
2
3
4
5
6
func main() {
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))
}
// t:[2 3] len(t):2 cap(t):4

完整切片表达式需要满足的条件是0 <= low <= high <= max <= cap(a),其他条件和简单切片表达式相同。

修改底层数组的值

切片是引用类型,切片没有值,指向了底层一个数组,修改底层数组的值,会影响切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:] // [2 3]
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
s2 := s[:4] // 索引的上限是cap(s)而不是len(s)
fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
// 切片是引用类型,都指向了底层一个数组
a[4] = 500
fmt.Println("s:",s)
fmt.Println("s2:",s2)
}
// s:[2 3 4 5] len(s):4 cap(s):4
// s2:[2 3 4 5] len(s2):4 cap(s2):4
// s: [2 3 4 500]
// s2: [2 3 4 500]
make()函数构造切片

上面都是基于数组来创建的切片,如果需要动态的创建一个切片,就需要使用内置的make()函数,格式如下:

1
make([]T, size, cap)
  • T:切片的元素类型
  • size:切片中元素的数量
  • cap:切片的容量,不写cap默认cap等于size

举个栗子:

1
2
3
4
5
6
func main() {
a := make([]int, 2, 10)
fmt.Println(a) //[0 0]
fmt.Println(len(a)) //2
fmt.Println(cap(a)) //10
}

上面代码中a的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量。

切片的本质

切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。(切片就是一个框,框住了一块连续的内存)

举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下。

slice_01

切片s2 := a[3:6],相应示意图如下:

slice_02

判断切片是否为空

要检查切片是否为空,请始终使用len(s) == 0来判断,而不应该使用s == nil来判断

切片不能直接比较

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

1
2
3
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来判断。

切片的赋值拷贝

下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

1
2
3
4
5
6
7
8
func main() {
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100
// s1和s2都指向了一个底层数组
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
}
切片遍历

切片的遍历方式和数组是一致的,支持索引遍历和for range遍历

1
2
3
4
5
6
7
8
9
10
11
func main() {
s := []int{1, 3, 5}

for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}

for index, value := range s {
fmt.Println(index, value)
}
}
append()方法为切片添加元素

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

append追加元素,原来的底层数组放不下的时候,Go底层就会把底层数组换一个,还是要用原来的变量名去接收(比如公司扩容,公司名称不变)

1
2
3
4
5
6
7
func main(){
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
}

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

1
2
var s []int
s = append(s, 1, 2, 3)

没有必要像下面的代码一样初始化一个切片再传入append()函数使用

1
2
3
4
5
s := []int{}  // 没有必要初始化
s = append(s, 1, 2, 3)

var s = make([]int) // 没有必要初始化
s = append(s, 1, 2, 3)

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

举个栗子:

1
2
3
4
5
6
7
8
func main() {
//append()添加元素和切片扩容
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
}
}

输出:

1
2
3
4
5
6
7
8
9
10
[0]  len:1  cap:1  ptr:0xc0000a8000
[0 1] len:2 cap:2 ptr:0xc0000a8040
[0 1 2] len:3 cap:4 ptr:0xc0000b2020
[0 1 2 3] len:4 cap:4 ptr:0xc0000b2020
[0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000

从上面的结果可以看出:

  1. append()函数将元素追加到切片的最后并返回该切片
  2. 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍

append()函数还支持一次性追加多个元素。 例如:

1
2
3
4
5
6
7
8
9
var citySlice []string
// 追加一个元素
citySlice = append(citySlice, "北京")
// 追加多个元素
citySlice = append(citySlice, "上海", "广州", "深圳")
// 追加切片
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...) // ...表示拆开
fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆]
切片的扩容策略

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}

从上面的代码可以看出以下内容:

注:目前不确定

  • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
  • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
  • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

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

copy()函数复制切片

首先我们来看一个问题:

1
2
3
4
5
6
7
8
9
func main() {
a := []int{1, 2, 3, 4, 5}
b := a
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(b) //[1 2 3 4 5]
b[0] = 1000
fmt.Println(a) //[1000 2 3 4 5]
fmt.Println(b) //[1000 2 3 4 5]
}

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

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:

1
copy(destSlice, srcSlice []T)
  • destSlice: 目标切片
  • srcSlice: 数据来源切片

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
// copy()复制切片
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]
}
切片删除元素

Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:

1
2
3
4
5
6
7
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}

总结一下就是:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...),删除之后容量不发生变化

注意:删除元素也修改了底层数组,数组长度固定,所以需要前移并补充

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
func main() {
a := [...]int{1, 2, 3, 4, 5}
s := a[:]
fmt.Println("s=", s)
// 修改了底层数组,因为切片框住的是连续的内存,所以后面要往前移,数组的长度是固定的,用最后一个元素补充
s1 := append(s[:1], s[2:]...)
fmt.Println("s1=", s1)
fmt.Println("a=", a)
}

练习

以下代码会输出什么

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"

func main() {
var a = make([]string, 5, 10)
fmt.Println(a)
fmt.Println(a == nil)
for i := 0; i < 15; i++ {
a = append(a, fmt.Sprintf("%v", i))
}
fmt.Println(a, len(a), cap(a))

var b = make([]string, 5, 10)
fmt.Println(b)
for j := 0; j < 16; j++ {
b = append(b, fmt.Sprintf("%v", j))
}
fmt.Println(b, len(b), cap(b))
}
// [ ] 里面是5个空字符串
// false
// [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14] 20 20
// [ ] 里面是5个空字符串
// [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] 21 40

管道(Channel)

函数

接口(interface)

map

Go语言中提供的映射关系容器为map,其内部使用散列表(hash)实现。

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

map定义

Go语言中 map的定义语法如下:

1
map[KeyType]ValueType
  • KeyType:表示键的类型。
  • ValueType:表示键对应的值的类型。

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:

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

其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

map基本使用

map中的数据都是成对出现的,map的基本使用示例代码如下:

1
2
3
4
5
6
7
8
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}

输出:

1
2
3
map[小明:100 张三:90]
100
type of a:map[string]int

map也支持在声明的时候填充元素,例如:

1
2
3
4
5
6
7
func main() {
userInfo := map[string]string{
"username": "小王子",
"password": "123456",
}
fmt.Println(userInfo) //
}
判断某个键是否存在

Go语言中有个判断map中键是否存在的特殊写法,格式如下:

1
value, ok := map[key]

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
v, ok := scoreMap["娜扎"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
}
map的遍历

Go语言中使用for range遍历map。

1
2
3
4
5
6
7
8
9
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}

但我们只想遍历key的时候,可以按下面的写法:

1
2
3
4
5
6
7
8
9
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k := range scoreMap {
fmt.Println(k)
}
}

只想遍历value的时候,采用匿名变量,忽略key即可

注意: 遍历map时的元素顺序与添加键值对的顺序无关。

使用delete()函数删除键值对

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

1
delete(map, key)
  • map:表示要删除键值对的map
  • key:表示要删除的键值对的键

示例代码如下:

1
2
3
4
5
6
7
8
9
10
func main(){
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
delete(scoreMap, "小明")//将小明:100从map中删除
for k,v := range scoreMap{
fmt.Println(k, v)
}
}
按照指定顺序遍历map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
rand.Seed(time.Now().UnixNano()) //初始化随机数种子

var scoreMap = make(map[string]int, 200)

for i := 0; i < 100; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
value := rand.Intn(100) //生成0~99的随机整数
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])
}
}
元素为map类型的切片

下面的代码演示了切片中的元素为map类型时的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "小王子"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "永定门"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
// index:0 value:map[]
// index:1 value:map[]
// index:2 value:map[]
// after init
// index:0 value:map[address:永定门 name:小王子 password:123456]
// index:1 value:map[]
// index:2 value:map[]
值为切片类型的map

下面的代码演示了map中值为切片类型的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}

练习:

写一个程序,统计一个字符串中每个单词出现的次数。比如:”how do you do”中how=1 do=2 you=1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
"strings"
)
//写一个程序,统计一个字符串中每个单词出现的次数。比如:”how do you do”中how=1 do=2 you=1
func main() {
str := "how do you do"
list := strings.Split(str, " ") // 拆成slice
fmt.Printf("list=%v, list type:%T\n", list, list)
var dict = make(map[string]int, len(list)) // 初始化map
// 遍历切片,判断key在map是否存在,不存在添加,存在次数+1
for _, value := range list {
if _, ok := dict[value]; !ok {
dict[value] = 1
} else {
dict[value]++
}
}
fmt.Println(dict)
}

观察下面代码,写出最终的打印结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"fmt"
)
func main() {
type Map map[string][]int
m := make(Map)
s := []int{1, 2}
s = append(s, 3)
fmt.Printf("%+v\n", s)
m["q1mi"] = s
s = append(s[:1], s[2:]...)
fmt.Printf("%+v\n", s)
fmt.Printf("%+v\n", m["q1mi"])
}
1
2
3
// [1 2 3]
// [1 3]
// [1 3 3]

Chapter4:Golang运算符

运算符用于表示数据的运算、赋值和比较等

算术运算符

运算符 描述
+ 相加
- 相减
* 相乘
/ 相除
% 求余

注意: ++(自增)和--(自减)在Go语言中是单独的语句,并不是运算符。

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
package main

import (
"fmt"
)

func main() {
// 讲解 /、%
// 说明:如果运算的数是整数,那么除后,去掉小数部分,保留整数部分
fmt.Println(10 / 4) // 2
var n1 float32 = 10 / 4 // 2
fmt.Println(n1)
// 如果我们洗完保留小数部分,则需要浮点数参与运算
var n2 float32 = 10.0 / 4 // 2.5
var n3 float32 = 10 / 4.0 // 2.5
fmt.Println(n2, n3)
// 演示 % 的使用
// 看一个公式:a % b = a - a / b * b
fmt.Println("10%3=", 10%3) // 1
fmt.Println("-10%3=", -10%3) // -1
fmt.Println("10%-3=", 10%-3) // 1
fmt.Println("-10%-3=", -10%-3) // -1

// 演示++和--的使用
var i int = 10
i++
fmt.Println(i) // 11
i--
fmt.Println(i) // 10
}

注意细节

  1. 对于除号,他的整数除和小数除是有区别的,整数之间做除法,只保留整数部分而舍弃小数部分。例如:x := 19/5,结果是3
  2. 当对一个数取模时,可以等价a % b = a - a / b * b,这是取模的本质运算
  3. Golang的自增自减只能当做一个独立语言使用,不能这样使用。例如:a = i++if i++ >0
  4. Golang的++--只能写在变量的后面,不能写在前面,即没有++a和–a
  5. Golang的设计者去掉c/java中的自增自减容易混淆的写法,更加简洁统一

赋值运算符

运算符 描述
= 简单的赋值运算符,将一个表达式的值赋给一个左值
+= 相加后再赋值
-= 相减后再赋值
*= 相乘后再赋值
/= 相除后再赋值
%= 求余后再赋值
<<= 左移后赋值
>>= 右移后赋值
&= 按位与后赋值
|= 按位或后赋值
^= 按位异或后赋值

比较运算符/关系运算符

注意细节

关系运算符的结果都是bool型,要么是true,要么是false

关系表达式经常用在if结构或者循环结构中

比较运算符==不能写成=

运算符 描述
== 检查两个值是否相等,如果相等返回 True 否则返回 False。
!= 检查两个值是否不相等,如果不相等返回 True 否则返回 False。
> 检查左边值是否大于右边值,如果是返回 True 否则返回 False。
>= 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。
< 检查左边值是否小于右边值,如果是返回 True 否则返回 False。
<= 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。

逻辑运算符

结果也是一个bool值,&&与,||或,!

注意细节

逻辑与&&也叫短路与,如果第一个为false,第二个不会判断,最终结果为false

逻辑或||也叫短路或,如果第一个为true,则第二个不会判断,结果为true

运算符 描述
&& 逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。
|| 逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。
! 逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。

位运算符

运算符 描述
& 参与运算的两数各对应的二进位相与。 (两位均为1才为1)
| 参与运算的两数各对应的二进位相或。 (两位有一个为1就为1)
^ 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 (两位不一样则为1)
<< 左移n位就是乘以2的n次方。 “a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。
>> 右移n位就是除以2的n次方。 “a>>b”是把a的各二进位全部右移b位。

其他运算符

image-20200308170248691

Go语言明确不支持三目运算符,只能用if..else..表达

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"fmt"
)
func main() {
var n int
var i int = 10
var j int = 12
if i > j {
n = i
} else {
n = j
}
fmt.Println("n=", n)
}

python三元运算符

1
2
3
4
5
6
7
8
为真时的结果 if 判断条件 else 为假时的结果(注意,没有冒号)
# 斐波那契
def fn(n):
return n if n < 2 else fn(n-1)+fn(n-2)
# numpy
import numpy as np
np.where(判断条件,为真时的处理,为假时的处理)
x = np.where(x%2==1, x+1, x)

运算符优先级

image-20200308170530837

顺序:

  1. 括号,++--
  2. 单目运算
  3. 算数运算符
  4. 移位运算
  5. 关系运算符
  6. 位运算符
  7. 逻辑运算符
  8. 赋值运算符
  9. 逗号

键盘输入语句

接收用户输入的数据,就可以使用键盘输入语句来获取

函数:fmt.Scanln()或者fmt.Scanf()

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
package main
import (
"fmt"
)
func main() {
// 要求从控制台接收用户信息【姓名,年龄,薪水,是否通过考试】
// 方式一
var name string
var age byte
var sal float32
var isPass bool
fmt.Println("请输入姓名:")
// 阻塞等待输入
fmt.Scanln(&name)
fmt.Println("请输入年龄:")
fmt.Scanln(&age)
fmt.Println("请输入薪水:")
fmt.Scanln(&sal)
fmt.Println("请输入是否通过考试:")
fmt.Scanln(&isPass)
fmt.Printf("名字是:%v\n年龄是:%v\n薪水是:%v\n是否通过考试:%v\n", name, age, sal, isPass)
// 方式二
fmt.Println("请输入姓名,年龄,薪水,是否通过考试")
fmt.Scanf("%s %d %f %t", &name, &age, &sal, &isPass)
fmt.Printf("名字是:%v\n年龄是:%v\n薪水是:%v\n是否通过考试:%v\n", name, age, sal, isPass)
}

进制

n进制逢n进1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"fmt"
)
func main() {
// 二进制输出
var i int = 5
fmt.Printf("%b\n", i)
// 八进制:0-7,满8进1,以数字0开头表示
var j int = 011
fmt.Println("j=", j)
// 0-9及A-F,满16进1,以0x或者0X开头表示
var k int = 0x11
fmt.Println("k=", k)
}

二进制转十进制

从最低位(右边),将每个位上的数提取出来,乘以2的(位数-1)次方,然后求和

十进制转二进制

将该数不断除以2,直到商为0,然后将每步得到的余数倒过来,就是对应的二进制

八进制转十进制

从最低位(右边),将每个位上的数提取出来,乘以8的(位数-1)次方,然后求和

十进制转八进制

将该数不断除以8,直到商为0,然后将每步得到的余数倒过来,就是对应的八进制

十六进制转十进制

从最低位(右边),将每个位上的数提取出来,乘以16的(位数-1)次方,然后求和

十六进制转十进制

将该数不断除以16,直到商为0,然后将每步得到的余数倒过来,就是对应的十六进制

原码、反码、补码(帮助位运算)

对于有符号的而言:

  1. 二进制最高位是符号位:0表示正数,1表示负数

    1==》[0000 0001] -1==》[1000 0001]

  2. 正数的原码,反码和补码都一样

  3. 负数的反码=它的原码符号位不变,其他位取反(0->1,1->0)

    1==》原码[0000 0001] 反码[0000 0001] 补码[0000 0001]

    -1==》原码[1000 0001] 反码[1111 1110] 补码[1111 1111]

  4. 负数的补码=它的反码+1

  5. 0的反码,补码都是0

  6. 在计算机运算的时候,都是以补码的方式来运算的

    1-1 = 1+(-1)

Chapter5:Golang流程控制

控制决定程序执行

  • 顺序控制

    从上到下逐步执行

  • 分支控制

  • 循环控制

if-else分支控制

所有分支控制只有一个入口,满足条件执行,其他就不会执行

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

  1. 单分支

    1
    2
    3
    if 表达式1 {
    分支1
    }

    说明

    当条件表达式为true时,就会执行{}的代码,{}是必须有的,就算只有一行代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package main
    import (
    "fmt"
    )
    func main() {
    var age byte
    fmt.Println("请输入年龄:")
    // 阻塞等待输入
    fmt.Scanln(&age)
    if age > 18 {
    fmt.Println("已经成年")
    }
    }
  2. 多分支

    1
    2
    3
    4
    5
    6
    7
    if 表达式1 {
    分支1
    } else if 表达式2 {
    分支2
    } else{
    分支3
    }
  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
    // 参加百米运动会,如果用时八秒以内进入决赛,否则提示淘汰,根据性别提示进入男子组或女子组
    // 输入成绩和性别
    package main
    import (
    "fmt"
    )
    func main() {
    var second float64
    // 阻塞等待输入
    fmt.Println("请输入秒数:")
    fmt.Scanln(&second)
    if second <= 8 {
    var gender string
    fmt.Println("请输入性别:")
    fmt.Scanln(&gender)
    if gender == "男" {
    fmt.Println("进入男子组")
    } else {
    fmt.Println("进入女子组")
    }
    } else {
    fmt.Println("被淘汰")
    }
    }

switch分支控制

基本介绍

  1. switch语句用于基于不同条件执行不同动作,每一个case分支都是唯一的,从上到下逐一测试,知道匹配为止。
  2. 匹配项后面也不需要再加break
  3. Go语言规定每个switch只能有一个default分支。

基本语法

1
2
3
4
5
6
7
8
switch 表达式{
case 表达式1,表达式2,...:
语句块1
case 表达式3,表达式4,...:
语句块2
default:
语句块
}

image-20200315213852082

  1. 是她参加的执行流程是,先执行表达式,得到值,然后和case的表达式进行比较,如果相等,就匹配到,然后执行对应的case的语句块,并退出switch控制
  2. 如果switch的表达式的值没有和任何case表达式匹配到,则执行default语句块,执行后退出控制
  3. 在Golang中,case后的表达式可以有多个,使用逗号隔开
  4. Golang中case后不需要加break,因为默认会有。即当程序执行完case语句块后,就直接退出该switch控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
)
func main() {
var key byte
fmt.Println("请输入一个字符:abcde")
fmt.Scanf("%c", &key)
switch key {
case 'a':
fmt.Println("周一")
case 'b':
fmt.Println("周二")
case 'c':
fmt.Println("周三")
default:
fmt.Println("周末")
}
}

switch细节讨论

  1. case/switch后面是一个表达式(常量,变量,一个有返回值的函数等)

  2. case后的各个表达式数据类型必须和switch的表达式数据类型一致

    image-20200315185123228

  3. case后面可以跟多个表达式,用逗号隔开

  4. case后面的表达式如果是常量值,要求不可以重复

    image-20200315185509088

  5. case后面不需要带break,匹配到一个case后就会执行对应的代码块,然后退出switch,匹配不到则执行default

  6. default语句不是必须的

  7. switch后也可以不带表达式,类似if-else分支来使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package main
    import (
    "fmt"
    )
    func main() {
    var n1 int64 = 10
    switch {
    case n1 == 10:
    fmt.Println("10")
    case n1 != 10:
    fmt.Println("!=10")
    default:
    fmt.Println("未匹配")
    }
    }
  1. switch后也可以直接生命/定义一个变量,分号结束,不推荐

  2. switch穿透-fallthrough:如果在case语句块后增加fallthrough,则会继续执行下一个case,也叫switch穿透,是为了兼容C语言中的case设计的

    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 n1 int64 = 10
    switch n1 {
    case 10:
    fmt.Println("ok10")
    fallthrough // 默认只能穿透一层
    case 20:
    fmt.Println("ok20")
    case 30:
    fmt.Println("ok30")
    default:
    fmt.Println("未匹配")
    }
    }
  3. type switchswitch语句还可以被用于type-switch来判断某个interface变量中实际指向的变量类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package main
    import (
    "fmt"
    )
    func main() {
    var x interface{}
    var y = 10.0
    x = y
    switch i := x.(type) {// x.(type)可以结合switch判断接口实际指向变量类型
    case nil:
    fmt.Printf("x类型:%T", i)
    case int, bool, string:
    fmt.Println("x是int类型")
    case float64:
    fmt.Println("x是float类型")
    case func(int) float64:
    fmt.Println("x是func(int)类型")
    default:
    fmt.Println("未匹配")
    }
    }

switch和if的比较

  • 如果判断的具体数值不多,而且符合整数、浮点数、字符、字符串这几种类型。建议用switch语句,简洁有效
  • 其他情况,对区间判断和结果为bool类型的判断,使用ifif的使用范围更广

for循环控制

1
2
3
4
5
6
7
8
9
package main
import (
"fmt"
)
func main() {
for i :=1;i<=10;i++ {
fmt.Println("cool")
}
}

基本语法

1
2
3
for 循环变量初始化;循环条件;循环变量迭代{
循环操作(语句)
}

说明

  • 对于for循环来说,有四个要素
  • 循环变量初始化
  • 循环条件
  • 循环操作,也叫循环体
  • 循环变量迭代

执行顺序分析

  1. 执行循环变量初始化,比如i := 1
  2. 执行循环条件,比如 i <= 10
  3. 如果循环条件为真,就执行循环操作,比如打印
  4. 执行循环变量迭代,比如i++
  5. 反复执行2/3/4步骤,直到循环条件为false,退出for循环

image-20200315212658907

注意事项

  1. 循环条件是返回一个布尔值的表达式

  2. for循环的第二种使用方式,初始语句和结束语句都可以省略,这种写法类似于其他语言中的while,在while后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环

    循环变量初始化

    for 循环判断条件 {

    ​ 循环执行语句

    ​ 循环变量迭代

    }

for循环的初始语句可以被忽略,但是初始语句后的分号必须要写,举个栗子:

1
2
3
4
5
6
func forDemo2() {
i := 0
for ; i < 10; i++ {
fmt.Println(i)
}
}
1
2
3
4
5
6
7
func forDemo3() {
i := 0
for i < 10 {
fmt.Println(i)
i++
}
}
1
2
3
4
5
6
7
8
9
10
11
package main
import (
"fmt"
)
func main() {
j := 1
for j<=10 {
fmt.Println("cool")
j++
}
}
  1. 死循环

    for {

    ​ 循环执行语句

    }

    等价于for ; ; {}是一个无限循环,通常需要配合break语句使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package main
    import "fmt"
    func main() {
    k := 1
    for { // 等价于for ; ; {
    if k <= 10 {
    fmt.Println(k)
    } else {
    break
    }
    k++
    }
    }
  2. golang提供for-range的方式,可以方便遍历数组、切片、字符串、map 及通道(channel)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package main
    import "fmt"
    func main() {
    var str string = "hello,world"
    // 第一种遍历字符串方式
    for i := 1; i < len(str); i++ {
    fmt.Printf("%c\n", str[i])
    }
    // 第二种遍历字符串方式
    for index, val := range str {
    fmt.Printf("index=%d,val=%c\n", index, val)
    }
    }

    上面代码细节讨论

    如果我们的字符串含有中文,那么第一种遍历字符串方式就是错误,会出现乱码,原第一种方法是按照字节来遍历,而Golang采用utf-8编码,一个汉字对应三个字节

    如何解决:需要将str转成[]rune切片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package main
    import "fmt"
    func main() {
    var str string = "hello,world,北京"
    // 第一种遍历字符串方式
    str1 := []rune(str)
    for i := 1; i < len(str1); i++ {
    fmt.Printf("%c\n", str1[i])
    }
    }

    for-range采用字符方式遍历,不是按照字节来遍历的(注意index的变化)

    通过for range遍历的返回值有以下规律:

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

    image-20200315220926768

练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 100以内整除9的数之和
package main
import "fmt"
func main() {
var count int = 0
var sum int = 0
for i := 1; i <= 100; i++ {
if i%9 == 0 {
count++
sum += i
}
}
fmt.Printf("count=%v sum=%v\n", count, sum)
}

while和do…while实现

Go语言没有whiledo...while语法,用for循环实现

while实现

1
2
3
4
5
6
7
8
循环变量初始化
for {
if 循环条件表达式{
break跳出循环
}
循环操作语句
循环变量迭代
}

说明

for循环是一个无线循环

break语句跳出循环

do…while实现

1
2
3
4
5
6
7
8
循环变量初始化
for {
循环操作语句
循环变量迭代
if 循环条件表达式 {
break跳出循环
}
}

说明

do...while是先执行后判断,至少执行一次

当循环条件成立后,就会执行breakbreak就是跳出for循环

多重循环控制

基本介绍

  1. 将一个循环放在另一个循环体内,就形成了嵌套循环。外边的成为外层循环,里面的成为内层循环。(建议不要超过3层)
  2. 实质上,嵌套循环就是把内层循环当成外层循环的循环体。当只有内层循环的循环条件为false时,才会跳出内层循环,才可结束外层的当次循环,开始下一次循环
  3. 设外层循环为m次时,内层为n次,则内层循环实际上需要执行m*n

代码案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
import "fmt"
func main() {
var claNum int = 3
var stuNum int = 5
var passCount int = 0
sumGra := 0.0
for i := 1; i <= claNum; i++ {
var sumCla float64 = 0.0
for j := 1; j <= stuNum; j++ {
var score float64
fmt.Printf("请输入%d班第%d个学生成绩:", i, j)
fmt.Scanln(&score)
sumCla += score
if score >= 60 {
passCount++
}
}
fmt.Printf("%d班学生平均分为%f\n", i, sumCla/float64(stuNum))
sumGra += sumCla
}
fmt.Printf("年纪平均分=%f", sumGra/(float64(claNum*stuNum)))
fmt.Printf("及格人数为%d",passCount)
}

跳转控制语句-break

基本介绍:break用于终止某个语句块的执行,中断当前for循环或跳出switch语句

注意事项:break语句出现在多层嵌套的语句块中时,可以通过标签(label)指明要终止的是哪一层语句块,标签要求必须定义在对应的forswitchselect的代码块上

需求:随机生成0-100的一个数,直到生成了99这个数,看看你一共用了几次

分析:编写一个无限循环的空值,然后不停的随机生成数,当生成了99时,就退出循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
var count int = 0
for {
// 我们为了生成一个随机数,还需要rand设置一个随机种子
// time.Now().Unix():会返回一个从1970/1/1 0分0秒年到现在的秒数
rand.Seed(time.Now().Unix())
// 随机生成数
n := rand.Intn(100)
count++
if n == 99 {
break
}
}
fmt.Printf("一共用了%d次", count)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 标签的使用
package main
func main() {
label1:
for i := 0; i < 4; i++ {
//label2:
for j := 0; j < 10; j++ {
if j == 2 {
//break label2
break label1
}
}
}
}
  1. break默认会跳出最近的for循环
  2. break后面可以指定标签,跳出标签对应的for循环

跳转控制语句-continue

基本介绍:continue用于结束本次循环,继续执行下一次循环

注意事项:continue语句出现在多层嵌套的语句块中时,可以通过标签(label)指明要跳过的是哪一层语句块,和break相同

案例分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
label1:
for i := 0; i < 4; i++ {
//label2:
for j := 0; j < 10; j++ {
if j == 2 {
//break label2
continue label1
}
fmt.Println("j=", j)
}
}
}

跳转控制语句-goto

基本介绍

  1. Go语言的goto语句可以无条件地转移到程序中指定的行
  2. goto语句通常与条件语句配合使用。可用来实现条件转移,跳出循环体等功能
  3. 在Go程序设计中一般不主张使用goto语句,以免造成程序流程的混乱,使理解和调试程序都产生困难

基本语法

goto label

label:statement

goto流程图

image-20200322215728271

案例演示

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
func main() {
fmt.Println("OK1")
goto label1
fmt.Println("OK2")
fmt.Println("OK3")
fmt.Println("OK4")
label1:
fmt.Println("OK5")
fmt.Println("OK6")
}

举个简化代码的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func gotoDemo1() {
var breakFlag bool
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
// 设置退出标签
breakFlag = true
break
}
fmt.Printf("%v-%v\n", i, j)
}
// 外层for循环判断
if breakFlag {
break
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func gotoDemo2() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
// 设置退出标签
goto breakTag
}
fmt.Printf("%v-%v\n", i, j)
}
}
return
// 标签
breakTag:
fmt.Println("结束for循环")
}

跳转控制语句-return

基本介绍

return使用在方法或者函数中,表示跳出所在方法或者函数

函数中return语句底层实现:在底层并不是原子操作,它分为给返回值赋值和RET指令两步

image-20200328215413903

Chapter6:Golang函数

函数

基本介绍

为完成某一功能的程序指令(语句)的集合,称为函数

在Go中,函数分为自定义函数和系统函数

基本语法

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

​ 执行语句

​ return 返回值列表

}

函数可以有返回值,也可以没有返回值;

命名返回值就相当于在函数内部声明了返回值,可以直接return

案例:实现加减乘除计算

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
package main

import (
"fmt"
)

func calcu(n1 float64, n2 float64, operator byte) float64 {
var res float64
switch operator {
case '+':
res = n1 + n2
case '-':
res = n1 - n2
case '*':
res = n1 * n2
case '/':
res = n1 / n2
default:
fmt.Println("操作符号错误")
}
return res
}

func main() {
var n1 float64 = 1.2
var n2 float64 = 2.4
var operator byte = '-'
result := calcu(n1, n2, operator)
fmt.Println("res=", result)
}

变量作用域

  • 全局作用域

  • 函数作用域

    现在函数内部找变量,找不到往外曾照

    在函数内部的变量,外部是访问不到的

  • 语句块作用域(for循环,if,switch分支结构)

包的介绍

包的本质实际上就是创建不同的文件夹,来存放程序文件

go的每一个文件都属于一个包,也就是说go是以包的形式来管理文件和项目目录结构的

image-20200323134846953

包的三大作用

区分相同名字的函数、变量等标识符

当程序文件很多时,可以很好地管理项目

控制n数、变量等访问范围,即作用域

包的相关说明

  • 打包基本语法

    package util

  • 引包基本语法

    import “包的路径”

包的使用及注意事项

image-20200323135311948

1)给一个文件打包时,该包对应一个文件夹,文件包名通常和文件所在文件夹名一致,一般为小写字母

比如utils文件夹对应的包名就是utils,包名也可使用别的名字,但是调用的时候需要使用对应包名

image-20200323140030843

2)当一个文件要使用其他包函数或变量时,需要先引入对应的包

  • 引入方式1:import “包名”

  • 引入方式2:import (

    ​ “包名”

    ​ “包名”

    ​ )

    image-20200323140445792

  • package指令在文件第一行,然后是import指令、

  • 在import包时,路径是从$GOPATH的src下开始,不用带src,编译器会自动从src下开始引入

3)为了让其他包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似Java的public,这样才能跨包访问image-20200323214109451

4)在访问其他包函数时,其语法是包名.函数名image-20200323214327729

5)如果包名较长,Go支持给包取别名,注意取别名后,原包名就不能使用了

image-20200323214658979

6)在同一个包下,不能有相同的函数名(也不能有相当的全局变量名),否则报重复定义

7)如果要编译成一个可执行文件(go build -o),就需要将包声明为main,即package main,main包只能有一个,如果写一个库,包名可以自定义

注意:编译完生成一个库文件,如utils工具包会生成utils.a库文件,这样别人需要用库时就不需要拿到我们的源代码,用不可查看的库文件即可

函数-调用机制

调用过程

image-20200323224857639

机制说明

  • 在调用一个函数时,内存会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其他的栈的空间区分开来
  • 在每个函数对应的栈中,数据空间是独立的,不会混淆
  • 当一个函数执行完毕后,操作系统会销毁回收函数对应的栈空间
  • 当函数有return语句时,就会将结果返回给调用者

return语句

  • Go语言函数支持返回多个值
  • 如果返回多个值时,在接收时,希望忽略,则使用_符号表示站位忽略
  • 如果返回值只有一个,返回值类型列表可以不写括号 ()image-20200323230031546
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
)
func cal(n1 int, n2 int) (int, int) {
sum := n1 + n2
sub := n1 - n2
return sum, sub
}
func main() {
res1, res2 := cal(10, 20)
fmt.Printf("res1=%v\nres2=%v", res1, res2)
}

函数-递归调用

函数在函数体内调用本身,成为递归调用

斐波那契数列

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

import "fmt"

func fei(n int) int {
if n == 1 || n == 2 {
return 1
} else {
return fei(n-1) + fei(n-2)
}
}

func main() {
num := fei(8)
fmt.Println(num)
}
// 1 1 2 3 5 8 13 21

台阶问题

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

import "fmt"

func steps(n uint64) uint64 {
if n == 1 || n == 2 {
return n
}
return steps(n-1) + steps(n-2)
}

func main() {
res := steps(7)
fmt.Println(res)
}

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import "fmt"
func test1(n int) {
if n > 2 {
n--
test(n)
}
fmt.Println("n=", n)
}
func tes2(n int) {
if n > 2 {
n--
test(n)
} else {
fmt.Println("n=", n)
}
}

func main() {
test1(4) // n=2 n=2 n=3
test2(4) // n=2
}

test1函数示意图

image-20200324085323064

递归重要原则

  1. 执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
  2. 函数的局部变量是独立的,不会相互影响
  3. 递归必须向退出递归的条件逼近,否则就是无线递归
  4. 当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁

函数注意事项和细节(重要)

  • 函数的形参列表可以是多个,返回值列表也可以是多个,返回值一个可以不加括号,多个需要加括号

    1
    2
    3
    // 参数列表同种类型可以写成如下形式 
    func myfun(n1,n2 float32) float32 {
    }
  • 形参列表和返回值列表的数据类型可以是值类型和引用类型

  • 函数命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其他包文件使用,类似public,首字母小写只能被本包文件使用,类似private

  • 函数中的变量是局部的,函数外不生效,如果局部变量和全局变量重名,优先访问局部变量

  • 基本数据类型和数组默认都是值传递的,即进行值拷贝。函数内修改,不影响原来的值。如果希望函数内的变量能修改函数外的变量,可以传入变量地址,函数内以指针的方式操作变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package main
    import "fmt"
    func test01(n int) {
    n = n + 10
    fmt.Println("test01() n=", n)
    }
    func test02(n *int) {
    *n = *n + 10
    fmt.Println("test02() n=", *n)
    }
    func main() {
    num := 20
    test01(num)
    fmt.Println("oringin num=", num)
    test02(&num)
    fmt.Println("oringin num=", num)
    }
    //test01() n= 30
    //oringin num= 20
    //test02() n= 30
    //oringin num= 30
  • Go函数不支持重载,会报函数重复定义,python也不支持,python支持多个任意参数,用装饰器添加功能

  • 在Go中,函数也是一种数据类型,可以赋值给一个变量,该变量就是一个函数类型的变量,通过该变量可以对函数调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package main
    import "fmt"
    func getSum(n1 int, n2 int) int {
    return n1 + n2
    }
    func main() {
    a := getSum
    fmt.Printf("a的类型%T,getSum的类型%T\n", a, getSum)
    res := a(10, 20)
    fmt.Println("res=", res)
    }
    //a的类型func(int, int) int,getSum的类型func(int, int) int
    //res= 30
  • 函数既然是一种数据类型,函数也可以作为形参,并且调用

    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 getSum(n1 int, n2 int) int {
    return n1 + n2
    }
    func myFun(funvar func(int, int) int, num1 int, num2 int) int {
    return funvar(num1, num2)
    }
    func main() {
    a := getSum
    fmt.Printf("a的类型%T,getSum的类型%T\n", a, getSum)
    res := a(10, 20)
    fmt.Println("res=", res)
    // 函数作为形参,并且调用
    res2 := myFun(getSum, 50, 60)
    fmt.Println("res2=", res2)
    }
    //a的类型func(int, int) int,getSum的类型func(int, int) int
    //res= 30
    //res2= 110
  • 为了简化数据类型定义,Go支持自定义的数据类型,也支持类型别名

    基本语法:type 自定义数据类型名 数据类型 // 自定义类型

    ​ type 别名 = 数据类型 // 类型别名

    案例:type myint int // 这时候myint等同于int类型,可以在main()中定义

    案例:type myFunc func (int,int) int // 这myFunc等价于一个函数类型func (int,int) int。注意:函数类型不能在main()中定义,会报未声明定义

    别名案例:type myint = int

image-20200324144609270

image-20200324145855857

  • Go支持对函数返回值命名,返回值列表里变量已经创建好,默认为0,返回顺序依据返回值列表顺序。注意:要使用=必须事先已经声明

    image-20200324150748470

  • 使用_标识符,忽略返回值

  • Go支持可变参数,本质上函数的可变参数是通过切片来实现的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 支持0到多个参数
    func sum(args ...int) int {
    sum := xxx // 计算逻辑
    return sum
    }
    // 支持1到多个参数,可变参数一定要放在后面
    func sum(n1 int,args ...int) (sum int) {
    sum = xxx // 计算逻辑
    return
    }
    // 注意:返回值类型列表如果定义命名,那么一定要加括号,即使是一个返回值

    说明

    1、args是slice切片,通过args[index] 可以访问到各个值

    2、如果形参列表里有可变参数,则可变参数需要放在形参列表最后

    计算一个或多个参数的和

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 此处事先定义了返回值命名,直接return即可
    func sum(n1 int, args ...int) (sum int) {
    sum = n1
    // 遍历args
    for i := 0; i < len(args); i++ {
    sum += args[i]
    }
    return
    }
    func main() {
    res := sum(1,2,3,4)
    }

    练习

    编写一个函数swap(n1 *int, n2 *int),可以交换n1和n2的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package main
    import "fmt"

    func swap(n1 *int, n2 *int) {
    *n1, *n2 = *n2, *n1
    }

    func main() {
    a := 10
    b := 20
    swap(&a, &b)
    fmt.Printf("n1=%v,n2=%v", a, b)
    }

init函数

基本介绍

每一个源文件都可以包含一个init函数,该函数会在main函数执行前执行,通常用来完成初始化工作

注意事项和细节

  • 如果一个文件同时包含全局变量定义,init函数和main函数,则执行流程:全局变量定义=》init函数=》main函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package main
    import "fmt"
    // 全局变量定义
    var age = display()
    // 为了看到全局变量是最先执行的,我们这里用函数显示
    func display() int {
    fmt.Println("display()") // 1
    return 19
    }
    func init() {
    fmt.Println("init()") // 2
    }
    func main() {
    fmt.Println("main()") // 3
    }
  • init函数最主要的作用就是完成一些初始化的工作,案例:

    image-20200324230035131

    执行结果:

    1
    2
    3
    4
    5
    6
    //utils init()
    //main display()
    //main init()
    //main()
    //Age= 30
    //Name= Tony

思考:如果main.go和utils.go都含有全局变量定义和init函数是,执行流程什么样呢?

image-20200324231137047

匿名函数

Go支持匿名函数,在某个函数内部或者某个函数只是希望使用一次,可以使用匿名函数,匿名函数也可以实现多次调用

使用方式

  • 在定义匿名函数时就直接调用
  • 将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
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() {
// 方式一:在定义时直接调用,这种匿名函数只能调用一次
res1 := func(n1 int, n2 int) int {
return n1 + n2
}(10, 20) //加()传入参数直接调用
fmt.Println("res1=", res1)

// 方式二:将匿名函数func (n1 int,n2 int) int赋给f变量
// 则f的数据类型就是函数类型,f是函数变量,可以通过f完成调用
f := func(n1 int, n2 int) int {
return n1 - n2
}
res2 := f(10, 20)
fmt.Println("res2=", res2)
}

全局匿名函数

如果将匿名函数赋给一个全局变量,那么这个匿名函数就成为一个全局匿名函数,可以在程序中有效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"fmt"
)
var (
// 大写
Fgloabal = func(n1 int, n2 int) int {
return n1 * n2
}
)
func main() {
// 全局匿名函数的使用
res3 := Fgloabal(5, 20)
fmt.Println("res3=", res3)
}

闭包

闭包就是一个函数与其相关的引用环境组合的一个整体(实体)。简单来说,闭包=函数+引用环境

闭包底层原理:

  • 函数可以作为返回值
  • 函数内部查找变量的顺序,现在自己内部找,找不到往外层找(闭包=函数+包含外部变量的引用

案例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
}

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

闭包进阶示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
// 公用一个base参数
fmt.Println(f1(1), f2(2)) //11 9
fmt.Println(f1(3), f2(4)) //12 8
fmt.Println(f1(5), f2(6)) //13 7
}

闭包其实并不复杂,只要牢记闭包=函数+引用外部变量

defer语句

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

1
2
3
4
5
6
7
8
9
10
11
12
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多用于函数结束之前释放资源(文件句柄,数据库连接,socket连接)

defer的执行时机

image-20200328215854662

defer经典案例

顺序:先给返回值赋值,再defer注册,最后返回返回值

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
40
41
42
package main
import (
"fmt"
)

func f1() int {
x := 5
defer func() {
x++ // 修改的是x不是返回值,因为返回值=5,再执行derfer x=6,再返回返回值5
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5 //返回值x=5,x++,再执行derfer等于6
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x // 返回值=y=x=5,返回值没变,x变了,和f1相同
}
func f4() (x int) {
defer func(x int) {
x++ // x=0当做参数传入进来=1,返回值等于5
}(x)
return 5
}

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

defer面试题

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

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

内置函数

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

panic/recover

Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover模式来处理错误。 panic可以在任何地方引发,但recover只有在defer调用的函数中有效。 首先来看一个例子:

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"

func fun1() {
fmt.Println("f1")
}

func fun2() {
panic("出现了严重错误!!!") // 程序崩溃退出
fmt.Println("f2")
}

func fun3() {
fmt.Println("f3")
}

func main() {
fun1()
fun2()
fun3()
}

//f1
//panic: 出现了严重错误!!!
//
//goroutine 1 [running]:
//main.fun2(...)
//D:/GoProjectsCode/src/awesomeProject/project01/main/main.go:10
//main.main()
//D:/GoProjectsCode/src/awesomeProject/project01/main/main.go:20 +0x9d
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
package main

import "fmt"

func fun1() {
fmt.Println("f1")
}

func fun2() {
// 释放数据库连接
defer func() {
err := recover()
fmt.Println(err)
if err != nil {
fmt.Println("recover in fun2")
}
fmt.Println("释放数据库连接") // 会在panic之前执行
}()
panic("出现了严重错误!!!") // 程序崩溃退出
fmt.Println("f2")
}

func fun3() {
fmt.Println("f3")
}

func main() {
fun1()
fun2()
fun3()
}

//f1
//出现了严重错误!!!
//recover in fun2
//释放数据库连接
//f3

注意:

  1. recover()必须搭配defer使用
  2. defer一定要在可能引发panic的语句之前定义

Chapter7:Golang结构体

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

类型别名和自定义类型

自定义类型

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

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

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

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

类型别名

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

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

1
type TypeAlias = Type

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

1
2
type byte = uint8
type rune = int32

类型定义和类型别名的区别

类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
//类型定义
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
}

结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是intMyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

struct结构体

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了,通过struct来实现面向对象。

结构体的定义

结构体是值类型

使用typestruct关键字来定义结构体,具体代码格式如下:

1
2
3
4
5
type 类型名 struct {
字段名 字段类型
字段名 字段类型

}
  • 类型名:标识自定义结构体的名称,在同一个包内不能重复
  • 字段名:表示结构体字段名。结构体中的字段名必须唯一
  • 字段类型:表示结构体字段的具体类型

举个例子,我们定义一个Person(人)结构体,代码如下:

1
2
3
4
5
type person struct {
name string
city string
age int8
}

同样类型的字段也可以写在一行,

1
2
3
4
type person1 struct {
name, city string
age int8
}

这样我们就拥有了一个person的自定义类型,它有namecityage三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型

结构体实例化

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。

1
var 结构体实例 结构体类型
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
package main

import (
"fmt"
)

// 结构体
type person struct {
name string
age int
gender string
hobby []string
}

func main() {
var p person
p.name = "kevin"
p.age = 18
p.gender = "男"
p.hobby = []string{"篮球", "足球", "双色球"}
fmt.Println(p)
fmt.Printf("p.type:%T\n", p)
fmt.Println(p.name)
}

//{kevin 18 男 [篮球 足球 双色球]}
//p.type:main.person
//kevin

通过.来访问结构体的字段(成员变量),例如p1.namep1.age

匿名结构体

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

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

import (
"fmt"
)

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

创建指针类型结构体

结构体是值类型,需要用指针类型去修改,Go语言传参数永远传的是拷贝副本

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
40
41
package main

import (
"fmt"
)

// 结构体
type person struct {
name string
age int
gender string
hobby []string
}

func alterPerson(per person) {
per.gender = "女"
}

func alterPersonP(per *person) {
(*per).gender = "女"
per.gender = "女" // 语法糖,自动根据指针找对应的变量
}

func main() {
var p person
p.name = "kevin"
p.age = 18
p.gender = "男"
p.hobby = []string{"篮球", "足球", "双色球"}
fmt.Println(p)
// 调用alterPerson
alterPerson(p)
fmt.Println(p.gender)
// 调用指针类型结构体alterPersonP
alterPersonP(&p)
fmt.Println(p.gender)
}

//{kevin 18 男 [篮球 足球 双色球]}
//男
//女

通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:

1
2
3
4
5
6
7
8
var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}
// Go语言中支持对结构体指针直接使用.来访问结构体的成员
p2.name = "小王"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小王", city:"上海", age:28}

取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作

1
2
3
4
5
6
7
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "七米"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"七米", city:"成都", age:30}

p3.name = "七米"其实在底层是(*p3).name = "七米",这是Go语言帮我们实现的语法糖。

结构体初始化

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

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
40
41
42
43
44
45
type person struct {
name string
city string
age int8
}

func main() {
var p4 person
fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}

// 使用键值对初始化
p5 := person{
name: "小王",
city: "北京",
age: 18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"小王", city:"北京", age:18}

// 也可以对结构体指针进行键值对初始化
p6 := &person{
name: "小王",
city: "北京",
age: 18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"小王", city:"北京", age:18}
// 当某些字段没有初始值的时候,该字段可以不写
p7 := &person{
city: "北京",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}


// 使用值的列表初始化
// 初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值
p8 := &person{
"永定门娜扎",
"北京",
28,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"永定门娜扎", city:"北京", age:28}
// 使用这种格式初始化时,需要注意:
// 必须初始化结构体的所有字段。
// 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
// 该方式不能和键值初始化方式混用

结构体内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type test struct {
a int8
b int8
c int8
d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
// n.a 0xc0000a0060
// n.b 0xc0000a0061
// n.c 0xc0000a0062
// n.d 0xc0000a0063

空结构体

空结构体是不占用空间的

1
2
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0

构造函数

Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

1
2
3
4
5
6
7
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}

调用构造函数

1
2
p9 := newPerson("张三", "沙河", 90)
fmt.Printf("%#v\n", p9) //&main.person{name:"张三", city:"沙河", age:90}

方法和接收者

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

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

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Person 结构体
type Person struct {
name string
age int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}

//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
p1 := NewPerson("小王子", 25)
p1.Dream()
}

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

指针类型接收者

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

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

调用该方法:

1
2
3
4
5
6
func main() {
p1 := NewPerson("小王子", 25)
fmt.Println(p1.age) // 25
p1.SetAge(30)
fmt.Println(p1.age) // 30
}

值类型的接收者

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

1
2
3
4
5
6
7
8
9
10
11
12
13
// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
p.age = newAge
}

func main() {
p1 := NewPerson("小王子", 25)
p1.Dream()
fmt.Println(p1.age) // 25
p1.SetAge2(30) // (*p1).SetAge2(30)
fmt.Println(p1.age) // 25
}

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

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

任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
//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
}

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

结构体的匿名字段

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Person 结构体Person类型
type Person struct {
string
int
}

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

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

嵌套结构体

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Address 地址结构体
type Address struct {
Province string
City string
}

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

func main() {
user1 := User{
Name: "小王子",
Gender: "男",
Address: Address{
Province: "山东",
City: "威海",
},
}
fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}

嵌套匿名结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Address 地址结构体
type Address struct {
Province string
City string
}

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

func main() {
var user2 User
user2.Name = "小王子"
user2.Gender = "男"
user2.Address.Province = "山东" //通过匿名结构体.字段名访问
user2.City = "威海" //直接访问匿名结构体的字段名
fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}

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

嵌套结构体的字段名冲突

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

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
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}

//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}

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

func main() {
var user3 User
user3.Name = "沙河娜扎"
user3.Gender = "男"
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
}

结构体的“继承”

Chapter8:Golang常用标准库

fmt

fmt包实现了类似C语言printf和scanf的格式化I/O。主要分为向外输出内容和获取输入内容两大部分。

向外输出

标准库fmt提供了以下几种输出相关函数。

Print

Print系列函数会将内容输出到系统的标准输出,区别在于Print函数直接输出内容,Printf函数支持格式化输出字符串,Println函数会在输出内容的结尾添加一个换行符。

1
2
3
func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

举个简单的例子:

1
2
3
4
5
6
func main() {
fmt.Print("在终端打印该信息。")
name := "沙河小王子"
fmt.Printf("我是:%s\n", name)
fmt.Println("在终端打印单独一行显示")
}

执行上面的代码输出:

1
2
在终端打印该信息。我是:沙河小王子
在终端打印单独一行显示

Fprint

Fprint系列函数会将内容输出到一个io.Writer接口类型的变量w中,我们通常用这个函数往文件中写入内容。

1
2
3
func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)

举个例子:

1
2
3
4
5
6
7
8
9
10
// 向标准输出写入内容
fmt.Fprintln(os.Stdout, "向标准输出写入内容")
fileObj, err := os.OpenFile("./xx.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("打开文件出错,err:", err)
return
}
name := "沙河小王子"
// 向打开的文件句柄中写入内容
fmt.Fprintf(fileObj, "往文件中写如信息:%s", name)

注意,只要满足io.Writer接口的类型都支持写入。

Sprint

Sprint系列函数会把传入的数据生成并返回一个字符串。

1
2
3
func Sprint(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string
func Sprintln(a ...interface{}) string

简单的示例代码如下:

1
2
3
4
5
6
s1 := fmt.Sprint("沙河小王子")
name := "沙河小王子"
age := 18
s2 := fmt.Sprintf("name:%s,age:%d", name, age)
s3 := fmt.Sprintln("沙河小王子")
fmt.Println(s1, s2, s3)

Errorf

Errorf函数根据format参数生成格式化字符串并返回一个包含该字符串的错误。

1
func Errorf(format string, a ...interface{}) error

通常使用这种方式来自定义错误类型,例如:

1
err := fmt.Errorf("这是一个错误")

Go1.13版本为fmt.Errorf函数新加了一个%w占位符用来生成一个可以包裹Error的Wrapping Error。

1
2
e := errors.New("原始错误e")
w := fmt.Errorf("Wrap了一个错误%w", e)

格式化占位符

*printf系列函数都支持format格式化参数,在这里我们按照占位符将被替换的变量类型划分,方便查询和记忆。

通用占位符

占位符 说明
%v 值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名
%#v 值的Go语法表示
%T 打印值的类型
%% 百分号

示例代码如下:

1
2
3
4
5
6
7
fmt.Printf("%v\n", 100)
fmt.Printf("%v\n", false)
o := struct{ name string }{"小王子"}
fmt.Printf("%v\n", o)
fmt.Printf("%#v\n", o)
fmt.Printf("%T\n", o)
fmt.Printf("100%%\n")

输出结果如下:

1
2
3
4
5
6
100
false
{小王子}
struct { name string }{name:"小王子"}
struct { name string }
100%

布尔型

占位符 说明
%t true或false

整型

占位符 说明
%b 表示为二进制
%c 该值对应的unicode码值
%d 表示为十进制
%o 表示为八进制
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于”U+%04X”
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示

示例代码如下:

1
2
3
4
5
6
7
n := 65
fmt.Printf("%b\n", n)
fmt.Printf("%c\n", n)
fmt.Printf("%d\n", n)
fmt.Printf("%o\n", n)
fmt.Printf("%x\n", n)
fmt.Printf("%X\n", n)

输出结果如下:

1
2
3
4
5
6
1000001
A
65
101
41
41

浮点数与复数

占位符 说明
%b 无小数部分、二进制指数的科学计数法,如-123456p-78
%e 科学计数法,如-1234.456e+78
%E 科学计数法,如-1234.456E+78
%f 有小数部分但无指数部分,如123.456
%F 等价于%f
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)

示例代码如下:

1
2
3
4
5
6
7
f := 12.34
fmt.Printf("%b\n", f)
fmt.Printf("%e\n", f)
fmt.Printf("%E\n", f)
fmt.Printf("%f\n", f)
fmt.Printf("%g\n", f)
fmt.Printf("%G\n", f)

输出结果如下:

1
2
3
4
5
6
6946802425218990p-49
1.234000e+01
1.234000E+01
12.340000
12.34
12.34

字符串和[]byte

占位符 说明
%s 直接输出字符串或者[]byte
%q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
%x 每个字节用两字符十六进制数表示(使用a-f
%X 每个字节用两字符十六进制数表示(使用A-F)

示例代码如下:

1
2
3
4
5
s := "小王子"
fmt.Printf("%s\n", s)
fmt.Printf("%q\n", s)
fmt.Printf("%x\n", s)
fmt.Printf("%X\n", s)

输出结果如下:

1
2
3
4
小王子
"小王子"
e5b08fe78e8be5ad90
E5B08FE78E8BE5AD90

指针

占位符 说明
%p 表示为十六进制,并加上前导的0x

示例代码如下:

1
2
3
a := 10
fmt.Printf("%p\n", &a)
fmt.Printf("%#p\n", &a)

输出结果如下:

1
2
0xc000094000
c000094000

宽度标识符

宽度通过一个紧跟在百分号后面的十进制数指定,如果未指定宽度,则表示值时除必需之外不作填充。精度通过(可选的)宽度后跟点号后跟的十进制数指定。如果未指定精度,会使用默认精度;如果点号后没有跟数字,表示精度为0。举例如下:

占位符 说明
%f 默认宽度,默认精度
%9f 宽度9,默认精度
%.2f 默认宽度,精度2
%9.2f 宽度9,精度2
%9.f 宽度9,精度0

示例代码如下:

1
2
3
4
5
6
n := 12.34
fmt.Printf("%f\n", n)
fmt.Printf("%9f\n", n)
fmt.Printf("%.2f\n", n)
fmt.Printf("%9.2f\n", n)
fmt.Printf("%9.f\n", n)

输出结果如下:

1
2
3
4
5
12.340000
12.340000
12.34
12.34
12

其他falg

占位符 说明
’+’ 总是输出数值的正负号;对%q(%+q)会生成全部是ASCII字符的输出(通过转义);
’ ‘ 对数值,正数前加空格而负数前加负号;对字符串采用%x或%X时(% x或% X)会给各打印的字节之间加空格
’-’ 在输出右边填充空白而不是默认的左边(即从默认的右对齐切换为左对齐);
’#’ 八进制数前加0(%#o),十六进制数前加0x(%#x)或0X(%#X),指针去掉前面的0x(%#p)对%q(%#q),对%U(%#U)会输出空格和单引号括起来的go字面值;
‘0’ 使用0而不是空格填充,对于数值类型会把填充的0放在正负号后面;

举个例子:

1
2
3
4
5
6
7
8
s := "小王子"
fmt.Printf("%s\n", s)
fmt.Printf("%5s\n", s)
fmt.Printf("%-5s\n", s)
fmt.Printf("%5.7s\n", s)
fmt.Printf("%-5.7s\n", s)
fmt.Printf("%5.2s\n", s)
fmt.Printf("%05s\n", s)

输出结果如下:

1
2
3
4
5
6
7
小王子
小王子
小王子
小王子
小王子
小王
00小王子

获取输入

Go语言fmt包下有fmt.Scanfmt.Scanffmt.Scanln三个函数,可以在程序运行过程中从标准输入获取用户的输入。

fmt.Scan

函数定签名如下:

1
func Scan(a ...interface{}) (n int, err error)
  • Scan从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因。

具体代码示例如下:

1
2
3
4
5
6
7
8
9
func main() {
var (
name string
age int
married bool
)
fmt.Scan(&name, &age, &married)
fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

将上面的代码编译后在终端执行,在终端依次输入小王子28false使用空格分隔。

1
2
3
$ ./scan_demo 
小王子 28 false
扫描结果 name:小王子 age:28 married:false

fmt.Scan从标准输入中扫描用户输入的数据,将以空白符分隔的数据分别存入指定的参数。

fmt.Scanf

函数签名如下:

1
func Scanf(format string, a ...interface{}) (n int, err error)
  • Scanf从标准输入扫描文本,根据format参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。

代码示例如下:

1
2
3
4
5
6
7
8
9
func main() {
var (
name string
age int
married bool
)
fmt.Scanf("1:%s 2:%d 3:%t", &name, &age, &married)
fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

将上面的代码编译后在终端执行,在终端按照指定的格式依次输入小王子28false

1
2
3
$ ./scan_demo 
1:小王子 2:28 3:false
扫描结果 name:小王子 age:28 married:false

fmt.Scanf不同于fmt.Scan简单的以空格作为输入数据的分隔符,fmt.Scanf为输入数据指定了具体的输入内容格式,只有按照格式输入数据才会被扫描并存入对应变量。

例如,我们还是按照上个示例中以空格分隔的方式输入,fmt.Scanf就不能正确扫描到输入的数据。

1
2
3
$ ./scan_demo 
小王子 28 false
扫描结果 name: age:0 married:false

fmt.Scanln

函数签名如下:

1
func Scanln(a ...interface{}) (n int, err error)
  • Scanln类似Scan,它在遇到换行时才停止扫描。最后一个数据后面必须有换行或者到达结束位置。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。

具体代码示例如下:

1
2
3
4
5
6
7
8
9
func main() {
var (
name string
age int
married bool
)
fmt.Scanln(&name, &age, &married)
fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

将上面的代码编译后在终端执行,在终端依次输入小王子28false使用空格分隔。

1
2
3
$ ./scan_demo 
小王子 28 false
扫描结果 name:小王子 age:28 married:false

fmt.Scanln遇到回车就结束扫描了,这个比较常用。

bufio.NewReader

有时候我们想完整获取输入的内容,而输入的内容可能包含空格,这种情况下可以使用bufio包来实现。示例代码如下:

1
2
3
4
5
6
7
func bufioDemo() {
reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
fmt.Print("请输入内容:")
text, _ := reader.ReadString('\n') // 读到换行
text = strings.TrimSpace(text)
fmt.Printf("%#v\n", text)
}

Fscan系列

这几个函数功能分别类似于fmt.Scanfmt.Scanffmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从io.Reader中读取数据。

1
2
3
func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)

Sscan系列

这几个函数功能分别类似于fmt.Scanfmt.Scanffmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从指定字符串中读取数据。

1
2
3
func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)
Share