Chapter1:Golang开篇
终于等到你!Go语言——让你用写Python代码的开发效率编写C语言代码
Golang方向
区块链研发工程师
Go服务器端/游戏软件工程师
Golang分布式/云计算软件工程师(消息系统)
Golang应用领域
区块链应用(分布式账本技术,去中心化,每个人均可参与数据库记录)
后端服务器应用(美团后台流量支撑程序-排序推荐搜索)
云计算/云服务后台应用(盛大云CDN-内容分发网络,京东消息推送云服务,分布式文件系统)
Go语言特点
简介:Go语言保证了既能达到静态编译语言的安全和性能,由达到动态语言开发维护的高效率,Go=C +Python。
1、从C语言中继承了很多理念,包括表达式语法,控制结构,基础数据类型,调用参数传值,指针等,也保留和C语言一样的编译执行方式及弱化的指针。举例:
1 | //go语言指针的使用特点 |
2、引入包的概念,用于组织程序结构,Go语言的一个文件都要归属于一个包,而不能单独存在
3、垃圾回收机制,内存自动回收,不需开发人员管理
4、天然并发(重要特点)
从语言层面支持并发,实现简单
goroutine
,轻量级线程,可实现大并发处理,高效利用多核
基于CPS并发模型实现
5、吸收了管道通信机制,形成Go语言特有的管道channel,通过管道channel可以实现不同的goroute之间的相互通信
6、函数可以返回多个值。举例:
1 | //写一个函数,实现同时返回和,差 |
7、新的创新:比如切片、延时执行defer(回收资源)等
goroutine特点
Go语言的并发是基于 goroutine
的,goroutine
类似于线程,但并非线程。可以将 goroutine
理解为一种虚拟线程。Go 语言运行时会参与调度 goroutine
,并将 goroutine
合理地分配到每个 CPU 中,最大限度地使用CPU性能。开启一个goroutine
的消耗非常小(大约2KB的内存),你可以轻松创建数百万个goroutine
goroutine
具有可增长的分段堆栈。这意味着它们只在需要时才会使用更多内存。goroutine
的启动时间比线程快。goroutine
原生支持利用channel安全地进行通信。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 | mkdir -p /usr/local/go # 创建目录 |
如果提示没有权限,加上sudo
以root用户的身份再运行。执行完就可以在/usr/local/
下看到go目录了。
配置环境变量: Linux下有两个文件可以配置环境变量,其中/etc/profile
是对所有用户生效的;$HOME/.profile
是对当前用户生效的,根据自己的情况自行选择一个文件打开,添加如下两行代码,保存退出。
1 | export GOROOT=/usr/local/go |
修改/etc/profile
后要重启生效,修改$HOME/.profile
后使用source命令加载$HOME/.profile
文件即可生效。 检查:
1 | ~ go version |
Go配置环境变量
GOROOT
和GOPATH
都是环境变量,其中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.io
或https://goproxy.cn
。
可以执行下面的命令修改GOPROXY:
1 | go env -w GOPROXY=https://goproxy.cn,direct |
Go语言快速入门
开发目录结构
goproject/src/go_code/project01/main or package
开发步骤
- 编写Go代码
- 通过
go build
命令对该go文件进行编译,生成可执行文件 - 在终端执行可执行文件
- 注意:通过
go run
命令可以直接运行go程序,生产环境都是先编译再执行
Go执行流程分析
- .go源文件 –>
go build
–> 可执行文件 –> 结果 - .go源文件 –>
go run
–> 结果
两种执行流程方式的区别:
- 如果先编译生成可执行文件,那么可以将该可执行文件拷贝到没有go开发环境的机器上,仍然可以运行
- 如果是直接
go run .go
源代码,那么如果在另一台机器上运行,需要go开发环境 - 在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件变大了很多
go install
go install
表示安装的意思,它先编译源代码得到可执行文件,然后将可执行文件移动到GOPATH
的bin目录下。因为我们的环境变量中配置了GOPATH
下的bin目录,所以我们就可以在任意地方直接执行可执行文件了。
编译和运行说明
编译
有了go源文件,通过编译器将其编译成机器可以识别的二进制码文件
在该源文件目录下,通过go build
对hello.go
文件进行编译。可以指定生成的可执行文件名,在windows下必须是.exe后缀
1 | // -o指定编译后的可执行文件名 |
如果程序没有错误,编译没有任何提示,反之编译时会在错误的那行报错
运行
直接运行编译完的可执行文件
go run
运行源代码
跨平台编译
默认我们go build
的可执行文件都是当前操作系统可执行的文件,如果我想在windows下编译一个linux下可执行文件,那需要怎么做呢?
只需要指定目标操作系统的平台和处理器架构即可:
1 | SET CGO_ENABLED=0 // 禁用CGO |
使用了cgo的代码是不支持跨平台编译的
然后再执行go build
命令,得到的就是能够在Linux平台运行的可执行文件了。
Mac 下编译 Linux 和 Windows平台 64位 可执行程序:
1 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build |
Linux 下编译 Mac 和 Windows 平台64位可执行程序:
1 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build |
Windows下编译Mac平台64位可执行程序:
1 | SET CGO_ENABLED=0 |
Go语言开发注意事项
- Go源文件以go为扩展名
- Go应用程序的执行入口是
main()
函数 - Go语言严格区分大小写
- Go方法由一条条语句构成,每个语句后不需要分号(Go语言会在每行后自动加分号),这也体现出Golang的简洁性
- Go编译器是一行一行进行编译的,因为我们一行就写一条语句,不能把多条语句写在同一行,否则报错
- Go语言定义的变量或者import的包没有使用到,代码不能编译通过
- 大括号是成对出现的,缺一不可
Go语言转义字符
转义符 | 含义 |
---|---|
\r |
回车符(返回行首) |
\n |
换行符(直接跳到下一行的同列位置) |
\t |
制表符 |
\' |
单引号 |
\" |
双引号 |
\\ |
反斜杠 |
注释与格式化
Go官方更推荐行注释
1 | 行注释://... |
正确的缩进和空白
- 使用
tab
与shift tab
- 使用
gofmt
来进行格式化 - 运算符两边各加一个空格
1 | // 格式化输出,但不保存 |
行长约定:一行最长不超过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 | break default func interface select |
此外,Go语言中还有37个保留字。
1 | Constants: true false iota nil |
变量介绍
概念:变量相当于内存中一个数据存储空间的表示,可以将变量看做一个房间的门牌号
变量来历:程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。
变量使用:声明变量 -> 赋值 -> 使用
Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。
1 | package main // 声明 main 包,表明当前是一个可执行程序 |
变量使用注意事项
变量=变量名+值+数据类型,变量三要素
变量表示内存中的一个存储区域,该区域有自己的名称(变量名)和类型(数据类型)
Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用
变量声明
标准声明
Go语言的变量声明格式为:
1 | var 变量名 变量类型 |
变量声明以关键字var
开头,变量类型放在变量的后面,行尾无需分号。 举个例子:
1 | var name string |
批量声明
每声明一个变量就需要写var
关键字会比较繁琐,go语言中还支持批量变量声明:
1 | var ( |
变量初始化
Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0
。 字符串变量的默认值为空字符串
。 布尔型变量默认为false
。 切片、函数、指针变量的默认为nil
。
当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:
1 | var 变量名 类型 = 表达式 |
举个栗子:
1 | var name string = "Q1mi" |
或者一次初始化多个变量
1 | var name, age = "Q1mi", 20 |
类型推导
有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。
1 | var name = "Q1mi" |
短变量声明
在函数内部,可以使用更简略的 :=
方式声明并初始化变量。
1 | package main |
多变量声明
1 | package main |
匿名变量
在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)
。 匿名变量用一个下划线_
表示,例如:
1 | func foo() (int, string) { |
匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua
等编程语言里,匿名变量也被叫做哑元变量。)
注意事项
- 函数外的每个语句都必须以关键字开始(var、const、func等)
:=
不能使用在函数外。_
多用于占位,表示忽略值。
补充
作用域的数据值可以在同一类型范围内不断变化
1 | package main |
变量在一个作用域内(一个函数或者代码块内)不能重命名
1 | package main |
判断变量类型
1 | // 方式一 |
1 | // 方式二 |
常量
相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var
换成了const
,常量在定义的时候必须赋值。
1 | const pi = 3.1415 |
声明了pi
和e
这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。
多个常量也可以一起声明:
1 | const ( |
const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。 例如:
1 | const ( |
上面示例中,常量n1
、n2
、n3
的值都是100。
iota
iota
是go语言的常量计数器,只能在常量的表达式中使用。
iota
在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota
计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。
举个栗子:
1 | const ( |
常见的iota
示例
使用_
跳过某些值
1 | const ( |
iota
声明中间插队
1 | const ( |
定义数量级 (这里的<<
表示左移操作,1<<10
表示将1的二进制表示向左移10位,也就是由1
变成了10000000000
,也就是十进制的1024。同理2<<2
表示将2的二进制表示向左移2位,也就是由10
变成了1000
,也就是十进制的8。)
1 | const ( |
多个iota
定义在一行
1 | const ( |
变量的数据类型
基本数据类型
整型
一个字节是八位,八位占存储空间一字节(1byte = 8bit
,bit
计算机中最小存储单位,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
等同于uint8
(0 ~ 2^8-1
),int16
对应C语言中的short
型,int64
对应C语言中的long
型
1 | package main |
Golang中整形变量在使用时,遵守保小不保大原则,避免存储空间浪费
1 | // 例如年龄,byte范围在(0,255) |
类型 | 描述 |
---|---|
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 | 无符号整型,用于存放一个指针 |
注意: 在使用int
和 uint
类型时,不能假定它是32位或64位的整型,而是考虑int
和uint
可能在不同平台上的差异。
注意事项: 获取对象的长度的内建len()
函数返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用int
来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用int
和 uint
。
浮点型
Go语言支持两种浮点型数:float32
和float64
。这两种浮点型数据格式遵循IEEE 754
标准: float32
的浮点数的最大范围约为 3.4e38
,可以使用常量定义:math.MaxFloat32
。 float64
的浮点数的最大范围约为 1.8e308
,可以使用一个常量定义:math.MaxFloat64
float32
,单精度,占用四字节float64
,双精度,占用八字节
浮点数存储=符号位+指数位+尾数位,浮点数的使用可能会丢失尾数造成精度损失
打印浮点数时,可以使用fmt
包配合动词%f
,代码如下:
1 | package main |
复数
complex64
和complex128
1 | var c1 complex64 |
布尔型
Go语言中以bool
类型进行声明布尔型数据,布尔型数据只有true(真)
和false(假)
两个值。
注意:
- 布尔类型变量的默认值为
false
- Go 语言中不允许将整型强制转换为布尔型
- 布尔型无法参与数值运算,也无法与其他类型进行转换
1 | package main |
字符型(没有专门的字符型,使用byte来保存单个字母字符)
Golang中没有专门的字符类型,如果要存储单个字符(字母),一般使用byte来保存
字符串就是一串固定长度的字符连接起来的字符序列,Go的字符串是由单个字节连接起来的
1 | package main |
对上面代码说明:
- 如果我们保存的字符在ASCII表的,比如[0-1,a-z,A-Z…]可以直接保存到byte
- 如果我们保存的字符对应码值均大于255,这时我们可以考虑使用int类型保存
- 如果我们需要按照字符的方式输出,这时我们需要格式化输出,即fmt.Printf(“%c”, c)
字符类型使用细节
字符常量使用单引号(‘’)括起来的单个字符。例如:var c1 byte = ‘a’,var c2 int = ‘中’,var c3 byte = ‘1’
Go中允许使用转义字符’\‘来将其后的字符转变为特殊字符型常量。例如:var c4 char = ‘\n’
Go语言的字符使用UTF-8编码,英文字母占1个字节,汉字占3个字节
在Go中,字符的本质是一个整数,直接输出时,是该字符对应的UTF-8编码的码值
可以直接给某个变量赋一个数字,然后按格式化输出%c,会输出该数字对应的Unicode字符
1
2
3var c4 int = 22269
fmt.Printf("c4 = %c\n",c4)
// c4 = 国字符类型是可以进行运算的,相当于一个整数,因为他都有Unicode码
1 | var c5 = 10 + 'a' // 10+97=107 |
字符类型本质探讨
- 字符存储到计算机中,需要将字符对应的码值(整数)找出来
- 字符和码值的对应关系是通过字符编码表决定的
- Go语言的编码都统一成了utf-8,很统一,没有编码乱码困扰
字符串
基本介绍:字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用utf-8编码表示Unicode文本,统一用双引号
包裹,多行字符串用反引号
包裹,字符用单引号
包裹
注意事项和使用细节
- Go语言的字符串的字节使用utf-8编码,golang统一使用utf-8,不会出现乱码
- 字符串一旦赋值了,就不能修改了。在Go中字符串是不可变的
- 字符串的两种表示形式,普通双引号括起来,反引号按原样输出
- 字符串拼接方式
- 多行字符串使用
- Go语言中字符串一定是用双引号包裹的,单引号包裹的是字符
1 | package main |
字符串的常用操作
方法 | 介绍 |
---|---|
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 | func changeString() { |
byte和rune类型
组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:
1 | var a := '中' |
Go 语言的字符有以下两种:
uint8
类型,或者叫 byte 型,代表了ASCII码
的一个字符rune
类型,代表一个UTF-8字符
当需要处理中文、日文或者其他复合字符时,则需要用到rune
类型。rune
类型实际是一个int32
。
Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。
1 | // 遍历字符串 |
输出:
1 | 104(h) 101(e) 108(l) 108(l) 111(o) 230(æ) 178(²) 153() 230(æ) 178(²) 179(³) |
因为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 | package main |
细节说明
- Go中,数据类型的转换可以是从表示范围小到表示范围大,也可以从范围大到范围小
- 被转换的是变量存储的数据,变量本身数据类型并没有变化
- 在转换中,比如将int64转成int8,编译时不会报错,只是转换的结果是按溢出处理,和我们希望的结果不一样
1 | package main |
举个栗子:计算直角三角形的斜边长时使用math包的Sqrt()函数,该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制类型转换为float64类型
1 | func sqrtDemo() { |
cookies:如果我们没有使用到一个包,但又不想删去,前面加一个 _,表示忽略
1 | import ( |
基本数据类型和string的转换
基本数据类型转string
方式1:fmt.Sprintf(“%参数”,表达式)
方式2:strconv包函数
1 | package main |
string类型转基本数据类型
使用strconv包函数
1 | package main |
注意事项:将string转换成基本类型时,要确保string类型能够转成有效的数据,比如我们可以把”123”转换成整数,但是不能把”hello”转换成整数,如果这样做,Golang直接将其转换成0,转换成bool的就变成false。
派生/复杂类型
指针(Pointer)
基本介绍
- 基本数据类型,变量存的就是值,也叫值类型
- 获取变量的地址,用
&
,比如:var n int
,获取n的地址:&n
- 指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值,例如:
var ptr *int = &n
- 获取指针类型所指向的值,使用
*
,比如:var ptr *int
,使用 *ptr获取ptr
指向的值
1 | package main |
错误示范
1 | func main() { |
指针细节说明
- 所有的值类型,都有对应的指针类型,形式为
*数据类型
- 值类型包括:基本数据类型
int
,float
,bool
,string
,数组和结构体struct
new
new()函数申请一个内存地址,返回的是类型指针,语法如下:
1 | func new(Type) *Type |
- Type表示类型,new函数只接受一个参数,这个参数是一个类型
- *Type表示类型指针,new函数返回一个指向该类型内存地址的指针
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值
1 | func main() { |
指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值
1 | package main |
make
make也是用于内存分配的,区别于new,他只用于slice,map,channel的内存创建,而且他返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型本身就是引用类型,所以就没必要返回他们的指针。make函数的语法如下:
1 | func make(t Type, size...IntegerType) Type |
make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作
1 | // 构造切片 |
new与make的区别
- 二者都是用来做内存分配的
- make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身
- 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针
值类型和引用类型
值类型:基本数据类型int
,float
,bool
,string
,数组和结构体struct
引用类型:指针,slice
切片,map
,管道channel
,interface
等都是引用类型
值类型和引用类型使用特点
值类型:变量直接存储值,内存通常在栈中分配
引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址的数据空间就成为一个垃圾,由GC
来回收
标识符的命名规范
标识符概念
Go对各种变量、方法、函数等命名时使用的字符序列称为标识符,凡是可以起名字的地方都叫标识符 var num int
标识符命名规则
- 由英文字母、数字、下划线组成
- 数字不可以开头
- golang中严格区分大小写
- 标识符不能包含空格
- 下划线在Go中是一个特殊的标识符,称为空标识符。可以代表任何其他的标识符,但是他对应的值会被忽略(比如,忽略某个返回值)。所以仅能作为占位符使用,不能作为表示符使用
- 不能以系统保留关键字作为标识符,比如
break
,if
等
标识符命名注意事项
- 包名:保持
package
的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,不要和标准库冲突 - 变量名、函数名、常量名采用驼峰法
- 如果变量名、函数名、常量名首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用(注:可以简单地理解成,首字母大写是共有的,首字母小写是私有的),golang中没有
public
、private
等关键字
数组(Array)
数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。 基本语法:
1 | // 定义一个长度为3元素类型为int的数组a |
数组定义
1 | var 数组变量名 [元素数量]T |
比如:var a [5]int
, 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。 [5]int
和[10]int
是不同的类型。
1 | var a [3]int |
数组可以通过下标进行访问,下标是从0
开始,最后一个元素下标是:len-1
,访问越界(下标在合法范围之外),则触发访问越界,会panic。
数组的初始化
数组的初始化也有很多方式
方法一
初始化数组时可以使用初始化列表来设置数组元素的值
1 | func main() { |
方法二
按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,例如:
1 | func main() { |
方法三
我们还可以使用指定索引值的方式来初始化数组,例如:
1 | func main() { |
数组的遍历
遍历数组有以下两种方法:
1 | func main() { |
多维数组
Go语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。
二维数组的定义
1 | func main() { |
二维数组的遍历
1 | func main() { |
注意: 多维数组只有第一层可以使用...
来让编译器推导数组长度。例如:
1 | //支持的写法 |
数组是值类型
数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
1 | func modifyArray(x [3]int) { |
注意:
- 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的
[n]*T
表示指针数组,*[n]T
表示数组指针
练习:
找出数组中和为指定值的两个元素的下标,比如从数组[1, 3, 5, 7, 8]
中找出和为8的两个元素的下标分别为(0,3)
和(1,2)
1 | package main |
结构体(struct)
切片(slice)
因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
。切片一般用于快速地操作一块数据集合
切片的定义
声明切片类型的基本语法如下:
1 | var name []T |
其中,
- name:表示变量名
- T:表示切片中的元素类型
举个例子:
1 | func main() { |
切片的长度和容量
切片拥有自己的长度和容量,我们可以通过使用内置的len()
函数求长度,使用内置的cap()
函数求切片的容量
注意:
- 切片指向了一个底层的数组
- 切片的长度就是它元素的个数
- 切片的容量是底层数组从切片的第一个元素到最后一个元素的数量
1 | package main |
切片表达式
切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。
切片的底层就是一个数组,所以可以基于数组通过切片表达式得到切片。 切片表达式中的low
和high
表示一个索引范围(左闭右开),也就是下面代码中从数组a中选出1<=索引值<4
的元素组成切片s,得到的切片长度=high-low
,容量等于底层数组从切片的第一个元素到最后一个元素的数量。
1 | package main |
为了方便起见,可以省略切片表达式中的任何索引。省略了low
则默认为0;省略了high
则默认为切片操作数的长度(和python相同):
1 | a[2:] // 等同于 a[2:len(a)] |
对于数组或字符串,如果0 <= low <= high <= len(a)
,则索引合法,否则就会索引越界(out of range)。
切片再切片
对切片再执行切片表达式时,索引的上限是切片的容量cap(s)
,而不是长度,即0 <= low <= high <= cap(s)
。常量索引必须是非负的,并且可以用int类型的值表示;对于数组或常量字符串,常量索引也必须在有效范围内。如果low
和high
两个指标都是常数,它们必须满足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 | func main() { |
完整切片表达式
对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:
1 | a[low : high : max] |
上面的代码会构造与简单切片表达式a[low: high]
相同类型、相同长度和元素的切片。另外,它会将得到的结果切片的容量设置为max-low
。在完整切片表达式中只有第一个索引值(low)可以省略;它默认为0。
1 | func main() { |
完整切片表达式需要满足的条件是0 <= low <= high <= max <= cap(a)
,其他条件和简单切片表达式相同。
修改底层数组的值
切片是引用类型,切片没有值,指向了底层一个数组,修改底层数组的值,会影响切片
1 | package main |
make()函数构造切片
上面都是基于数组来创建的切片,如果需要动态的创建一个切片,就需要使用内置的make()
函数,格式如下:
1 | make([]T, size, cap) |
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量,不写cap默认cap等于size
举个栗子:
1 | func main() { |
上面代码中a
的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)
返回2,cap(a)
则返回该切片的容量。
切片的本质
切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。(切片就是一个框,框住了一块连续的内存)
举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
,切片s1 := a[:5]
,相应示意图如下。
切片s2 := a[3:6]
,相应示意图如下:
判断切片是否为空
要检查切片是否为空,请始终使用len(s) == 0
来判断,而不应该使用s == nil
来判断
切片不能直接比较
切片之间是不能比较的,我们不能使用==
操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil
比较。 一个nil
值的切片并没有底层数组,一个nil
值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil
,例如下面的示例:
1 | var s1 []int //len(s1)=0;cap(s1)=0;s1==nil |
所以要判断一个切片是否是空的,要是用len(s) == 0
来判断,不应该使用s == nil
来判断。
切片的赋值拷贝
下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。
1 | func main() { |
切片遍历
切片的遍历方式和数组是一致的,支持索引遍历和for range
遍历
1 | func main() { |
append()方法为切片添加元素
Go语言的内建函数append()
可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…,…表示将切片元素拆开)
append追加元素,原来的底层数组放不下的时候,Go底层就会把底层数组换一个,还是要用原来的变量名去接收(比如公司扩容,公司名称不变)
1 | func main(){ |
注意:通过var声明的零值切片可以在append()
函数直接使用,无需初始化
1 | var s []int |
没有必要像下面的代码一样初始化一个切片再传入append()
函数使用
1 | s := []int{} // 没有必要初始化 |
每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()
函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
举个栗子:
1 | func main() { |
输出:
1 | [0] len:1 cap:1 ptr:0xc0000a8000 |
从上面的结果可以看出:
append()
函数将元素追加到切片的最后并返回该切片- 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍
append()函数还支持一次性追加多个元素。 例如:
1 | var citySlice []string |
切片的扩容策略
可以通过查看$GOROOT/src/runtime/slice.go
源码,其中扩容相关代码如下:
1 | newcap := old.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)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int
和string
类型的处理方式就不一样。
copy()函数复制切片
首先我们来看一个问题:
1 | func main() { |
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下:
1 | copy(destSlice, srcSlice []T) |
- destSlice: 目标切片
- srcSlice: 数据来源切片
举个栗子:
1 | func main() { |
切片删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
1 | func main() { |
总结一下就是:要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
,删除之后容量不发生变化
注意:删除元素也修改了底层数组,数组长度固定,所以需要前移并补充
1 | package main |
练习
以下代码会输出什么
1 | package main |
管道(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 | func main() { |
输出:
1 | map[小明:100 张三:90] |
map也支持在声明的时候填充元素,例如:
1 | func main() { |
判断某个键是否存在
Go语言中有个判断map中键是否存在的特殊写法,格式如下:
1 | value, ok := map[key] |
举个栗子:
1 | func main() { |
map的遍历
Go语言中使用for range
遍历map。
1 | func main() { |
但我们只想遍历key的时候,可以按下面的写法:
1 | func main() { |
只想遍历value的时候,采用匿名变量,忽略key即可
注意: 遍历map时的元素顺序与添加键值对的顺序无关。
使用delete()函数删除键值对
使用delete()
内建函数从map中删除一组键值对,delete()
函数的格式如下:
1 | delete(map, key) |
- map:表示要删除键值对的map
- key:表示要删除的键值对的键
示例代码如下:
1 | func main(){ |
按照指定顺序遍历map
1 | func main() { |
元素为map类型的切片
下面的代码演示了切片中的元素为map类型时的操作:
1 | func main() { |
值为切片类型的map
下面的代码演示了map中值为切片类型的操作:
1 | func main() { |
练习:
写一个程序,统计一个字符串中每个单词出现的次数。比如:”how do you do”中how=1 do=2 you=1
1 | package main |
观察下面代码,写出最终的打印结果
1 | package main |
1 | // [1 2 3] |
Chapter4:Golang运算符
运算符用于表示数据的运算、赋值和比较等
算术运算符
运算符 | 描述 |
---|---|
+ | 相加 |
- | 相减 |
* | 相乘 |
/ | 相除 |
% | 求余 |
注意: ++
(自增)和--
(自减)在Go语言中是单独的语句,并不是运算符。
1 | package main |
注意细节
- 对于除号,他的整数除和小数除是有区别的,整数之间做除法,只保留整数部分而舍弃小数部分。例如:
x := 19/5
,结果是3
- 当对一个数取模时,可以等价
a % b = a - a / b * b
,这是取模的本质运算 - Golang的自增自减只能当做一个独立语言使用,不能这样使用。例如:
a = i++
,if i++ >0
- Golang的
++
和--
只能写在变量的后面,不能写在前面,即没有++a和–a - 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位。 |
其他运算符
Go语言明确不支持三目运算符,只能用if..else..
表达
1 | package main |
python三元运算符
1 | 为真时的结果 if 判断条件 else 为假时的结果(注意,没有冒号) |
运算符优先级
顺序:
- 括号,
++
,--
- 单目运算
- 算数运算符
- 移位运算
- 关系运算符
- 位运算符
- 逻辑运算符
- 赋值运算符
- 逗号
键盘输入语句
接收用户输入的数据,就可以使用键盘输入语句来获取
函数:fmt.Scanln()或者fmt.Scanf()
1 | package main |
进制
n进制逢n进1
1 | package main |
二进制转十进制
从最低位(右边),将每个位上的数提取出来,乘以2的(位数-1)次方,然后求和
十进制转二进制
将该数不断除以2,直到商为0,然后将每步得到的余数倒过来,就是对应的二进制
八进制转十进制
从最低位(右边),将每个位上的数提取出来,乘以8的(位数-1)次方,然后求和
十进制转八进制
将该数不断除以8,直到商为0,然后将每步得到的余数倒过来,就是对应的八进制
十六进制转十进制
从最低位(右边),将每个位上的数提取出来,乘以16的(位数-1)次方,然后求和
十六进制转十进制
将该数不断除以16,直到商为0,然后将每步得到的余数倒过来,就是对应的十六进制
原码、反码、补码(帮助位运算)
对于有符号的而言:
二进制最高位是符号位:0表示正数,1表示负数
1==》[0000 0001] -1==》[1000 0001]
正数的原码,反码和补码都一样
负数的反码=它的原码符号位不变,其他位取反(0->1,1->0)
1==》原码[0000 0001] 反码[0000 0001] 补码[0000 0001]
-1==》原码[1000 0001] 反码[1111 1110] 补码[1111 1111]
负数的补码=它的反码+1
0的反码,补码都是0
在计算机运算的时候,都是以补码的方式来运算的
1-1 = 1+(-1)
Chapter5:Golang流程控制
控制决定程序执行
顺序控制
从上到下逐步执行
分支控制
循环控制
if-else分支控制
所有分支控制只有一个入口,满足条件执行,其他就不会执行
Go语言规定与if
匹配的左括号{
必须与if和表达式
放在同一行,{
放在其他位置会触发编译错误。 同理,与else
匹配的{
也必须与else
写在同一行,else
也必须与上一个if
或else if
右边的大括号在同一行。
单分支
1
2
3if 表达式1 {
分支1
}说明
当条件表达式为
true
时,就会执行{}
的代码,{}
是必须有的,就算只有一行代码1
2
3
4
5
6
7
8
9
10
11
12
13package main
import (
"fmt"
)
func main() {
var age byte
fmt.Println("请输入年龄:")
// 阻塞等待输入
fmt.Scanln(&age)
if age > 18 {
fmt.Println("已经成年")
}
}多分支
1
2
3
4
5
6
7if 表达式1 {
分支1
} else if 表达式2 {
分支2
} else{
分支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分支控制
基本介绍
switch
语句用于基于不同条件执行不同动作,每一个case
分支都是唯一的,从上到下逐一测试,知道匹配为止。- 匹配项后面也不需要再加
break
- Go语言规定每个
switch
只能有一个default
分支。
基本语法
1 | switch 表达式{ |
- 是她参加的执行流程是,先执行表达式,得到值,然后和
case
的表达式进行比较,如果相等,就匹配到,然后执行对应的case
的语句块,并退出switch
控制 - 如果
switch
的表达式的值没有和任何case
表达式匹配到,则执行default
语句块,执行后退出控制 - 在Golang中,
case
后的表达式可以有多个,使用逗号隔开 - Golang中
case
后不需要加break
,因为默认会有。即当程序执行完case
语句块后,就直接退出该switch
控制
1 | package main |
switch细节讨论
case/switch
后面是一个表达式(常量,变量,一个有返回值的函数等)case
后的各个表达式数据类型必须和switch
的表达式数据类型一致case
后面可以跟多个表达式,用逗号隔开case
后面的表达式如果是常量值,要求不可以重复case
后面不需要带break
,匹配到一个case
后就会执行对应的代码块,然后退出switch
,匹配不到则执行default
default
语句不是必须的switch
后也可以不带表达式,类似if-else
分支来使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package 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("未匹配")
}
}
switch
后也可以直接生命/定义一个变量,分号结束,不推荐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
18package 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("未匹配")
}
}type switch
:switch
语句还可以被用于type-switch
来判断某个interface
变量中实际指向的变量类型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package 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
类型的判断,使用if
,if
的使用范围更广
for循环控制
1 | package main |
基本语法
1 | for 循环变量初始化;循环条件;循环变量迭代{ |
说明
- 对于
for
循环来说,有四个要素 - 循环变量初始化
- 循环条件
- 循环操作,也叫循环体
- 循环变量迭代
执行顺序分析
- 执行循环变量初始化,比如
i := 1
- 执行循环条件,比如
i <= 10
- 如果循环条件为真,就执行循环操作,比如打印
- 执行循环变量迭代,比如
i++
- 反复执行2/3/4步骤,直到循环条件为
false
,退出for
循环
注意事项
循环条件是返回一个布尔值的表达式
for
循环的第二种使用方式,初始语句和结束语句都可以省略,这种写法类似于其他语言中的while
,在while
后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环循环变量初始化
for 循环判断条件 {
循环执行语句
循环变量迭代
}
for循环的初始语句可以被忽略,但是初始语句后的分号必须要写,举个栗子:
1 | func forDemo2() { |
1 | func forDemo3() { |
1 | package main |
死循环
for {
循环执行语句
}
等价于
for ; ; {}
是一个无限循环,通常需要配合break
语句使用1
2
3
4
5
6
7
8
9
10
11
12
13package main
import "fmt"
func main() {
k := 1
for { // 等价于for ; ; {
if k <= 10 {
fmt.Println(k)
} else {
break
}
k++
}
}golang提供
for-range
的方式,可以方便遍历数组、切片、字符串、map 及通道(channel)1
2
3
4
5
6
7
8
9
10
11
12
13package 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
10package 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
遍历的返回值有以下规律:- 数组、切片、字符串返回索引和值
map
返回键和值- 通道(channel)只返回通道内的值
练习
1 | // 100以内整除9的数之和 |
while和do…while实现
Go语言没有while
和do...while
语法,用for
循环实现
while实现
1 | 循环变量初始化 |
说明
for
循环是一个无线循环
break
语句跳出循环
do…while实现
1 | 循环变量初始化 |
说明
do...while
是先执行后判断,至少执行一次
当循环条件成立后,就会执行break
,break
就是跳出for
循环
多重循环控制
基本介绍
- 将一个循环放在另一个循环体内,就形成了嵌套循环。外边的成为外层循环,里面的成为内层循环。(建议不要超过3层)
- 实质上,嵌套循环就是把内层循环当成外层循环的循环体。当只有内层循环的循环条件为
false
时,才会跳出内层循环,才可结束外层的当次循环,开始下一次循环 - 设外层循环为
m
次时,内层为n
次,则内层循环实际上需要执行m*n
次
代码案例
1 | package main |
跳转控制语句-break
基本介绍:break
用于终止某个语句块的执行,中断当前for
循环或跳出switch
语句
注意事项:break
语句出现在多层嵌套的语句块中时,可以通过标签(label
)指明要终止的是哪一层语句块,标签要求必须定义在对应的for
、switch
和 select
的代码块上
需求:随机生成0-100的一个数,直到生成了99这个数,看看你一共用了几次
分析:编写一个无限循环的空值,然后不停的随机生成数,当生成了99时,就退出循环
1 | package main |
1 | // 标签的使用 |
break
默认会跳出最近的for
循环break
后面可以指定标签,跳出标签对应的for
循环
跳转控制语句-continue
基本介绍:continue
用于结束本次循环,继续执行下一次循环
注意事项:continue
语句出现在多层嵌套的语句块中时,可以通过标签(label
)指明要跳过的是哪一层语句块,和break
相同
案例分析:
1 | package main |
跳转控制语句-goto
基本介绍
- Go语言的
goto
语句可以无条件地转移到程序中指定的行 goto
语句通常与条件语句配合使用。可用来实现条件转移,跳出循环体等功能- 在Go程序设计中一般不主张使用
goto
语句,以免造成程序流程的混乱,使理解和调试程序都产生困难
基本语法
goto label
…
label:statement
goto流程图
案例演示
1 | package main |
举个简化代码的栗子:
1 | func gotoDemo1() { |
1 | func gotoDemo2() { |
跳转控制语句-return
基本介绍
return
使用在方法或者函数中,表示跳出所在方法或者函数
函数中return语句底层实现:在底层并不是原子操作,它分为给返回值赋值和RET指令两步
Chapter6:Golang函数
函数
基本介绍
为完成某一功能的程序指令(语句)的集合,称为函数
在Go中,函数分为自定义函数和系统函数
基本语法
func 函数名 (形参列表) (返回值列表) {
执行语句
return 返回值列表
}
函数可以有返回值,也可以没有返回值;
命名返回值就相当于在函数内部声明了返回值,可以直接return
案例:实现加减乘除计算
1 | package main |
变量作用域
全局作用域
函数作用域
现在函数内部找变量,找不到往外曾照
在函数内部的变量,外部是访问不到的
语句块作用域(for循环,if,switch分支结构)
包
包的介绍
包的本质实际上就是创建不同的文件夹,来存放程序文件
go的每一个文件都属于一个包,也就是说go是以包的形式来管理文件和项目目录结构的
包的三大作用
区分相同名字的函数、变量等标识符
当程序文件很多时,可以很好地管理项目
控制n数、变量等访问范围,即作用域
包的相关说明
打包基本语法
package util
引包基本语法
import “包的路径”
包的使用及注意事项
1)给一个文件打包时,该包对应一个文件夹,文件包名通常和文件所在文件夹名一致,一般为小写字母
比如utils文件夹对应的包名就是utils,包名也可使用别的名字,但是调用的时候需要使用对应包名
2)当一个文件要使用其他包函数或变量时,需要先引入对应的包
引入方式1:import “包名”
引入方式2:import (
“包名”
“包名”
)
package指令在文件第一行,然后是import指令、
在import包时,路径是从$GOPATH的src下开始,不用带src,编译器会自动从src下开始引入
3)为了让其他包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似Java的public,这样才能跨包访问
4)在访问其他包函数时,其语法是包名.函数名
5)如果包名较长,Go支持给包取别名,注意取别名后,原包名就不能使用了
6)在同一个包下,不能有相同的函数名(也不能有相当的全局变量名),否则报重复定义
7)如果要编译成一个可执行文件(go build -o),就需要将包声明为main,即package main,main包只能有一个,如果写一个库,包名可以自定义
注意:编译完生成一个库文件,如utils工具包会生成utils.a库文件,这样别人需要用库时就不需要拿到我们的源代码,用不可查看的库文件即可
函数-调用机制
调用过程
机制说明
- 在调用一个函数时,内存会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其他的栈的空间区分开来
- 在每个函数对应的栈中,数据空间是独立的,不会混淆
- 当一个函数执行完毕后,操作系统会销毁回收函数对应的栈空间
- 当函数有return语句时,就会将结果返回给调用者
return语句
- Go语言函数支持返回多个值
- 如果返回多个值时,在接收时,希望忽略,则使用_符号表示站位忽略
- 如果返回值只有一个,返回值类型列表可以不写括号 ()
1 | package main |
函数-递归调用
函数在函数体内调用本身,成为递归调用
斐波那契数列
1 | package main |
台阶问题
1 | package main |
代码分析
1 | package main |
test1函数示意图
递归重要原则
- 执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
- 函数的局部变量是独立的,不会相互影响
- 递归必须向退出递归的条件逼近,否则就是无线递归
- 当一个函数执行完毕,或者遇到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
21package 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= 30Go函数不支持重载,会报函数重复定义,python也不支持,python支持多个任意参数,用装饰器添加功能
在Go中,函数也是一种数据类型,可以赋值给一个变量,该变量就是一个函数类型的变量,通过该变量可以对函数调用
1
2
3
4
5
6
7
8
9
10
11
12
13package 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
20package 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
Go支持对函数返回值命名,返回值列表里变量已经创建好,默认为0,返回顺序依据返回值列表顺序。注意:要使用=必须事先已经声明
使用_标识符,忽略返回值
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
13package 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
15package 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函数最主要的作用就是完成一些初始化的工作,案例:
执行结果:
1
2
3
4
5
6//utils init()
//main display()
//main init()
//main()
//Age= 30
//Name= Tony
思考:如果main.go和utils.go都含有全局变量定义和init函数是,执行流程什么样呢?
匿名函数
Go支持匿名函数,在某个函数内部或者某个函数只是希望使用一次,可以使用匿名函数,匿名函数也可以实现多次调用
使用方式
- 在定义匿名函数时就直接调用
- 将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
1 | package main |
全局匿名函数
如果将匿名函数赋给一个全局变量,那么这个匿名函数就成为一个全局匿名函数,可以在程序中有效
1 | package main |
闭包
闭包就是一个函数与其相关的引用环境组合的一个整体(实体)。简单来说,闭包=函数+引用环境
闭包底层原理:
- 函数可以作为返回值
- 函数内部查找变量的顺序,现在自己内部找,找不到往外层找(
闭包=函数+包含外部变量的引用
)
案例演示
1 | func adder() func(int) int { |
变量f
是一个函数并且它引用了其外部作用域中的x
变量,此时f
就是一个闭包。 在f
的生命周期内,变量x
也一直有效。
闭包进阶示例1:
1 | func adder2(x int) func(int) int { |
闭包进阶示例2:
1 | func makeSuffixFunc(suffix string) func(string) string { |
闭包进阶示例3:
1 | func calc(base int) (func(int) int, func(int) int) { |
闭包其实并不复杂,只要牢记闭包=函数+引用外部变量
defer语句
Go语言中的defer
语句会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行,也就是说,先被defer
的语句最后被执行,最后被defer
的语句,最先被执行(先进后出)
1 | func main() { |
defer多用于函数结束之前释放资源(文件句柄,数据库连接,socket连接)
defer的执行时机
defer经典案例
顺序:先给返回值赋值,再defer注册,最后返回返回值
1 | package main |
defer面试题
1 | package main |
内置函数
内置函数 | 介绍 |
---|---|
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 | package main |
1 | package main |
注意:
recover()
必须搭配defer
使用defer
一定要在可能引发panic
的语句之前定义
Chapter7:Golang结构体
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
类型别名和自定义类型
自定义类型
在Go语言中有一些基本的数据类型,如string
、整型
、浮点型
、布尔
等数据类型, Go语言中可以使用type
关键字来定义自定义类型
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
1 | //将MyInt定义为int类型 |
通过type
关键字的定义,MyInt
就是一种新的类型,它具有int
的特性
类型别名
类型别名是Go1.9
版本添加的新功能
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人
1 | type TypeAlias = Type |
我们之前见过的rune
和byte
就是类型别名,他们的定义如下:
1 | type byte = uint8 |
类型定义和类型别名的区别
类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
1 | //类型定义 |
结果显示a的类型是main.NewInt
,表示main包下定义的NewInt
类型。b的类型是int
。MyInt
类型只会在代码中存在,编译完成时并不会有MyInt
类型。
struct结构体
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct
。 也就是我们可以通过struct
来定义自己的类型了,通过struct
来实现面向对象。
结构体的定义
结构体是值类型
使用type
和struct
关键字来定义结构体,具体代码格式如下:
1 | type 类型名 struct { |
- 类型名:标识自定义结构体的名称,在同一个包内不能重复
- 字段名:表示结构体字段名。结构体中的字段名必须唯一
- 字段类型:表示结构体字段的具体类型
举个例子,我们定义一个Person
(人)结构体,代码如下:
1 | type person struct { |
同样类型的字段也可以写在一行,
1 | type person1 struct { |
这样我们就拥有了一个person
的自定义类型,它有name
、city
、age
三个字段,分别表示姓名、城市和年龄。这样我们使用这个person
结构体就能够很方便的在程序中表示和存储人信息了。
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型
结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用var
关键字声明结构体类型。
1 | var 结构体实例 结构体类型 |
1 | package main |
通过.
来访问结构体的字段(成员变量),例如p1.name
和p1.age
等
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
1 | package main |
创建指针类型结构体
结构体是值类型,需要用指针类型去修改,Go语言传参数永远传的是拷贝副本
1 | package main |
通过使用new
关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
1 | var p2 = new(person) |
取结构体的地址实例化
使用&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作
1 | p3 := &person{} |
p3.name = "七米"
其实在底层是(*p3).name = "七米"
,这是Go语言帮我们实现的语法糖。
结构体初始化
没有初始化的结构体,其成员变量都是对应其类型的零值。
1 | type person struct { |
结构体内存布局
1 | type test struct { |
空结构体
空结构体是不占用空间的
1 | var v struct{} |
构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person
的构造函数。 因为struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
1 | func newPerson(name, city string, age int8) *person { |
调用构造函数
1 | p9 := newPerson("张三", "沙河", 90) |
方法和接收者
Go语言中的方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者 self
1 | func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { |
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
举个栗子:
1 | //Person 结构体 |
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
指针类型接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this
或者self
。 例如我们为Person
添加一个SetAge
方法,来修改实例变量的年龄。
1 | // SetAge 设置p的年龄 |
调用该方法:
1 | func main() { |
值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
1 | // SetAge2 设置p的年龄 |
什么时候应该使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int
类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
1 | //MyInt 将int定义为自定义MyInt类型 |
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
1 | //Person 结构体Person类型 |
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
1 | //Address 地址结构体 |
嵌套匿名结构体
1 | //Address 地址结构体 |
当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
1 | //Address 地址结构体 |
结构体的“继承”
Chapter8:Golang常用标准库
fmt
fmt包实现了类似C语言printf和scanf的格式化I/O。主要分为向外输出内容和获取输入内容两大部分。
向外输出
标准库fmt
提供了以下几种输出相关函数。
Print
系列函数会将内容输出到系统的标准输出,区别在于Print
函数直接输出内容,Printf
函数支持格式化输出字符串,Println
函数会在输出内容的结尾添加一个换行符。
1 | func Print(a ...interface{}) (n int, err error) |
举个简单的例子:
1 | func main() { |
执行上面的代码输出:
1 | 在终端打印该信息。我是:沙河小王子 |
Fprint
Fprint
系列函数会将内容输出到一个io.Writer
接口类型的变量w
中,我们通常用这个函数往文件中写入内容。
1 | func Fprint(w io.Writer, a ...interface{}) (n int, err error) |
举个例子:
1 | // 向标准输出写入内容 |
注意,只要满足io.Writer
接口的类型都支持写入。
Sprint
Sprint
系列函数会把传入的数据生成并返回一个字符串。
1 | func Sprint(a ...interface{}) string |
简单的示例代码如下:
1 | s1 := fmt.Sprint("沙河小王子") |
Errorf
Errorf
函数根据format参数生成格式化字符串并返回一个包含该字符串的错误。
1 | func Errorf(format string, a ...interface{}) error |
通常使用这种方式来自定义错误类型,例如:
1 | err := fmt.Errorf("这是一个错误") |
Go1.13版本为fmt.Errorf
函数新加了一个%w
占位符用来生成一个可以包裹Error的Wrapping Error。
1 | e := errors.New("原始错误e") |
格式化占位符
*printf
系列函数都支持format格式化参数,在这里我们按照占位符将被替换的变量类型划分,方便查询和记忆。
通用占位符
占位符 | 说明 |
---|---|
%v | 值的默认格式表示 |
%+v | 类似%v,但输出结构体时会添加字段名 |
%#v | 值的Go语法表示 |
%T | 打印值的类型 |
%% | 百分号 |
示例代码如下:
1 | fmt.Printf("%v\n", 100) |
输出结果如下:
1 | 100 |
布尔型
占位符 | 说明 |
---|---|
%t | true或false |
整型
占位符 | 说明 |
---|---|
%b | 表示为二进制 |
%c | 该值对应的unicode码值 |
%d | 表示为十进制 |
%o | 表示为八进制 |
%x | 表示为十六进制,使用a-f |
%X | 表示为十六进制,使用A-F |
%U | 表示为Unicode格式:U+1234,等价于”U+%04X” |
%q | 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示 |
示例代码如下:
1 | n := 65 |
输出结果如下:
1 | 1000001 |
浮点数与复数
占位符 | 说明 |
---|---|
%b | 无小数部分、二进制指数的科学计数法,如-123456p-78 |
%e | 科学计数法,如-1234.456e+78 |
%E | 科学计数法,如-1234.456E+78 |
%f | 有小数部分但无指数部分,如123.456 |
%F | 等价于%f |
%g | 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出) |
%G | 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出) |
示例代码如下:
1 | f := 12.34 |
输出结果如下:
1 | 6946802425218990p-49 |
字符串和[]byte
占位符 | 说明 |
---|---|
%s | 直接输出字符串或者[]byte |
%q | 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示 |
%x | 每个字节用两字符十六进制数表示(使用a-f |
%X | 每个字节用两字符十六进制数表示(使用A-F) |
示例代码如下:
1 | s := "小王子" |
输出结果如下:
1 | 小王子 |
指针
占位符 | 说明 |
---|---|
%p | 表示为十六进制,并加上前导的0x |
示例代码如下:
1 | a := 10 |
输出结果如下:
1 | 0xc000094000 |
宽度标识符
宽度通过一个紧跟在百分号后面的十进制数指定,如果未指定宽度,则表示值时除必需之外不作填充。精度通过(可选的)宽度后跟点号后跟的十进制数指定。如果未指定精度,会使用默认精度;如果点号后没有跟数字,表示精度为0。举例如下:
占位符 | 说明 |
---|---|
%f | 默认宽度,默认精度 |
%9f | 宽度9,默认精度 |
%.2f | 默认宽度,精度2 |
%9.2f | 宽度9,精度2 |
%9.f | 宽度9,精度0 |
示例代码如下:
1 | n := 12.34 |
输出结果如下:
1 | 12.340000 |
其他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 | s := "小王子" |
输出结果如下:
1 | 小王子 |
获取输入
Go语言fmt
包下有fmt.Scan
、fmt.Scanf
、fmt.Scanln
三个函数,可以在程序运行过程中从标准输入获取用户的输入。
fmt.Scan
函数定签名如下:
1 | func Scan(a ...interface{}) (n int, err error) |
- Scan从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符。
- 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因。
具体代码示例如下:
1 | func main() { |
将上面的代码编译后在终端执行,在终端依次输入小王子
、28
和false
使用空格分隔。
1 | $ ./scan_demo |
fmt.Scan
从标准输入中扫描用户输入的数据,将以空白符分隔的数据分别存入指定的参数。
fmt.Scanf
函数签名如下:
1 | func Scanf(format string, a ...interface{}) (n int, err error) |
- Scanf从标准输入扫描文本,根据format参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中。
- 本函数返回成功扫描的数据个数和遇到的任何错误。
代码示例如下:
1 | func main() { |
将上面的代码编译后在终端执行,在终端按照指定的格式依次输入小王子
、28
和false
。
1 | $ ./scan_demo |
fmt.Scanf
不同于fmt.Scan
简单的以空格作为输入数据的分隔符,fmt.Scanf
为输入数据指定了具体的输入内容格式,只有按照格式输入数据才会被扫描并存入对应变量。
例如,我们还是按照上个示例中以空格分隔的方式输入,fmt.Scanf
就不能正确扫描到输入的数据。
1 | $ ./scan_demo |
fmt.Scanln
函数签名如下:
1 | func Scanln(a ...interface{}) (n int, err error) |
- Scanln类似Scan,它在遇到换行时才停止扫描。最后一个数据后面必须有换行或者到达结束位置。
- 本函数返回成功扫描的数据个数和遇到的任何错误。
具体代码示例如下:
1 | func main() { |
将上面的代码编译后在终端执行,在终端依次输入小王子
、28
和false
使用空格分隔。
1 | $ ./scan_demo |
fmt.Scanln
遇到回车就结束扫描了,这个比较常用。
bufio.NewReader
有时候我们想完整获取输入的内容,而输入的内容可能包含空格,这种情况下可以使用bufio
包来实现。示例代码如下:
1 | func bufioDemo() { |
Fscan系列
这几个函数功能分别类似于fmt.Scan
、fmt.Scanf
、fmt.Scanln
三个函数,只不过它们不是从标准输入中读取数据而是从io.Reader
中读取数据。
1 | func Fscan(r io.Reader, a ...interface{}) (n int, err error) |
Sscan系列
这几个函数功能分别类似于fmt.Scan
、fmt.Scanf
、fmt.Scanln
三个函数,只不过它们不是从标准输入中读取数据而是从指定字符串中读取数据。
1 | func Sscan(str string, a ...interface{}) (n int, err error) |