作者的话:
- 这篇文章是之前刚从Java转到Go,学习时编写的,力求全面且详细。
- 本文是基础内容,适合初学者,也适合老手用来当备忘录。和网上其他文档不同的是,我陆陆续续将很多小的知识点也补进来了,后续也会继续补充。
- 这篇文章开始是我写在本机,然后又改写到自己的个人博客,现在有誊写到CSDN,而且内容达到3w+字。其中难免会有遗漏和错误,希望读者发现后能够评论告知我。
Golang
简介
Go编程语言起源于 2007 年,2009 年正式对外发布,是一个开源项目。旨在提高软件开发者编程效率,在不损失应用程序性能的情况下降低代码的复杂性。
Go 富有表现力、简洁、干净、高效。它的并发机制便于编写能够充分利用多核和联网机器的程序,而其新颖的类型系统可以实现灵活和模块化的程序构建。Go可以快速编译成机器代码,同时还有便利的垃圾回收机制和强大的运行时反射功能。它是一种静态编译语言,但是快得动态解释语言。它的主要目标是“兼具 Python 等动态语言的开发速度和 C/C++ 等编译型语言的性能与安全性”。
Go语言有时候被描述为“C 类似语言”,或者是“21 世纪的C语言”。Go 从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想,还有C语言一直所看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。 Go语言是编程语言设计的又一次尝试,是对类C语言的重大改进,它不但能让你访问底层操作系统,还提供了强大的网络编程和并发编程支持。Go语言的用途众多,可以进行网络编程、系统编程、并发编程、分布式编程。
很多重要的开源项目都是使用Go语言开发的,其中包括 Docker、Go-Ethereum、Thrraform 和 Kubernetes。
特点
-
简单易学
Go 语言简单易学,学习曲线平缓,不需要像 C/C++ 语言动辄需要两到三年的学习期。
-
代码风格统一
Go 语言提供了一套格式化工具——go fmt。
-
开发、执行效率高
-
原生支持并发
Go语言在多核并发上拥有原生的设计优势,Go语言从底层原生支持并发,无须第三方库、开发者的编程技巧和开发经验。
-
轻量级类型系统
Go语言没有类和继承的概念,所以它和 Java 或 C++ 看起来并不相同。但是它通过接口(interface)的概念来实现多态性。Go语言有一个清晰易懂的轻量级类型系统,在类型之间也没有层级之说。因此可以说Go语言是一门混合型的语言。
为什么要学习Go语言
如果你要创建系统程序,或者基于网络的程序,Go语言是很不错的选择。作为一种相对较新的语言,它是由经验丰富且受人尊敬的计算机科学家设计的,旨在应对创建大型并发网络程序面临的挑战。
在Go语言出现之前,开发者们总是面临非常艰难的抉择,究竟是使用执行速度快但是编译速度并不理想的语言(如:C++),还是使用编译速度较快但执行效率不佳的语言(如:.NET、Java),或者说开发难度较低但执行速度一般的动态语言呢?显然,Go语言在这 3 个条件之间做到了最佳的平衡:快速编译,高效执行,易于开发。
Go语言支持交叉编译,比如说你可以在运行 Linux 系统的计算机上开发可以在 Windows 上运行的应用程序。这是第一门完全支持 UTF-8 的编程语言,这不仅体现在它可以处理使用 UTF-8 编码的字符串,就连它的源码文件格式都是使用的 UTF-8 编码。Go语言做到了真正的国际化!
适合场景
- 服务端开发
- 分布式系统,微服务
- 网络编程
- 区块链开发
- 内存KV数据库,例如boltDB、levelDB
- 云平台
Go语言吉祥物
Go语言有一个吉祥物,在会议、文档页面和博文中,大多会包含下图所示的 Go Gopher,这是才华横溢的插画家 Renee French 设计的,她也是 Go 设计者之一 Rob Pike 的妻子。
开发环境
Go的安装
安装方法参考官方文档:
Download and install - The Go Programming Language
安装后验证的命令:
1 | go version |
查看Go配置的命令:
1 | go env |
CentOS
使用yum安装go:
1 2 3 4 5 6 7 8 | # 安装Golang yum install go # 查看Go版本 go version # 查看Go配置 go env |
IDE编辑器
Goland(推荐)
官网:
GoLand by JetBrains: More than just a Go IDE
VSCode
官网:
Visual Studio Code - Code Editing. Redefined
语言基础
命名规则
所有的go源码都是以 “.go” 结尾。
Go的函数、变量、常量、自定义类型、包(package)
的命名方式遵循以下规则:
1)首字符可以是任意的Unicode字符或者下划线
2)剩余字符可以是Unicode字符、下划线、数字
3)字符长度不限
关键字
GO语言共有如下25个关键字:
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
保留字
Go语言共有如下37个保留字:
Constants: true false iota nil
Types: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
Functions: make len cap new append copy close delete
complex real imag
可见性
Go语言中声明的可见性通过声明的位置和名称首字母的大小写来控制
声明位置 | 首字母大小写 | 可见性 |
---|---|---|
函数内 | 大 | 函数的本地值,类似private |
函数内 | 小 | 函数的本地值,类似private |
函数外 | 大 | 所有包可见 |
函数外 | 小 | 对当前包可见,类似protect |
Go命令
-
bug:start a bug report
输入此命名后会直接打开默认浏览器,显示go的github页面进行bug报告,并会自动添加系统的信息。
-
build:compile packages and dependencies
此命令用于编译指定的源码文件或代码包及依赖包。
-
clean:remove object files and cached files
go clean命令用于删除执行其他命令时产生的文件或目录.
-
doc:show documentation for package or symbol
打印附于 Go 语言程序实体上的文档。
-
env:print Go environment information
go env 可以查看当前go的环境变量
-
fix:update packages to use new APIs
把指定代码包的所有
Go
语言源码文件中的旧版本代码修正为新版本的代码。 -
fmt:gofmt (reformat) package sources
go fmt用于检查并格式化成go语言的规范格式。
-
generate:generate Go files by processing source
go generate 用于在编译前自动化生成某类代码
-
get:add dependencies to current module and install them
go get命令用于动态获取远程代码包及其依赖包,并进行编译和安装。
-
install:compile and install packages and dependencies
go install 跟 go build 类似,只是多做了一件事就是安装编译后的文件到指定目录。
-
list:list packages or modules
go list 会列出当前安装的包
-
mod:module maintenance
go mod 是 go modules的简写,用于对go包的管理。
-
work:workspace maintenance
Go工作区模式相关
-
run:compile and run Go program
go run用于编译并运行源码文件,由于包含编译步骤,所以go build参数都可用于go run,在go run 中只接受go源码文件而不接受代码包。
-
test:test packages
自动测试通过导入路径命名的包
-
tool:run specified go tool
运行
Go
提供的工具。 -
version:print Go version
go version 可以查看当前go的版本
-
vet:report likely mistakes in packages
是一个用于检查
Go
语言源码中静态错误的简单工具。
运算符
算术运算符
运算符 | 描述 |
---|---|
+ | 相加 |
- | 相减 |
* | 相乘 |
/ | 相除 |
% | 求余 |
注意: ++(自增)和–(自减)在Go语言中是单独的语句,并不是运算符。
逻辑运算符
运算符 | 描述 |
---|---|
&& | 逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。 |
ll | 逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。 |
! | 逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。 |
关系运算符
运算符 | 描述 |
---|---|
== | 检查两个值是否相等,如果相等返回 True 否则返回 False。 |
!= | 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 |
> | 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 |
>= | 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 |
< | 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 |
<= | 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 |
位运算符
位运算符对整数在内存中的二进制位进行操作。
运算符 | 描述 |
---|---|
& | 参与运算的两数各对应的二进位相与。(两位均为1才为1) |
l | 参与运算的两数各对应的二进位相或。(两位有一个为1就为1) |
^ | 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。(两位不一样则为1) |
<< | 左移n位就是乘以2的n次方。“a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。 |
>> | 右移n位就是除以2的n次方。“a>>b”是把a的各二进位全部右移b位。 |
赋值运算符
运算符 | 描述 |
---|---|
= | 简单的赋值运算符,将一个表达式的值赋给一个左值 |
+= | 相加后再赋值 |
-= | 相减后再赋值 |
*= | 相乘后再赋值 |
/= | 相除后再赋值 |
%= | 求余后再赋值 |
<<= | 左移后赋值 |
>>= | 右移后赋值 |
&= | 按位与后赋值 |
l= | 按位或后赋值 |
^= | 按位异或后赋值 |
占位符
_下划线在Go语言中表示占位符,可以在import语句或赋值语句中使用。
import语句中使用:
有时候我们导入一个包,只是为了执行该包中的所有init函数,而不需要使用包中的其他内容。此时可以使用下划线占位符来导入该包,例如:
import _ "./hello"
赋值语句中使用:
go语言的函数支持返回一个或多个值,有时我们并不需要这些返回值,或者只需要一部分返回值。此时我们可以使用下划线占位符来忽略哪些我们并不需要的返回值,例如:
file, _ := os.Open("xxxxxx")
变量
何为变量
程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。
变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型、浮点型、布尔型等。
Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。
变量声明
Go语言中声明变量的格式如下:
// 声明单个变量
var <变量名> <变量类型>
// 声明同一类型的多个变量
var <变量名1>,<变量名2>,<变量名n> <变量类型>
// 声明不同类型的多个变量
var (
<变量名1> <变量类型1>
<变量名2> <变量类型2>
<变量名n> <变量类型n>
)
示例:
// 声明单个变量
var a int
// 声明同一类型的多个变量
var b, c, d int
// 声明不同类型的多个变量
var (
e string
f int
)
变量声明并初始化
Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false
。 切片、函数、指针变量的默认为nil
。
当然我们也可在声明变量的时候为其指定初始值。变量初始化的格式如下:
// 声明并初始化单个变量
var <变量名> <变量类型> = <表达式 / 值>
// 声明并初始化同一类型的多个变量
var <变量名1>,<变量名2>,<变量名n> <变量类型> = <表达式1/值1>,<表达式2/值2>,<表达式n/值n>
// 声明并初始化不同类型的多个变量
var (
<变量名1> <变量类型1> = <表达式1/值1>
<变量名2> <变量类型2> = <表达式2/值2>
<变量名n> <变量类型n> = <表达式n/值n>
)
示例:
// 声明并初始化单个变量
var a int = 1 + 1
// 声明并初始化同一类型的多个变量
var b, c, d int = 1, 2, 3
// 声明并初始化不同类型的多个变量
var (
e string = ""
f int = 1
)
类型推导
在变量声明并初始化时,如果我们不设置变量类型,编译器会根据等号右边的值或表达式结果来推导变量的类型,完成初始化。
所以如果编译器推导的类型与我们的目标类型一致,就可以不编写变量类型:
var a int = 1 // 指定变量类型为int
var a = 1 // 编译器推导变量类型为int
var a int32 = 1 // 指定变量类型为int32
在上述示例中,第一个语句和第二个语句效果完全一致。只有在第三种情况,编译器推导的变量类型与我们的目标变量类型不一致时,才有必要编写变量类型。
简短变量声明并初始化
在类型推导章节中,我们知道当编译器推导的类型与我们的目标类型一致时,var a int =1
和 var a = 1
效果完全一样。
在函数内部,可以将这两个表达式替换成更简略的:=
方式声明并初始化变量:
a := 1
匿名变量
当使用下划线占位符来代替变量名称时,即称该占位符代表的变量为匿名变量,表示忽略值。匿名变量最常用的方式是用来接收调用方法的返回值,表示忽略该返回值。
常量
何为常量
相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。
常量声明与初始化
常量的声明和变量声明非常类似,只是把var
换成了const
,常量在定义的时候必须进行初始化,且初始化后不允许再次赋值。
// 声明并初始化单个常量
const <变量名> <类型> = <表达式 / 值>
// 声明并初始化同一类型的多个常量
const <变量名1>,<变量名2>,<变量名n> <类型> = <表达式1/值1>,<表达式2/值2>,<表达式n/值n>
// 声明并初始化不同类型的多个常量
const (
<变量名1> <类型1> = <表达式1/值1>
<变量名2> <类型2> = <表达式2/值2>
<变量名n> <类型n> = <表达式n/值n>
)
示例:
// 声明并初始化单个常量
const a int = 1 + 1
// 声明并初始化同一类型的多个常量
const b, c, d int = 1, 2, 3
// 声明并初始化不同类型的多个常量
const (
e string = ""
f int = 1
)
常量声明与初始化时,如果类型推导的类型与目标类型一致,也可以忽略常量类型的编写。
特别注意:
在第三种声明并初始化常量的方式中,如果省略了值则表示和上面一行的值相同。
const (
a int = 1 // a=1
b // b=1
c = 2 // c=2
d // d=2
)
常量计数器iota
iota
是go
语言的常量计数器,只能在常量的表达式中使用。 iota
在常量定义中出现时将被重置为0
。批量定义常量时,const
中每新增一行常量声明将使iota
计数一次(iota
可理解为const
语句块中的行索引)。 使用iota
能简化定义,在定义枚举时很有用。
- 定义单个常量时,iota=0。
const a = iota // a=0 const b = iota // b=0 const c // 报错
- 批量定义同一行的多个常量时,iota可以出现多次,但每次都是0.
const a,b,c = iota,1,iota // a=0,b=1,c=0
- 批量定义不同行的多个常量时,iota的值为所在行数(从0开始计算)。
const ( a = iota // a=0 b // b=iota=1 c // c=iota=2 ) const ( a = iota*2 // a=0 b // b=iota*2=1*1=2 c // c=iota*2=2*2=4 ) const ( a = 4 // a=4 b = iota // b=iota=1 c // c=iota=2 ) const ( a = iota // a=iota=0 b = 100 // b = 100 c // c=100 d = iota // d=iota=3 e // e=iota=4 ) const ( a, b = iota+1, iota+2 // a=1,b=2 c, d // c=2,d=3 e, f // e=3,f=4 )
基本类型
整型
整型主要分为两类:
-
有符号整型
1 2 3
int8 int16 int32 int64 int rune(int32)
-
int 是有符号整数类型,大小至少为 32 位。然而,它是一种独特的类型,而不是 int32 等类型的别名。
-
rune 是 int32 的别名,在所有方面与 int32 等效。按照惯例,它用于区分字符值和整数值。
-
-
无符号整型
1 2 3 4
uint8 uint16 uint32 uint64 uint byte(uint8) uintptr
-
uint 是一种无符号整数类型,大小至少为 32 位。然而,它是一种独特的类型,而不是 uint32 等类型的别名。
-
uintptr 是一个足够大的整数类型,可以容纳任何指针的位模式。
-
byte 是 uint8 的别名,在所有方面与 uint8 等效。按照惯例,它用于区分字节值和 8 位无符号整数值。
-
浮点型
Go语言支持两种浮点型数:float32
和float64
。这两种浮点型数据格式遵循IEEE 754
标准: float32
的浮点数的最大范围约为3.4e38
,可以使用常量定义:math.MaxFloat32
。 float64
的浮点数的最大范围约为 1.8e308
,可以使用一个常量定义:math.MaxFloat64
。
复数
complex64
和complex128
复数有实部和虚部,complex64
的实部和虚部为32位,complex128
的实部和虚部为64位。
布尔值
Go语言中以bool
类型进行声明布尔型数据,布尔型数据只有true(真)
和false(假)
两个值。
-
布尔类型变量的默认值为false。
-
Go 语言中不允许将整型强制转换为布尔型.
-
布尔型无法参与数值运算,也无法与其他类型进行转换。
字符串string
Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)
一样。 Go 语言里的字符串的内部实现使用UTF-8编码。
字符串转义符
Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。
转义 | 含义 |
---|---|
\r | 回车符(返回行首) |
\n | 换行符(直接跳到下一行的同列位置) |
\t | 制表符 |
' | 单引号 |
" | 双引号 |
\ | 反斜杠 |
多行字符串
Go语言中要定义一个多行字符串时,就必须使用反引号
字符:
1 2 3 4 5 | s1 := `第一行 第二行 第三行 ` fmt.Println(s1) |
反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
字符串的常用操作
方法 | 介绍 |
---|---|
len(str) | 求长度 |
+或fmt.Sprintf | 拼接字符串 |
strings.Split | 分割 |
strings.Contains | 判断是否包含 |
strings.HasPrefix,strings.HasSuffix | 前缀/后缀判断 |
strings.Index(),strings.LastIndex() | 子串出现的位置 |
strings.Join(a[]string, sep string) | join操作 |
字符串修改
要修改字符串,需要先将其转换成[]rune或[]byte
,完成后再转换为string
。无论哪种转换,都会重新分配内存,并复制字节数组。
byte和rune类型
组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:
1 2 3 | var a := '中' var b := 'x' |
Go 语言的字符有以下两种:
1 2 3 | uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。 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 := "pprof.cn博客" 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 | 112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 229(å) 141() 154() 229(å) 174(®) 162(¢) 112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 21338(博) 23458(客) |
因为UTF8编码下一个中文汉字由3~4
个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果。
字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。
字符串拷贝
参考:《函数 - 内置函数 - copy》章节
从字符串截取切片
参考 《语言基础 - 切片Slice - 切片截取 》 章节
数组Array
定义
数组是同一种数据类型的固定长度的序列。
定义数组的格式为:
1 | var 数组名称 [数组长度]数据类型 |
注意:
-
数组长度是数组类型的一部分,因此
var a [5] int
和var b [10]int
是不同的类型。 -
数组长度必须是常量。
-
数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
-
支持 “==”、”!=” 操作符,因为内存总是被初始化过的。
-
指针数组 [n]*T,数组指针 *[n]T。
初始化
-
常规初始化
-
一维数组
1
var arr = [5]int{1,2,3,4,5}
-
多维数组
1 2 3 4
var arr = [2][2]int{ {1,2}, {3,4} }
-
-
根据初始化元素确定数组长度
-
一维数组
1
var arr = [...]int{1,2,3,4,5}
-
多维数组
1 2 3 4
var arr = [...][2]int{ {1, 2}, {3, 4}, }
多维数组只有第一维支持根据初始化元素确定数组长度
-
-
使用索引号初始化元素
-
一维数组
1 2
var arr = [...]int{1: 2, 4: 5} var arr2 = [5]int{1: 2, 4: 5}
-
多维数组
1 2 3 4 5 6 7 8
var arr = [2][2]int{ 0: {1: 2}, 1: {1: 4}, } var arr2 = [...][2]int{ 0: {1: 2}, 1: {1: 4}, }
-
数组长度
内置函数 len 和 cap 都返回数组长度 (元素数量)。
参考:《函数 - 内置函数 - len》章节
参考:《函数 - 内置函数 - cap》章节
遍历数组
使用range:
var arr = [...]int{1, 2, 3, 4, 5}
// range只取索引
for index := range arr {
fmt.Println(arr[index])
}
// range取索引和值
for index, value := range arr {
fmt.Println(index, value)
}
不使用range:
var arr = [...]int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}
数组截取
参考 《语言基础 - 切片Slice - 切片截取 》 章节
切片Slice
定义
slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。
-
切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
-
切片的长度可以改变,因此,也可以说切片是可变数组。
-
切片也可以是多维的
切片的定义格式如下:
1 | var 切片名称 []数据类型 |
初始化
切片定义后,默认为nil,切片初始化方式有以下几种:
-
初始化赋值
var slice = []int{1, 2, 3, 4, 5} fmt.Println(len(slice), cap(slice), slice, slice == nil) // 5 5 [1 2 3 4 5] false
-
make函数
参考:《函数 - 内置函数 - make - slice》章节
-
从数组或Slice中截取
参考 《语言基础 - 切片Slice - 切片截取 》 章节
切片截取
截取切片 / 数组 / 字符串 的格式如下:
1 | slice := 切片/数组/字符串名称[开始索引:结束索引:最大位置] |
-
截取的范围从开始索引(包含)到结束索引(不包含)。
-
最大位置用于设置slice的容量,
cap(slice)=最大位置-开始索引
。 -
如果是从字符串截取,slice的类型必须为
[]byte
-
开始索引、结束索引、最大位置必须满足以下条件
1
0 <= startIndex <= endIndex <= maxIndex <= cap(原数组/切片)
内存结构
切片的底层是数组,其数据结构如下图所示:
拷贝
参考:《函数 - 内置函数 - copy》章节
元素追加
参考:《函数 - 内置函数 - append》章节
元素删除
Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素。
使用冒号操作符来删除元素:
这种方式常常用于删除前置元素 或 删除后置元素
// 删除前置元素
arr := []int{1, 2, 3, 4, 5}
arr := arr[2:] // [3, 4, 5]
// 删除后置元素
arr := []int{1, 2, 3, 4, 5}
arr := arr[:2] // [1,2]
单纯使用冒号操作符无法删除中间元素,还需要搭配append内置函数来实现,参考《使用append内置函数来删除元素》方式。
使用append内置函数来删除元素:
这种方式常常用于删除中间元素
// 删除前置元素
a = []int{1, 2, 3,4,5}
a = append(a[:0], a[2:]...) // [3, 4, 5]
// 删除后置元素
a = []int{1, 2, 3,4,5}
a = append(a[:0], a[:2]...) // [1,2]
// 删除中间元素
a = []int{1, 2, 3,4,5}
a = append(a[:2], a[3:]...) // [1,2,4,5]
使用copy内置函数来删除元素:
// 删除前置元素
a = []int{1, 2, 3,4,5}
a = a[:copy(a, a[2:])]// [3, 4, 5]
// 删除后置元素
a = []int{1, 2, 3,4,5}
a = a[:copy(a, a[:2])] // [1,2]
// 删除中间元素
a = []int{1, 2, 3,4,5}
a = a[:2+copy(a[2:], a[3:])] // [1,2,4,5]
遍历Slice
使用range:
var arr = []int{1, 2, 3, 4, 5}
// range只取索引
for index := range arr {
fmt.Println(arr[index])
}
// range取索引和值
for index, value := range arr {
fmt.Println(index, value)
}
不使用range:
var arr = []int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}
映射Map
定义
map是一种无序的基于key-value的数据结构。
-
Go语言中的map是引用类型。
-
map类型的变量默认初始值为nil,必须初始化才能使用。
-
map可以嵌套,如
map[string]map[string]int
map的定义格式如下:
1 | var 映射名称 map[键类型]值类型 |
初始化
直接赋值初始化:
在定义时,直接设置map的键值对来进行初始化。
var m = map[string]int{
"1":1,
"2":2,
"3":3,
"4":4,
}
使用make内置函数:
参考 《函数 - 内置函数 - make》章节
插入键值
插入键值对的格式如下:
1 | 映射名称[键] = 值 |
如果插入的键在Map中已经存在,将使用新值替换旧值。
删除键值对
参考 《函数 - 内置函数 - delete》章节
根据键获取值 / 判断是否存在
根据键获取值有以下两种格式:
1 2 | 值变量 = 映射名称[键] 值变量,是否存在 = 映射名称[键] |
-
第二种方式用于需要判定键是否存在的场景。
-
如果键不存在,返回的值为值类型的默认值。
示例:
var m = map[string]int{
"1": 1,
}
i := m["1"]
fmt.Println(i) // 1
i, ok := m["1"]
fmt.Println(i, ok) // 1 true
遍历Map
遍历Map有如下两种格式:
1 2 3 4 5 6 7 8 | // 只遍历键 for 键 := range 映射名称{ } // 遍历键和值 for 键,值 := range 映射名称{ } |
指针
定义
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。所谓指针,即值为某个变量在内存中的位置
的变量,指针的类型为所引用变量的类型的指针。
指针声明格式如下:
1 | var 指针名称 *所引用变量类型 |
var a int // 整型
var p *int // 整型指针
-
Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。
-
区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
-
指针默认值为nil。
初始化
-
从现有变量取地址
1
指针名称 := &现有变量名称
-
使用new内置函数
参考《函数 - 内置函数 - new》章节
& 与 * 操作符
&
为取地址操作符,&变量名称
表示获取变量在内存中的地址。
*
为取值操作符,*指针名称
或 *地址
表示获取内存中某个地址的值。
& 和 * 可以叠加使用,如
&*b
示例:
var a = 1
fmt.Println(a) // 1
fmt.Println(&a) // 0xc0000aa058
//fmt.Println(*a) // 错误
var ap = &a
fmt.Println(ap) // 0xc0000aa058
fmt.Println(&ap) // 0xc0000ce020
fmt.Println(*ap) // 1
fmt.Println(&*ap) // 0xc0000aa058
结构体
定义
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。
Go语言中通过struct来实现面向对象。
1 2 3 4 | type 类型名 struct{ 属性1 属性类型1 属性2 属性类型2 } |
-
类型名:标识自定义结构体的名称,在同一个包内不能重复。
-
属性名:
-
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
-
结构体中的属性名必须唯一。
-
-
属性类型:表示结构体属性的具体类型。
示例:
type person struct {
name, city string // 相同类型的属性可以写在一起
age int8
}
基于现有类型定义新类型
可以基于现有的数据类型定义新的类型:
1 | type 新类型名称 现有类型名称 |
-
新类型具有和现有类型相同的属性。
-
新类型与现有类型是两个不同的类型,(不能直接使用现有类型的方法,也不能把新类型存入现有类型的数组…);可以通过强转成现有类型后使用。
例如:
type MyInt int
通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
类型别名
类型别名是Go1.9版本添加的功能。定义格式如下:
1 | type 类型别名 = 现有类型名称 |
-
类型别名只是Type的别名,本质上TypeAlias与Type是同一个类型。
-
类型别名与现有类型是相同的类型,(能直接使用现有类型的方法,也能把类型别名的变量存入现有类型的数组…)。
-
类型别名和“基于现有类型定义新类型”不同,前者本质上就是现有类型;后者是基于现有类型创建了一个新的类型。
示例:
type OldType struct{} // 现有类型
type TypeAlias = OldType // 类型别名
type NewType OldType // 基于现有类型创建新类型
func main() {
fmt.Printf("%T\n", OldType{}) // main.OldType
fmt.Printf("%T\n", TypeAlias{}) // main.OldType
fmt.Printf("%T\n", NewType{}) // main.NewType
}
结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
声明实例化
结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型:
1 | var 变量名称 结构体类型 |
- 声明结构体类型的变量时,就已经为变量分配了内存,并完成了结构体属性的默认赋值。
使用new内置函数
参考 《函数 - 内置函数 - new》 章节
结构体初始化
结构体初始化实际上是指对结构体内属性的初始化。
键值对初始化
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
示例:
type person struct {
name, city string // 相同类型的属性可以写在一起
age int8
}
func main() {
var p = person{
name: "chinehe",
city: "WuXi",
}
}
使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值。
这种方式需要注意:
-
必须初始化结构体的所有字段
-
初始值的填充顺序必须与字段在结构体中的声明顺序一致。
-
该方式不能与键值对初始化方式混用。
type person struct {
name, city string // 相同类型的属性可以写在一起
age int8
}
func main() {
var p = person{
"chinehe",
"WuXi",
1,
}
}
构造函数
Go语言的结构体没有构造函数,我们可以自己实现。
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}
调用构造函数:
p9 := newPerson("pprof.cn", "测试", 90)
fmt.Printf("%#v\n", p9)
因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
结构体方法
定义
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
结构体方法的定义格式如下:
1 2 3 | func (接收者变量 接收者类型)方法名称(参数列表)(返回参数){ 方法体 } |
-
接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
-
接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
-
同一个类型,不能定义两个名称相同的方法,即使接收者类型分别为指针类型和非指针类型。
-
同一个类型的方法,推荐保持接收者类型一致(非强制),要么都是指针类型,要么都是非指针类型。
-
同一个类型的方法,推荐保持接收者变量名称一致(非强制)。
-
-
方法名、参数列表、返回参数:具体格式与函数定义相同。
注意事项:非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
指针与非指针类型接收者
- 指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。
- 当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
特别注意:
- 不论接收者是指针还是非指针类型,都可以使用类型对象变量和类型对象指针变量来调用
- 如果不使用变量,而直接在初始化语句后调用结构体方法,则必须与接收者类型一致。
type Person struct {
name string
}
func (p Person) Hello(prefix string) string {
return fmt.Sprintf(prefix, p.name)
}
func (p *Person) Hello2(prefix string) string {
return fmt.Sprintf(prefix, p.name)
}
func main() {
p := &Person{"chinehe"}
p2 := Person{"chinehe"}
fmt.Println(p.Hello("hello,i am %s")) // 正确
fmt.Println(p.Hello2("hello,i am %s")) // 正确
fmt.Println(p2.Hello("hello,i am %s")) // 正确
fmt.Println(p2.Hello2("hello,i am %s")) // 正确
fmt.Println((&Person{"chinehe"}).Hello("hello,i am %s")) // 正确
fmt.Println((&Person{"chinehe"}).Hello2("hello,i am %s")) // 正确
fmt.Println((Person{"chinehe"}).Hello("hello,i am %s")) // 正确
fmt.Println((Person{"chinehe"}).Hello2("hello,i am %s")) // 错误
}
结构体方法的两种调用方式
根据调用者不同,分为两种表现形式:
-
结构体对象直接调用
1
结构体对象.方法名称(参数列表)
-
结构体对象作为参数调用
1
结构体类型或类型指针.方法名称(结构体对象,参数列表)
这种方式下,结构体对象的调用规则应该遵从参数的规则,而非接收者的规则
示例:
type Person struct {
name string
}
func (p Person) Hello(prefix string) string {
return fmt.Sprintf(prefix, p.name)
}
func (p *Person) Hello2(prefix string) string {
return fmt.Sprintf(prefix, p.name)
}
func main() {
p := Person{"chinehe"}
p2 := &Person{"chinehe"}
fmt.Println(Person.Hello(p, "hello,i am %s"))
fmt.Println(Person.Hello2(p2, "hello,i am %s")) // 错误
fmt.Println((*Person).Hello(p, "hello,i am %s")) // 错误
fmt.Println((*Person).Hello(p2, "hello,i am %s"))
}
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体,即没有名字的结构体:
1 2 3 4 | var 变量名称 struct{ 属性1 属性类型1 属性2 属性类型2 } |
示例:
var person = struct {
name, city string // 相同类型的属性可以写在一起
age int8
}{
age: 18,
name: "chinehe",
}
结构体匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
示例:
type person struct {
string
age int
}
注意:
-
匿名字段默认采用类型名作为字段名。
-
匿名字段可以和非匿名字段混用。
-
结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。且不能有非匿名同种类型的字段使用类型名做字段名。
嵌套结构体与继承
嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
格式:
1 2 3 4 | type 结构体名称 struct{ 嵌套结构体属性名称 嵌套结构体 // 其他属性 } |
-
嵌套结构体可以是结构体本身,也可以是结构体指针。
-
嵌套结构体属性和其他的属性并没有什么区别,只是属性类型为结构体。
示例:
type address struct {
pro string
city string
}
type person struct {
name string
age int
firstAddress address // 结构体
secondAddress *address // 结构体指针
}
嵌套匿名结构体
如果在嵌套结构体属性时,不指定属性名称,这就称为嵌套匿名结构体。
格式:
1 2 3 4 | type 结构体名称 struct{ 嵌套结构体 // 其他属性 } |
-
嵌套结构体可以是结构体本身,也可以是结构体指针。
-
该匿名嵌套结构体属性的默认名称为结构体名称。
使用匿名结构体属性的子属性时,可以直接访问。
-
当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
-
外层结构体属性与匿名嵌套结构体属性子属性重复时,默认使用外层结构体属性。需要指定匿名结构体属性名称。
-
多个匿名嵌套结构体属性的子属性重复时,为了避免歧义需要指定具体的匿名内嵌结构体属性,否则会报错。
-
-
初始化时,若要初始化匿名结构体属性的子,则不能直接赋值子属性。
type address struct {
pro string
city string
}
type person struct {
name string
age int
address
}
func main() {
var person = person{
name: "chinehe",
age: 18,
address: address{
pro: "JS",
city: "WX",
},
}
fmt.Println(person.name) // chinehe
fmt.Println(person.age) // 18
fmt.Println(person.pro) // JS
fmt.Println(person.city) // WX
}
结构体的“继承”
Go语言中使用嵌套匿名结构体的方式,也可以实现类似于继承的效果。
-
外层结构体可以直接使用嵌套匿名结构体属性的子属性。
-
外层结构体可以直接使用嵌套匿名结构体的方法。
示例:
type person struct {
name string
age int
}
func (p *person) hello() {
fmt.Printf("hello,i am %v", p.name)
}
type student struct {
person
school string
}
func main() {
s := student{
person: person{
name: "ChineHe",
age: 18,
},
school: "JNU",
}
s.hello()
}
结构体标签Tag
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
1 2 3 | type 结构体名称 struct{ 属性名称 属性类型 `k1:"v1" k2:"v2"` } |
-
结构体标签由一个或多个键值对组成。
-
键与值使用冒号分隔,值用双引号括起来。
-
键值对之间使用一个空格分隔。
注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
接口
定义
在Go语言中接口(interface)是一种类型,一种抽象的类型。interface是一组method的集合,是duck-type programming的一种体现。
接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
定义接口的格式:
1 2 3 4 5 | type 接口名称 interface{ 方法名称1(参数列表1) 返回值列表1 方法名称2(参数列表2) 返回值列表2 ... } |
- 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
- 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
- 接口中没有数据属性。
- 接口中只有方法声明,没有方法实现。
- 接口中可以内嵌别的接口,表示拥有内嵌接口的全部方法。
- 接口中可以嵌套匿名结构体,这是一种特殊情况。
- 接口命名习惯以
er
结尾。
实现接口
常规实现
如果一个结构体的方法集,包含了某个接口定义的全部方法(相同名称、参数列表 (不包括参数名) 以及返回值),就表示它实现了该接口,无需在该结构体上显式声明(这称为Structural Typing)。
type Sayer interface {
say()
}
type dog struct {
}
func (d dog) say() {
fmt.Println("汪汪汪")
}
type cat struct {
}
func (c cat) say() {
fmt.Println("喵喵喵")
}
func Say(sayer Sayer) {
sayer.say()
}
func main() {
Say(cat{}) // 喵喵喵
Say(dog{}) // 汪汪汪
}
一个结构体可以同时实现多个接口,一个接口也可以被多个结构体实现。
值/指针接收者实现接口的区别
- 使用值接收者实现接口之后,不管是结构体对象,还是结构体对象指针,都可以赋值给该接口类型的变量。因为Go语言中有对指针类型变量求值的语法糖
- 使用指针接收者实现接口之后,只有结构体对象指针可以赋值给该接口类型的变量。
结构体实现接口方法时,如果混用了值接收者和指针接收者,那么也只有结构体对象指针可以赋值给该接口类型的变量。
type Sayer interface {
say()
run()
}
type dog struct {
}
func (d dog) say() {
fmt.Println("汪汪")
}
func (d *dog) run() {
fmt.Println("run")
}
func test(s Sayer) {
s.say()
s.run()
}
func main() {
test(&dog{}) // 正确
te
内嵌匿名接口字段
结构体中可以内嵌匿名接口字段,表示实现了该接口。此时结构体可以不去显式实现接口的所有方法,而只需要显式实现结构体需要使用的方法即可。
示例:
type Sayer interface {
say()
run()
}
type dog struct {
Sayer
}
func (d dog) say() {
fmt.Println("汪汪")
}
func test(s Sayer) {
s.say() // dog显式实现了,所以正常打印:汪汪
s.run() // dog没有显式实现:panic: runtime error: invalid memory address or nil pointer dereference
}
func main() {
test(dog{})
}
空接口
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
接口值
一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。
示例:
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
分析:
接口使用
接口类型变量
-
接口类型变量能够存储所有实现了该接口的实例。
// 假设结构体A、B都实现了接口I,接口中有一个方法say() var i I // 接口变量 a := A{} b := B{} i = a i.say() i = b i.say()
- Go语言中所有类型都默认实现了空接口,所以空接口可以作为任何类型数据的容器 。
a := 1 var i interface{} // 空接口变量i i = a
流程控制
条件语句if
Go语言中if条件语句的格式如下:
1 2 3 4 5 6 7 | if 布尔表达式1 { // 布尔表达式1结果为true时执行 } else if 布尔表达式2 { // 布尔表达式1结果为false且布尔表达式2结果为true时执行 }else{ // 前面的布尔表达式都为false时执行 } |
-
else if
子语句可能没有,也可能又多个。 -
else
子语句可能没有,但不能有多个。 -
if
、else if
、else
子语句执行体内部都可以嵌套if条件语句
Go语言中不支持三目运算符!
条件语句switch
switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上直下逐一测试,直到匹配为止。 Golang switch 分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止。
Go语言中switch条件语句的格式如下:
1 2 3 4 5 6 7 8 9 10 11 | switch 外层表达式 { case 内层表达式1: // 内层执行体1 case 内层表达式2: // 内层执行体2 fallthrough case 内层表达式3,内层表达式4: // 内层执行体3 default: // 默认执行 } |
-
外层表达式和内层表达式都可以是任意类型,但需要保证两者的结果是同一种类型。
-
当外层表达式和内层表达式的结果一致时,执行对应的结构体。
-
同一个case后面可以有多个内层表达式,用,分隔。这表示当后面的任意表达式满足时,执行对应的内层执行体。
上面的内层表达式3和内层表达式4,他们任意一个满足时,都会执行内层执行体3
-
外层表达式可以忽略(等于
switch true{...}
),此时内层表达式只能为布尔表达式。当内层布尔表达式为true时执行对应的执行体。 -
与Java不同,Go语言中的某个内层执行体执行完成后,默认不执行下一个内层执行体,如果需要执行下一个执行体,需要添加
fallthrough
关键字。上面的内层表达式1满足后,只会执行内层执行体1.
内层表达式2满足后,会执行内层执行体2和内层执行体3.
-
default
下的默认执行体,在上面的所有case条件都不成立的时候执行。 -
default
可以省略。
条件语句type-switch
switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
typw-switch语句算是switch语句的一个变种
语法格式如下:
1 2 3 4 5 6 7 8 | switch 接口变量.(type){ case 类型1: // 执行体1 case 类型2: // 执行体2 default: // 默认执行体 } |
条件语句select
select 是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。 select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
Go 编程语言中 select 语句的语法如下:
1 2 3 4 5 6 7 8 9 | select { case communication clause : // statement(s) case communication clause : // statement(s) /* 你可以定义任意数量的 case */ default : /* 可选 */ statement(s) } |
-
每个case都必须是一个通信操作.
-
所有channel表达式都会被求值,所有被发送的表达式都会被求值.
-
如果任意某个通信操作可以进行,它就执行;其他被忽略。
-
如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
-
当没有任何通信操作可以执行时,如果有default子句,则执行该语句。如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。
在一个select语句中,Go会按顺序从头到尾评估每一个发送和接收的语句。
如果其中的任意一个语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。 如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况: ①如果给出了default语句,那么就会执行default的流程,同时程序的执行会从select语句后的语句中恢复。 ②如果没有default语句,那么select语句将被阻塞,直到至少有一个case可以进行下去。
循环语句for
for循环是一个循环控制结构,可以执行指定次数的循环。
Go 语言中for循环的语法格式如下:
1 2 3 | for 初始化子句;条件子句;后置子句{ // 执行体 } |
执行流程:
执行初始化子句
判断条件子句结果,如果为true继续执行,否则退出循环。
执行执行体
执行后置子句
自动跳转到第2步。
注意事项:
-
初始化子句、条件子句、后置子句都可以为空。当初始化子句和后置子句都为空时,可以省略两个分号。
-
初始化子句为空表示不执行初始化操作。
-
后置子句为空表示不执行后置操作。
-
条件子句为空默认为true。
-
-
for循环可以嵌套使用。
循环语句for - range
Golang range类似迭代器操作,可以对 slice、map、数组、字符串等进行迭代循环。
-
map:
参考《语言基础 - 映射map - 遍历map》 章节。
-
slice
参考《语言基础 - 切片Slice - 遍历Slice》 章节。
-
数组
参考《语言基础 - 数组 - 遍历数组》 章节。
-
字符串
参考《语言基础 - 字符串 - 遍历字符串》 章节。
跳转关键字break
break关键字用于跳出当前循环语句或者switch语句。
循环中使用
break关键字在循环体中使用,表示不执行后续操作,直接退出当前循环体。
1 2 3 4 5 | 循环体{ // 前面的执行语句 break // 后面的执行语句 } |
-
break关键字执行后,关键字后面的执行语句不会被执行。
-
break关键字执行后,退出当前循环。
-
如果break关键字在多重循环内,将退出的是break关键字所在的最内层循环。
示例:
func main() {
a := []int{1, 2, 3, 4, 5}
for i := 0; i < 5; i++ {
for j, v := range a {
fmt.Print(v)
if j == i {
break
}
}
fmt.Println()
}
}
// 结果:
1
12
123
1234
12345
break关键字还可以搭配标签使用,表示退出标签后面紧跟的循环。
语法格式:
1 2 3 4 5 6 7 8 9 10 | 标签名字: 循环1 { // 前置语句1 循环2 { // 前置语句2 break 标签名字 // 后置语句2 } // 后置语句1 } |
-
标签后面应该紧跟循环,不能再有其他语句。
-
break关键字执行后,关键字后面的执行语句不会被执行。
如后置语句2、后置语句1
-
break关键字执行后,退出标签后面紧跟的循环。
switch中使用
break关键字在switch中使用,表示不执行后续操作,直接退出当前switch体。
1 2 3 4 5 6 7 8 | switch语句{ case子句{ // 前面的执行语句 break // 后面的执行语句 } } |
-
break关键字执行后,关键字后面的执行语句不会被执行。
-
break关键字执行后,退出当前switch。
-
如果break关键字在多重switch内,将退出的是break关键字所在的最内层switch。
示例:
func main() {
var a = 1
var b = 2
switch a {
case 1:
fmt.Println(11)
switch b {
case 2:
fmt.Println(21)
if b > a {
break
}
fmt.Println(22)
}
fmt.Println(12)
}
}
// 结果
11
21
12
break关键字还可以搭配标签使用,表示退出标签后面紧跟的switch。
示例:
func main() {
var a = 1
var b = 2
label:
switch a {
case 1:
fmt.Println(11)
switch b {
case 2:
fmt.Println(21)
if b > a {
break label
}
fmt.Println(22)
}
fmt.Println(12)
}
}
// 结果
11
21
-
标签后面应该紧跟switch,不能再有其他语句。
-
break关键字执行后,关键字后面的执行语句不会被执行。
-
break关键字执行后,退出标签后面紧跟的switch。
跳转关键字continue
continue关键字用于停止当前循环体的执行,转而执行下一次循环。
语法格式:
1 2 3 4 5 | for 初始化子句;条件子句;后置子句{ // 执行体1 continue // 执行体2 } |
-
continue关键字只能在循环中使用。
-
continue关键字执行后,关键字后面的执行体不会执行。
- 执行体2 不会被执行。
-
如果continue关键字在多重循环体内,将作用在关键字所在的最内层循环。
示例:
func main() {
a := []int{1, 2, 3}
for i := range a {
fmt.Print(i, "->")
if i == 1 {
fmt.Println("XXX")
continue
}
fmt.Println(a[i])
}
}
// 结果
0->1
1->XXX
2->3
搭配标签使用
continue关键字还可以搭配标签使用,表示作用于标签后面紧跟的循环。
语法格式:
1 2 3 4 5 6 7 8 9 10 | 标签名字: 循环1 { // 前置语句1 循环2 { // 前置语句2 continue 标签名字 // 后置语句2 } // 后置语句1 } |
-
标签后面应该紧跟循环,不能再有其他语句。
-
continue关键字执行后,关键字后面的执行语句不会被执行。
如后置语句2、后置语句1
-
break关键字执行后,继续执行标签后面紧跟的循环。
跳转关键字goto
goto必须搭配标签使用,表示跳转到标签所在的位置,并开始执行标签后的语句。
func main() {
a := []int{1, 2, 3}
count := 0
tag:
for i := 0; i < len(a); i++ {
fmt.Print(a[i])
}
fmt.Println()
count++
if count < 3 {
goto tag
}
}
// 结果
123
123
123
函数
定义
声明格式
Go 语言中声明函数的格式如下:
1 2 3 | func 函数名称(参数列表)(返回值列表){ // 函数 } |
-
函数名称
-
Go语言中不支持函数重载,同一个包中函数名称不能重复,即使参数列表不一样。
-
同变量一样,函数名称的首字母的大小写会决定能否在包外访问。
-
-
参数列表
-
参数列表可以为空,也可以为多个。
- 当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。
func test(x,y int,s string){}
- 当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。
- 支持不定参数
func test(s string, args ...int) {}
- 参数列表中的参数名称可以忽略(必须同时忽略全部参数的名称)。
func maxInt(int, int) int { return math.MaxInt }
-
-
返回值列表
- 函数可以返回任意数量的返回值。如果返回值为0或1个,括号可以省略。
- 返回值列表中,可以对返回值进行命名。命名后返回值变量将被初始化为对应类型的零值。
- 返回值要么全都命名,要么全都不命名,不能混用。
- 当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。
函数也是一种类型
函数也是一种类型,一个函数可以赋值给变量,也可以作为函数参数传递。还可以定义函数类型!
赋值给变量:
func maxInt(x, y int) int {
if x > y {
return x
}
return y
}
func main() {
max := maxInt
x, y := 1, 2
fmt.Println(max(x, y))
}
作为参数传递:
func maxInt(x, y int) int {
if x > y {
return x
}
return y
}
func process(f func(x, y int) int, x, y int) int {
return f(x, y)
}
func main() {
x, y := 1, 2
fmt.Println(process(maxInt, x, y))
}
定义函数类型:
func maxInt(x, y int) int {
if x > y {
return x
}
return y
}
type IntOperation func(x, y int) int
func process(f IntOperation, x, y int) int {
return f(x, y)
}
func main() {
x, y := 1, 2
fmt.Println(process(maxInt, x, y))
}
值传递与引用传递
形参和实参:
-
形参
函数定义时的参数,可称为函数的形参。形参就像定义在函数体内的局部变量。
-
实参
调用函数,传递过来的变量就是函数的实参.
函数可以通过两种方式来传递参数:
-
值传递
指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
-
引用传递
是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
map、slice、chan、指针、interface默认以引用的方式传递。
指针与非指针参数
与结构体方法的接收者调用不同,函数参数(包括结构体方法的参数)的指针与非指针类型参数不能混用。
type Person struct {
name string
}
func Hello(format string, p Person) string {
return fmt.Sprintf(format, p.name)
}
func Hello2(format string, p *Person) string {
return fmt.Sprintf(format, p.name)
}
func main() {
p1 := Person{"chinehe"}
p2 := &Person{"chinehe"}
fmt.Println(Hello("hello,i am %s", p1))
fmt.Println(Hello("hello,i am %s", p2)) // 错误
fmt.Println(Hello2("hello,i am %s", p1)) // 错误
fmt.Println(Hello2("hello,i am %s", p2))
}
不定参数
Go语言函数支持使用不定参数(参数个数不确定)。
如果函数参数列表中,某个参数可以会传入0-n个值,那么这个参数就称为不定参数。
声明示例:
func test(x int,args ...string){}
示例中的args参数就是一个不定参数,调用该函数时,可以传入0-n个值:
func test(x int, args ...string) {
fmt.Println(args)
}
func main() {
test(1) // []
test(1, "A") // [A]
test(1, "A", "B", "C") // [A B C]
}
注意事项:
-
一个函数中,最多只能设置一个不定参数。
-
不定参数必须在参数列表中的最后位置。
-
调用时,不定参数传值的个数为0-n,传值的类型为定义的不定参数的类型。
-
Golang 可变参数本质上就是 slice。
func test(args ...string) { fmt.Printf("%T", args) } func main() { a := []string{"A", "B", "C"} test(a...) // []string test() // []string }
-
在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可。
a := []string{"A", "B", "C"} test(1, a...)
函数返回
使用return关键字进行函数的返回操作:
1 2 3 4 | func test()(x,y int){ // 某些操作 return x,y } |
- return 关键字后面返回的值类型必须与返回值列表相同。
没有参数的 return 语句被称作“裸”返回。这只能用于以下两种场景:
-
函数没有返回值
-
函数返回值都命名了
这种场景返回各个返回变量的当前值。
命名返回参数可被同名局部变量遮蔽,此时需要显式返回。
func add(x, y int) (z int) {
{ // 不能在一个级别,引发 "z redeclared in this block" 错误。
var z = x + y
// return // Error: z is shadowed during return
return z // 必须显式返回。
}
}
匿名函数
匿名函数是指不需要定义函数名的一种函数实现方式。1958年LISP首先采用匿名函数。Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明:
func main() {
getSqrt := func(a float64) float64 {
return math.Sqrt(a)
}
fmt.Println(getSqrt(4))
}
闭包
闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。
官方定义:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
示例:
func a() func() {
i := 0
b := func() {
i++
fmt.Println(i)
}
return b
}
func main() {
c := a()
c() // 1
c() // 2
c() // 3
c() // 4
}
在该例子中,变量i就是“被绑定的环境变量”,b就是“绑定变量的函数”。
延迟调用defer
defer 关键字用于注册延迟调用,延迟调用直到return前才会被执行(常用来做资源清理工作)。
示例:
func main() {
a := 1
fmt.Printf("%s-%d\n", "start", a)
a++
defer func(a int) {
fmt.Printf("%s-%d\n", "defer", a)
}(a)
a++
fmt.Printf("%s-%d\n", "end", a)
}
// 输出
start-1
end-3
defer-2
defer 语句将在函数返回前执行
- 延迟调用直到return前才会被执行(常用来做资源清理工作)。
- 多个defer语句,按照先进后出的方式执行
- defer语句中的变量,在defer声明时就决定了。
内置函数
Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持
close
func close(c chan<- Type) |
close 内置函数用于关闭通道。
-
该通道必须是双向的或仅发送的。它只能由发送方执行,而不能由接收方执行,并且具有在收到最后发送的值后关闭通道的效果。
-
从关闭的通道 c 接收到最后一个值后,来自 c 的任何接收都将成功而不会阻塞,并返回通道元素的零值。
-
对于关闭的通道,形式 x, ok := <-c 也会将 ok 设置为 false。
delete
1 | func delete(m map[Type]Type1, key Type) |
delete 内置函数用于从映射中删除具有指定键 (m[key]) 的元素。
- 如果 m 为 nil 或者不存在这样的元素,则删除是无操作。
panic
1 | func panic(v any) |
内置函数panic会停止当前goroutine的正常执行。当函数 F 调用恐慌时,F 的正常执行立即停止。任何被 F 推迟执行的函数都会以通常的方式运行,然后 F 返回到它的调用者。对于调用者 G 来说,F 的调用就像调用恐慌一样,终止 G 的执行并运行任何延迟的函数。这将继续下去,直到执行中的 goroutine 中的所有函数都以相反的顺序停止。此时,程序将以非零退出代码终止。这个终止序列称为恐慌,可以通过内置函数恢复来控制。
recover
1 | func recover() any |
recover 内置函数允许程序管理恐慌 goroutine 的行为。在延迟函数(但不是它调用的任何函数)内执行恢复调用可以通过恢复正常执行来停止恐慌序列,并检索传递给恐慌调用的错误值。如果在延迟函数之外调用恢复,它不会停止恐慌序列。在这种情况下,或者当 goroutine 没有恐慌时,或者如果提供给恐慌的参数为 nil,recover 将返回 nil。因此,recover 的返回值报告 goroutine 是否处于恐慌状态。
new
1 | func new(Type) *Type |
new内置函数用于分配内存。第一个参数是类型,而不是值,返回的值是指向该类型新分配的零值的指针。
print / println
1 2 | func print(args ...Type) func println(args ...Type) |
print 内置函数以特定于实现的方式格式化其参数并将结果写入标准错误。print对于引导和调试很有用;不能保证它会保留在该语言中。
println 内置函数以特定于实现的方式格式化其参数并将结果写入标准错误。参数之间始终添加空格并附加换行符。println对于引导和调试很有用;不能保证它会保留在该语言中。
cap
1 | func cap(v Type) int |
cap 内置函数根据参数 v 的类型返回 v 的容量:
-
数组:返回数组的长度(与 len(v) 相同)。
-
数组指针:返回指针所指向的数组的长度(与 len(v) 相同)。
-
Slice:返回重新切片时切片所能达到的最大长度;如果切片为nil,返回0。
-
Channel:返回通道缓冲区容量,以元素为单位;如果通道为nil,返回0。
len
1 | func len(v Type) int |
len 内置函数根据参数 v 的类型返回 v 的长度:
-
数组:返回数组的长度。
-
数组指针:返回指针所指向的数组的长度。
-
切片Slice:返回切片中元素的个数,如果切片为nil,返回0。
-
映射Map:返回映射中元素的个数,如果映射为nil,返回0。
-
字符串:返回字符串中字节的个数。
-
通道Channel:返回通道缓冲区中排队(未读)的元素数量;如果通道为nil,返回0。
append
1 | func append(slice []Type, elems ...Type) []Type |
append 内置函数将元素追加到切片的末尾。如果目的地有足够的容量,则会对目的地进行重新切片以容纳新元素。如果没有,将分配一个新的底层数组。追加返回更新后的切片。因此,有必要存储追加的结果,通常存储在保存切片本身的变量中。作为一种特殊情况,可以合法的将字符串附加到字节切片
使用方式:
-
追加单个元素
1
slice = append(slice,1)
-
追加多个元素
1
slice = append(slice,1,2,3)
-
追加切片/数组片段
1
slice = append(slice,arr[1:3]...)
-
追加字符串片段
1
slice = append(slice,"hello"[1:3]...)
copy
1 | func copy(dst, src []Type) int |
copy 内置函数将源切片中的元素复制到目标切片中。 (作为一种特殊情况,它还将字节从字符串复制到字节切片。)源和目标可能重叠。 Copy 返回复制的元素数量,该数量将是 len(src) 和 len(dst) 中的最小值。
-
dst 参数用于指定目标切片,其类型只能为切片,不能为数组/字符串。
-
src 参数用于指定源切片,其类型只能为切片或字符串。
-
dst 与 src 的类型必须相同。当src为字符串时,dst必须为
[]byte
。
make
1 | func make(t Type, size ...IntegerType) Type |
make 内置函数分配并初始化类型为 slice、map 或 chan的对象。与 new 一样,第一个参数是类型,而不是值。与 new 不同,make 的返回类型与其参数的类型相同,而不是指向它的指针。结果的规格取决于类型。
-
slice
参数size用于指定长度,切片的容量等于其长度。可以提供第二个整数参数来指定不同的容量(容量必须不小于长度)。
1
make([]数据类型,切片长度,切片容量)
例如,
make([]int, 0, 10)
分配大小为 10 的基础数组,并返回由该基础数组支持的长度为 0、容量为 10 的切片。 -
Map
为空映射分配足够的空间来容纳指定数量的元素。该大小可以省略,在这种情况下会分配较小的起始大小。
1
make(map[键类型]值类型,初始容量)
-
Channel
通道的缓冲区使用指定的缓冲区容量进行初始化。如果为零或省略大小,则通道是无缓冲的。
1
make(chan 数据类型,缓冲区容量)
complex
1 | func complex(r, i FloatType) ComplexType |
complex 内置函数根据两个浮点值构造一个复数值。实部和虚部必须具有相同的大小,可以是 float32 或 float64(或可分配给它们),并且返回值将是相应的复数类型(float32 为complex64,float64 为complex128)。
imag
1 | func imag(c ComplexType) FloatType |
imag 内置函数返回复数 c 的虚部。返回值将是与 c 类型对应的浮点类型。
real
1 | func real(c ComplexType) FloatType |
real 内置函数返回复数 c 的实部。返回值将是与 c 类型对应的浮点类型。
main函数是Go语言程序的默认入口函数(主函数),main函数的定义如下:
1 2 3 | func main(){ // 代码逻辑 } |
main函数具有以下特性:
-
定义main函数不能有参数和返回值
-
main函数只能用于main包中,且只能定义一个。
init函数
定义:
go语言中init
函数用于包(package)
的初始化,该函数是go语言的一个重要特性。init函数的定义方式如下:
1 2 3 | func init(){ // 代码逻辑 } |
特性:
init函数是用于程序执行前做包初始化的函数,比如可以用来初始化包内的变量。init函数具有以下等特性:
-
定义init函数不能有参数和返回值。
-
每个包里面可以包含多个init函数。
-
包里的每个源文件也可以定义多个init函数。
-
同一个包中多个init函数的执行顺序go语言没有明确定义(说明)。
-
不同包的init函数按照包的导入依赖关系决定该初始化函数的执行顺序。
-
init函数在main函数执行之前,自动被调用,不能被其他函数调用。
执行顺序:
-
同文件中定义的多个init函数,根据函数声明的位置顺序从上到下执行。
-
同包中不同文件定义的多个init函数,根据源文件名从小到大的顺序执行。
-
不同包中的init函数,如果不互相依赖的话,根据main包中import的顺序执行。
-
不同包中的init函数,如果存在依赖的话,则先调用最早被依赖的
package
中的init()
,最后调用main
函数。
如果
init
函数中使用了println()
或者print()
你会发现在执行过程中这两个不会按照你想象中的顺序执行。这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用。
异常处理
Go语言中异常分为两类:error、panic
panic
什么是panic
panic是一种错误类型,一般是是指导致关键流程出现不可修复性错误的错误。
例如下面的代码中,出现了除以0的情况,这就会导致panic错误:
func test() {
for i := 0; i < 3; i++ {
s := 1 / i
fmt.Println(s)
}
}
func main() {
test()
}
// 错误信息
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.test()
....
向已关闭的通道发送数据也会引发panic
自定义panic
除了除以0、向已关闭通道发送数据等场景会导致发生panic错误外,还支持使用内置函数func panic(v any)
自定义panic错误。
func div(x, y int) int {
if y == 0 {
panic("The divisor y cannot be 0")
}
return x / y
}
func main() {
fmt.Println(div(1, 0))
}
// 错误信息
panic: The divisor y cannot be 0
goroutine 1 [running]:
main.div(...)
panic发生后
此处说的时panic发生后,不进行异常处理的默认流程
如果函数F中某个代码发生了panic错误:
-
停止执行含函数F内的其他代码。
-
如果panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行。
此处要求defer函数在panic之前定义,在panic之后定义的defer不会执行。
-
返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行。
此处要求defer函数在panic之前定义,在panic之后定义的defer不会执行。
-
直到goroutine整个退出,并报告错误。
recover捕获panic
Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
内置函数
func recover() any
用来捕获panic,控制一个goroutine的panic行为,从而影响应用的行为。
func div(x, y int) int {
if y == 0 {
panic("The divisor y cannot be 0")
}
return x / y
}
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("got panic:%v\n", err)
}
}()
fmt.Println(div(1, 0))
}
// 错误信息
got panic:The divisor y cannot be 0
内置函数func recover() any
会终止一个goroutine的panicking过程,从而恢复正常代码的执行
注意事项:
-
recover函数必须在defer函数中使用,且所在的defer函数必须在panic之前定义。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
-
recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
-
多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
-
延迟调用中引发的panic,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
-
捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
-
捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
func test() {
defer func() {
fmt.Println(recover()) //有效
}()
defer recover() //无效!
defer fmt.Println(recover()) //无效!
defer func() {
func() {
println("defer inner")
recover() //无效!
}()
}()
panic("test panic")
}
func main() {
test()
}
func except() {
fmt.Println(recover()) // 有效
}
func test() {
defer except()
panic("test panic")
}
func main() {
test()
}
error
什么是error
error是Go语言中的另一种错误类型,一般来说是指一些可控制的错误。
除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。
例如io包下的func ReadAll(r Reader) ([]byte, error)
函数就定义了一个error返回值,用来表示函数读取过程中出现的错误信息。
自定义error
Go语言中所有error都实现了Error接口:
type error interface {
Error() string
}
自定义error时,只需要实现Error接口即可:
type DivZeroErr struct {
}
func (d DivZeroErr) Error() string {
return "The divisor y cannot be 0"
}
处理自定义结构体并实现Error接口来自定义Error外,还可以使用errors.New()
和fmt.Errorf()
函数来自定义错误。
var DivZeroErr = errors.New("The divisor y cannot be 0")
var DivZeroErr = fmt.Errorf("The divisor y cannot be 0")
error处理
error可以看作一个类型为Error的普通变量,它的定义和返回不会影响程序的执行流程。
error处理中一般通过判定error返回值是否为空来判定调用过程中是否发生错误。
var DivZeroErr = errors.New("the divisor y cannot be 0")
func div(x, y int) (int, error) {
if y == 0 {
return 0, DivZeroErr
}
return x / y, nil
}
func main() {
if r, err := div(1, 0); err != nil {
fmt.Printf("got error:%v\n", err)
} else {
fmt.Printf("success:%v", r)
}
}
参考文档
地鼠文档:Go语言中文文档
Go语言官网:Documentation - The Go Programming Language
C语言中文网:Go语言入门教程,Golang入门教程(非常详细)
标签:教程,函数,int,fmt,Golang,备忘录,func,类型,Go From: https://blog.csdn.net/ChineHe/article/details/139765874