开篇综述
语言介绍
Go 语言由谷歌(Google)公司于 2009 年正式对外发布,设计 Go 语言的初衷都是为了满足 Google 公司的需求。主要目标是“兼具 Python 等动态语言的开发速度和 C/C++等编译型语言的性能与安全性”,旨在不损失应用程序性能的情况下降低代码的复杂性,具有“部署简单、并发性好、执行性能好”等优势。最主要还是为了并发而生,并发是基于
goroutine的,goroutine类似于线程,但并非线程,可以将goroutine理解为一种轻量级线程。Go 语言运行时会参与调度goroutine,并将goroutine合理地分配到每个 CPU 中,最大限度地使用 CPU 性能。
作者介绍
从左到右分别是 :
- 罗伯特·格瑞史莫(Robert Griesemer): 曾为谷歌的 V8 JavaScript 引擎和 Chubby 开发代码
- 罗勃·派克(Rob Pike): 罗布·派克是 Unix 的先驱,是贝尔实验室最早和 Ken Thompson 以及 Dennis M. Ritche 开发 Unix 的猛人,UTF-8 的设计人。还是 1980 年奥运会射箭的银牌得主。
- 汤普逊(Ken Thompson): Ken Thompson 图灵奖得主,C 语言前身 B 语言的作者,Unix 的发明人之一, 操作系统 Plan 9 的主要作者。 共同开发了 UTF-8。
Go 语言特点
Go 语言,作为编程语言的后生,站在巨人的肩膀上,吸收了其他一些编程语言的特点。下面简单解释一下:
-
拥有编译器很方便
自带编译器可以检测出你犯的所有低级错误,如:变量名拼错,不要小看这种问题,没有编译器情况下,很可能浪费掉很长时间去排查,并且非常不容易发现,而且
Go语言也是跨平台编译的,你可以在 Mac 电脑上,编译出linux或者windows的目标程序。 -
开发速度
Go 是一个非常简单的语言,上手容易,无论你是小白还是老鸟,都会比其他语言 C/C++ 和 Java 等语言要容易很多,这点在做项目中体现得尤其明显。很多人可能会说那 python 呢,php 呢?从语言上说 python 和 php 他们没有编译检查,同样也会像上面说的出一些小的低级错误,或者运行时错误,这都给 php 和 python 带来了隐患,所以 Go 语言虽然牺牲了一点点代码书写的时间,但是从项目运行安全角度来说,已经非常值得了。
-
天生高并发
Go 语言就是为高并发而生的。当你需要使用并发场景,如果你有其他语言基础,第一反应是用到锁,但是 Go 语言提供了更加方便的方式协程+通道,在 Go 语言中代码不用修改就能直接多协程运行,只要在调用的时候加入 go 关键字,就可以了,非常方便。这与其他语言截然不同,你要考虑哪里开辟新的线程,哪里是代码执行逻辑。
-
部署简单
Go 语言最终执行就是一个二进制文件,包括了它所依赖的程序包,这让开发者不用考虑部署环境的问题,例如,如果你是 java 程序,要考虑执行你的程序,对方机器是否安装了 java 的运行环境,其他语言同理,如果对方机器没有安装,无法运行你的程序,Go 语言可以在 Mac 和 Linux 上交叉编译你的代码,将其拷贝到远程服务器上,然后就可以任其运行了。
Go 语言能做什么
Go 的优点:实现快 + 资源占用低 + 任意环境随便跑,综合考虑在很多场景十分好用 。
- Go 适合造轮子,哪个库不好用就自己造。
- Go 适合写工具,比如 hugo 、hub,还有国人写的 linux 下的百度 pan client 都是 go 实现的。
- Go 适合实现 C/C++ 一部分业务,Java 的大部分业务。
- Go 适合做最外层的胶水,通过 RPC/REST/CGO 粘合不同语言的模块,而在这个胶水层还可以实现各种业务逻辑,又不用像 shell/python/node 有诸多顾虑和局限。
- Go 提供了协程、指针、 unsafe, cgo 加上 C/C++ 兼容的内存布局和跨平台的汇编,有了这些你能做很多事情了。
所以问题不是它适合做什么,是你有能力用它做什么。
哪些公司在使用 Go
这个问题之前 Go 的官方 wiki 里面有一个维护列表,大家可以点击下面地址看到:
在 stackshare 里面可能更加直接的看到一些使用 Go 的国外企业列表:
- Uber
- Shopify
- Slack
- …
国内使用 Go 的企业也非常的多:
- 腾讯
- 阿里巴巴
- 百度
- 字节跳动
- BiliBili
- 小米
- 163Cloud
- PingCAP
- 积梦智能
开发环境搭建
“工欲善其事,必先利其器”,做好一件事,准备工作非常重要。在开始学习 Go 技术之前,先介绍如何搭建 Go 开发环境是非常重要的一件事。
安装和配置 SDK
本文基于go version go1.18
下载
Go 源码包官网下载地址为:golang.org/dl/(因为你知道的原…
Go 官方镜像站(推荐):golang.google.cn/dl/
Go 语言支持以下系统,根据自己的系统,自行选择安装即可:
- Linux
- FreeBSD
- Mac OS X(也称为 Darwin)
- Windows
操作系统
包名
Windows
go1.18.windows-amd64.msi
Linux
go1.18.linux-amd64.tar.gz
Mac
go1.18.darwin-amd64.pkg
FreeBSD
go1.18.freebsd-amd64.tar.gz
Win & Mac
对于 Windows 平台和 Mac 平台,只要下载对应的安装包,然后双击,一路 next 点击,就可以安装完成
在 win 中用 cmd,执行下面的命令,查看是否是你安装的版本
PS C:> go version
go version go1.18 windows/amd64
在 mac 中 Terminal 中执行
go version
go version go1.18 darwin/arm64
如果你在 mac 平台上,也可以使用 brew install go,这样的方式安装,只要这么一条命令就可以搞定。
Linux
使用go mod 管理项目,就不需要非得把项目放到GOPATH指定目录下,你可以在你磁盘的任何位置新建一个项目,包含go.mod文件的目录也被称为模块根,也就是说,go.mod 文件的出现定义了它所在的目录为一个模块。
GoLand 选择你电脑上安装好的 GO 编程环境 创建一个 Demo 工程,点击如图所示运行按钮,若能成功执行,则环境搭建完毕!
第一个 Go 程序
Hello Go
在控制台输出“Hello Go!”非常简单,仅需要几行代码就可以搞定,如下所示:
复制成功
package main // 声明 main 包
import "fmt" // 导入 fmt 包,打印字符串时需要用到
func main(){ // 声明 main 主函数
fmt.Println("Hello, Go!") // 打印 Hello Go!
}
大家也许不明白这些代码的含义,没关系,下面就来依次介绍。
注释
作用: 在代码中加一些说明和解释,方便自己或其他程序员程序员阅读代码,能够大大增强程序的可读性。
两种格式:
- 单行注释:通常放在一行代码的上方,或者一条语句的末尾,==对该行代码说明==
fmt.Println("Hello, Go!") // 右边的所有东西当做说明,而不是真正要执行的程序,起辅助说明作用 S
- 多行注释:通常放在一段代码的上方,==对该段代码做整体说明==
func main() {
/*
以下两行代码都是将信息打印在屏幕上
*/
fmt.Println("以下是我的微信公众号:")
fmt.Printf("《面向加薪学习》")
}
-
特别说明:
- 编译器在编译代码时,会忽略注释的内容。如果你写的代码不想让计算机执行,那么也可以加上注释。加了注释后代码不会被编译执行,这就是对已有的代码进行注销。
- 以后写程序要多加注释,这是我们程序猿的专业和职业道德,不加注释就是流氓。
package(创建包)
Go 语言以“包”作为管理单位,每个 Go 源文件必须先声明它所属的包,所以我们会看到每个 Go 源文件的开头都是一个 package 声明,格式如下:
package name // 其中 package 是声明包名的关键字, name 为包的名字。
Go 语言的包与文件夹是一一对应的,它具有以下几点特性:
- 一个目录下的同级文件属于同一个包。
- 包名可以与其目录名不同。
- main 包是 Go 语言程序的入口包,一个 Go 语言程序必须有且仅有一个 main 包。如果一个程序没有 main 包,那么编译时将会出错,无法生成可执行文件。
import(导入包)
在包声明之后,是 import 语句,用于导入程序中所依赖的包,导入的包名使用双引号""包围,格式如下:
import "name" // 其中 import 是导入包的关键字,name 为所导入包的名字。
代码第 4 行导入了 fmt 包,这行代码会告诉 Go 编译器,我们需要用到 fmt 包中的函数或者变量等,fmt 包是 Go 语言标准库为我们提供的,用于格式化输入输出的内容,类似的还有 os 包、io 包等。
另外有一点需要注意,导入的包中不能含有代码中没有使用到的包,否则 Go 编译器会报编译错误,例如 imported and not used: "xxx",”xxx” 表示包名。
也可以使用一个 import 关键字导入多个包,此时需要用括号( )将包的名字包围起来,并且每个包名占用一行,也就是写成下面的样子:
import(
"name1"
"name2"
)
main 函数
第 5 行代码创建了一个 main 函数,它是 Go 语言程序的入口函数,也即程序启动后运行的第一个函数。main 函数只能声明在 main 包中,不能声明在其他包中,并且,一个 main 包中也必须有且仅有一个 main 函数。
main 函数是自定义函数的一种,在 Go 语言中,所有函数都以关键字 func 开头的,定义格式如下所示:
func 函数名 (参数列表) (返回值列表){
函数体
}
注意:Go 语言函数的左大括号{必须和函数名称在同一行,否则会报错。
打印输出
代码的第 6 行fmt.Println("Hello Go!")中,Println 是 fmt 包中的一个函数,它用来格式化输出数据,比如字符串、整数、小数等。这里我们使用 Println 函数来打印字符串。注意,Println 函数打印完成后会自动换行,ln 是 line 的缩写。另外,代码fmt.Println("Hello Go!")的结尾,不需要使用;来作为结束符,Go 编译器会自动帮我们添加,当然,在这里加上;也是可以的。
Go变量与常量
变量
作用: 给一段指定的内存空间起名,方便操作这段内存。
声明变量的一般形式是使用 var 关键字。
方法一:声明一个变量, 默认的值是 0
package main
import "fmt"
func main(){
// 方法一:声明一个变量, 默认的值是0
var a int
fmt.Println("a = ", a)
fmt.Printf("a的类型是: %T\n", a)
}
方法二:声明一个变量, 并初始化一个值
package main
import "fmt"
func main(){
// 方法二:声明一个变量, 初始化一个值
var b int = 100
fmt.Printf("b = %d, type of b = %T\n", b, b)
var bb string = "从0到Go语言微服务架构师"
fmt.Printf("bb = %s, bb的类型是: %T\n", bb, bb)
}
方法三:在初始化的时候,可以省去数据类型,通过值去自动匹配当前变量的数据类型
package main
import "fmt"
func main(){
// 方法三:在初始化的时候,可以省去数据类型,通过值去自动匹配当前变量的数据类型
var c = 100
fmt.Printf("c = %d, type of c = %T\n", c, c)
var cc = "Go语言微服务架构师核心22讲"
fmt.Printf("cc = %s, cc的类型是: %T\n", cc, cc)
}
短声明,只能在函数内
package main
import "fmt"
func main(){
// 方法四:(常用的方法) 省去var关键字,使用:=,既推导数据类型又赋值
// 注: 短声明是在函数或方法内部使用, 不支持全局变量声明!!!!
e := 100
fmt.Printf("e = %d, e的类型是: %T\n", e, e)
f := "Go语言极简一本通"
fmt.Printf("f = %s, f的类型是: %T\n", f, f)
}
多变量声明
package main
import "fmt"
func main(){
// 声明多个变量
var xx, yy int = 100, 200
var kk, wx = 300, "write_code_666(欢喜哥)"
var (
nn int = 100
mm bool = true
)
}
常量
常量(constant)表示固定的值。在计算机程序运行时,不会被程序修改的。
定义一个常量,使用 const 关键字。常量定义的时候就要赋值。
package main
import "fmt"
func main(){
// 常量(只读属性)
const length int = 10
// length = 100 // 常量是不允许被修改的
fmt.Println("length = ", length)
}
使用 const 来定义枚举类型
package main
import "fmt"
// const来定义枚举类型
const (
BEIJING = 0
SHANGHAI = 1
SHENZHEN = 2
)
func main() {
fmt.Println("BEIJING = ", BEIJING) // 0
fmt.Println("SHANGHAI = ", SHANGHAI) // 1
fmt.Println("SHENZHEN = ", SHENZHEN) // 2
}
iota
iota 是 Go 语言的常量计数器,只能在常量的表达式中使用。 iota 在 const 关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。使用 iota 能简化定义,在定义枚举时很有用
package main
import "fmt"
// const来定义枚举类型
const (
// 可以在const()中添加一个关键字iota, 每行的iota都会累加1, 第一行的iota默认是0
BEIJING = 10 * iota // iota = 0
SHANGHAI // iota = 1
SHENZHEN // iota = 2
)
func main() {
fmt.Println("BEIJING = ", BEIJING) // 0
fmt.Println("SHANGHAI = ", SHANGHAI) // 10
fmt.Println("SHENZHEN = ", SHENZHEN) // 20
}
关键字
关键字是 Go 语言中预先保留的单词,在程序中有特殊含义,不能用来定义变量或常量。
Go 语言中有 25 个关键字:
Go基础数据类型
在静态类型语言(C++/Java/Golang 等)中规定在创建一个变量或者常量时,必须要指定出相应的数据类型,否则无法给变量分配内存。
整形
整型分两大类
- 有符号整型:int8、int16、int32、int64、int。
- 无符号整型:uint8、uint16、uint32、uint64、uint。
package main
import (
"fmt"
"math" "unsafe")
/*
----------有符号整型----------
*/
func Integer() {
var num8 int8 = 127
var num16 int16 = 32767
var num32 int32 = math.MaxInt32
//var num32 int32 = math.MinInt32 看最小值
var num64 int64 = math.MaxInt64
var num int = math.MaxInt
fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\n",
num8, unsafe.Sizeof(num8), num8)
fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\n",
num16, unsafe.Sizeof(num16), num16)
fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\n",
num32, unsafe.Sizeof(num32), num32)
fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\n",
num64, unsafe.Sizeof(num64), num64)
fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\n",
num, unsafe.Sizeof(num), num)
}
// 无符号整型
func unsignedInteger() {
var num8 uint8 = 128
var num16 uint16 = 32768
var num32 uint32 = math.MaxUint32
var num64 uint64 = math.MaxUint64
var num uint = math.MaxUint
fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\n",
num8, unsafe.Sizeof(num8), num8)
fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\n",
num16, unsafe.Sizeof(num16), num16)
fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\n",
num32, unsafe.Sizeof(num32), num32)
fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\n",
num64, unsafe.Sizeof(num64), num64)
fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\n",
num, unsafe.Sizeof(num), num)
}
func main() {
Integer()
println("---------------------------------------")
unsignedInteger()
}
Tips:
- 除非对整型的大小有特定的需求,否则你通常应该使用
int表示整型宽度,在32位系统下是32位,而在64位系统下是64位。表示范围:在32位系统下是-2147483648~2147483647,而在64位系统是-9223372036854775808~9223372036854775807。 - 对于
int8,int16等这些类型后面有跟一个数值的类型来说,它们能表示的数值个数是固定的。所以,在有的时候:例如在二进制传输、读写文件的结构描述(为了保持文件的结构不会受到不同编译目标平台字节长度的影响)等情况下,使用更加精确的int32和int64是更好的。
浮点型
浮点型表示存储的数据是实数,如 3.145。关于浮点型的说明,如表所示。 Go不同于C语言,Go的float类型是支持8位的长度
package main // 声明 main 包
import (
"fmt"
"math") // 导入 fmt 包,打印字符串时需要用到
func showFloat() {
var num1 float32 = math.MaxFloat32
var num2 float64 = math.MaxFloat64
fmt.Printf("num1的类型是%T,num1是%g\n", num1, num1)
fmt.Printf("num2的类型是%T,num1是%g\n", num2, num2)
}
func main() {
showFloat()
/*
num1的类型是float32,num1是3.4028235e+38
num2的类型是float64,num1是1.7976931348623157e+308
*/}
Tips:
-
通过上面的程序,我们知道浮点数能表示的数值很大,但是浮点数的精度却没有那么大:
float32的精度只能提供大约6个十进制数(表示小数点后6位)的精度。float64的精度能提供大约15个十进制数(表示小数点后15位)的精度。
字符
字符串中的每一个元素叫作“字符”,定义字符时使用单引号。Go 语言的字符有两种,如表所示。
package main
import (
"fmt"
"unsafe")
func showChar() {
var x byte = 65
var y uint8 = 65
fmt.Printf("x = %c\n", x) // x = A
fmt.Printf("y = %c\n", y) // y = A
}
func sizeOfChar() {
var x byte = 65
fmt.Printf("x = %c\n", x)
fmt.Printf("x 占用 %d 个字节\n", unsafe.Sizeof(x))
var y rune = 'A' //rune 是占用4个字节的
fmt.Printf("y = %c\n", y)
fmt.Printf("y 占用 %d 个字节\n", unsafe.Sizeof(y))
}
func main() {
showChar()
fmt.Println("-------------")
sizeOfChar()
/*
x = A
y = A
-------------
x = A x 占用 1 个字节
y = A y 占用 4 个字节
*/}
由此可见, byte 类型只能表示 28个值,所以你想表示其他一些值,例如中文的话,就得使用 rune 类型
var y rune = '牛'
字符串
字符串在 Go 语言中是以基本数据类型出现的,使用字符串就像使用其他原生基本数据类型 int、float32、float64、bool 一样。
示例程序:
package main
import "fmt"
func main() {
var study string // 定义名为str的字符串类型变量
study = "《Go语言极简一本通》" // 将变量赋值
study2 := "《从0到Go语言微服务架构师》" // 以自动推断方式初始化
fmt.Println(study)
fmt.Println(study2)
}
有些字符串没有现成的文字代号,所以只能用转义字符来表示。常用的转义字符如表所示。 定义多行字符串的方法如下。
- 双引号书写字符串被称为字符串字面量(string literal),这种字面量不能跨行。
- 多行字符串需要使用反引号“`”,多用于内嵌源码和内嵌数据。
- 在反引号中的所有代码不会被编译器识别,而只是作为字符串的一部分。
多行字符串定义方式如例所示:
package main
import "fmt"
func main() {
var s1 string
s1 = `
study := 'Go语言微服务架构核心22讲'
fmt.Println(study)
`
fmt.Println(s1)
}
布尔类型
关于 布尔(bool) 类型,无非就是两个值:true 或者 false 。
示例程序:
func showBool(){
a := true
b := false
fmt.Println("a=", a)
fmt.Println("b=", b)
fmt.Println("true && false = ", a && b)
fmt.Println("true || false = ", a || b)
}
func main() {
showBool()
}
Tip:
- 如果你学过其他编程语言或许会发现,布尔型可以参与数值运算,也可以与其他类型进行转换。但是在 Go 中,真值是用
true表示,并且 不与1相等;同样的,假值是用false表示,并且 不与0相等。
复数型
复数型用于表示数学中的复数,如 1+2j、1-2j、-1-2j 等。在 Go 语言中提供了两种精度的复数类型:complex64 和 complex128 ,分别对应 float32 和 float64 两种浮点数精度,如表所示。
package main
import "fmt"
func showComplex() {
// 内置的 complex 函数用于构建复数
var x complex64 = complex(1, 2)
var y complex128 = complex(3, 4)
var z complex128 = complex(5, 6)
fmt.Println("x = ", x)
fmt.Println("y = ", y)
fmt.Println("z = ", z)
fmt.Println("-------------------")
// 内建的 real 和 imag 函数分别返回复数的实部和虚部
fmt.Println("real(x) = ", real(x))
fmt.Println("imag(x) = ", imag(x))
fmt.Println("y * z = ", y*z)
}
func main() {
showComplex()
}
- 当然,我们可以对声明进行简化,使用自然的方式书写复数:
x := 1 + 2i
y := 3 + 4i
z := 5 + 6i
fmt 格式输出
Go容器类-数组
数组
数组 是一个由 固定长度 的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在 Go 语言中很少直接使用数组。和数组对应的类型是 slice(切片) ,它是可以动态的增长和收缩的序列, slice 功能也更灵活,下面我们再讨论 slice 。
数组声明
可以使用 [n]Type 来声明一个数组。其中 n 表示数组中元素的数量, Type 表示每个元素的类型。
package main
import "fmt"
func test01() {
// 声明时没有指定数组元素的值, 默认为零值
var arr [5]int
fmt.Println(arr)
arr[0] = 1
arr[1] = 2
arr[2] = 3
fmt.Println(arr)
}
func test02() {
// 直接在声明时对数组进行初始化
var arr1 = [5]int{15, 20, 25, 30, 35}
fmt.Println(arr1)
// 使用短声明
arr2 := [5]int{15, 20, 25, 30, 35}
fmt.Println(arr2)
// 部分初始化, 未初始化的为零值
arr3 := [5]int{15, 20} // [15 20 0 0 0]
fmt.Println(arr3)
// 可以通过指定索引,方便地对数组某几个元素赋值
arr4 := [5]int{1: 100, 4: 200}
fmt.Println(arr4) // [0 100 0 0 200]
// 直接使用 ... 让编译器为我们计算该数组的长度
arr5 := [...]int{15, 20, 25, 30, 35, 40}
fmt.Println(arr5)
}
func test03() {
// 特别注意数组的长度是类型的一部分,所以 [3]int 和 [5]int 是不同的类型
arr1 := [3]int{15, 20, 25}
arr2 := [5]int{15, 20, 25, 30, 35}
fmt.Printf("type of arr1 is %T\n", arr1)
fmt.Printf("type of arr2 is %T\n", arr2)
}
func test04() {
// 定义多维数组
arr := [3][2]string{
{"1", "Go语言极简一本通"},
{"2", "Go语言微服务架构核心22讲"},
{"3", "从0到Go语言微服务架构师"}}
fmt.Println(arr)
}
func main() {
test01()
fmt.Println("----------------")
test02()
fmt.Println("----------------")
test03()
fmt.Println("----------------")
test04()
fmt.Println("----------------")
}
数组长度
使用内置的 len 函数将返回数组中元素的个数,即数组的长度。
package main
import "fmt"
func main() {
arrLength()
}
func arrLength() {
arr := [...]string{"Go语言极简一本通", "Go语言微服务架构核心22讲", "从0到Go语言微服务架构师"}
fmt.Println("数组的长度是:", len(arr)) //数组的长度是: 3
}
数组遍历
使用 for range 循环可以获取数组每个索引以及索引上对应的元素。
package main
import "fmt"
func main() {
showArr()
}
func showArr() {
arr := [...]string{"Go语言极简一本通", "Go语言微服务架构核心22讲", "从0到Go语言微服务架构师"}
for index, value := range arr {
fmt.Printf("arr[%d]=%s\n", index, value)
}
fmt.Println("-----------")
for _, value := range arr {
fmt.Printf("value=%s\n", value)
}
}
数组是值类型
Go 中的数组是值类型而不是引用类型。当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,不会影响原始数组。
package main
import "fmt"
func main() {
arrByValue()
}
/*
Go 中的数组是值类型而不是引用类型。
当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。
如果对新变量进行更改,不会影响原始数组。
*/
func arrByValue() {
arr := [...]string{"Go语言极简一本通", "Go语言微服务架构核心22讲", "从0到Go语言微服务架构师"}
copy := arr
copy[0] = "Golang"
fmt.Println(arr)
fmt.Println(copy)
}
Go容器类-切片
切片(Slice)
切片是对数组的一个连续片段的引用,所以切片是一个引用类型。切片 本身不拥有任何数据,它们只是对现有数组的引用,每个切片值都会将数组作为其底层的数据结构。slice 的语法和数组很像,只是没有固定长度而已。
创建切片
使用 []Type 可以创建一个带有 Type 类型元素的切片。
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
你也可以使用 make 函数构造一个切片,格式为 make([]Type, size, cap) 。
//切片是用make的做的
numList := make([]int, 3, 5)
当然,我们可以通过对数组进行片段截取创建一个切片。
arr := [5]string{"Go语言极简一本通", "Go语言微服务架构核心22讲", "从0到Go语言微服务架构师", "微服务", "分布式"}
var s1 = arr[1:4]
fmt.Println(arr) // [Go语言极简一本通 Go语言微服务架构核心22讲 从0到Go语言微服务架构师 微服务 分布式]
fmt.Println(s1) // [Go语言微服务架构核心22讲 从0到Go语言微服务架构师 微服务]
切片的长度和容量
一个 slice 由三个部分构成:指针 、 长度 和 容量 。指针指向第一个 slice 元素对应的底层数组元素的地址,要注意的是 slice 的第一个元素并不一定就是数组的第一个元素。长度对应 slice 中元素的数目;长度不能超过容量,容量一般是从 slice 的开始位置到底层数据的结尾位置。简单的讲,容量就是从创建切片索引开始的底层数组中的元素个数,而长度是切片中的元素个数。
内置的 len 和 cap 函数分别返回 slice 的长度和容量。
s := make([]string, 3, 5)
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 5
如果切片操作超出上限将导致一个 panic 异常。
s := make([]int, 3, 5)
fmt.Println(s[10]) //panic: runtime error: index out of range [10] with length 3
Tips:
由于 slice 是引用类型,所以你不对它进行赋值的话,它的默认值是 nil
var numList []int
fmt.Println(numList == nil) // true
切片之间不能比较,因此我们不能使用== 操作符来判断两个 slice 是否含有全部相等元素。特别注意,如果你需要测试一个 slice 是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。
切片元素的修改
切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。
func modifySlice() {
var arr = [...]string{"Go语言极简一本通", "Go语言微服务架构核心22讲", "从0到Go语言微服务架构师"}
s := arr[:] //[0:len(arr)]
fmt.Println(arr) //[Go语言极简一本通 Go语言微服务架构核心22讲 从0到Go语言微服务架构师]
fmt.Println(s) //[Go语言极简一本通 Go语言微服务架构核心22讲 从0到Go语言微服务架构师]
s[0] = "Go语言"
fmt.Println(arr) //[Go语言 Go语言微服务架构核心22讲 从0到Go语言微服务架构师]
fmt.Println(s) //[Go语言 Go语言微服务架构核心22讲 从0到Go语言微服务架构师]
}
这个很容易理解切片是引用类型的数据,数组是值拷贝的数据 这里的 arr[:] 没有填入起始值和结束值,默认就是 0 和 len(arr)
追加切片元素
使用 append 可以将新元素追加到切片上。append 函数的定义是 func append(slice []Type, elems ...Type) []Type 。其中 elems ...Type 在函数定义中表示该函数接受参数 elems 的个数是可变的。这些类型的函数被称为可变函数。
package main
import "fmt"
func appendSliceData() {
s := []string{"Go语言极简一本通"}
fmt.Println(s)
fmt.Println(cap(s))
s = append(s, "Go语言微服务架构核心22讲")
fmt.Println(s)
fmt.Println(cap(s))
s = append(s, "从0到Go语言微服务架构师", "分布式")
fmt.Println(s)
fmt.Println(cap(s))
s = append(s, []string{"微服务", "分布式锁"}...)
fmt.Println(s)
fmt.Println(cap(s))
}
func main() {
appendSliceData()
}
当新的元素被添加到切片时,如果容量不足,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回新的引用。现在新切片的容量是旧切片的两倍。
多维切片
类似于数组,切片也可以有多个维度。
package main
import (
"fmt"
"unsafe")
func main() {
mSlice()
}
func mSlice() {
numList := [][]string{
{"1", "Go语言极简一本通"},
{"2", "Go语言微服务架构核心22讲"},
{"3", "从0到Go语言微服务架构师"}, {"4", "Beego"},
}
fmt.Println(numList)
fmt.Println("s的类型%T", unsafe.Sizeof(numList)) //固定24
}
Go容器类-Map
Map
在 Go 语言中,map 是散列表(哈希表)的引用。它是一个拥有键值对元素的无序集合,在这个集合中,键是唯一的,可以通过键来获取、更新或移除操作。无论这个散列表有多大,这些操作基本上是通过常量时间完成的。所有可比较的类型,如 整型 ,字符串 等,都可以作为 key 。
创建 Map
使用 make 函数传入键和值的类型,可以创建 map 。具体语法为 make(map[KeyType]ValueType) 。
// 创建一个键类型为 string 值类型为 int 名为 scores 的 map
scores := make(map[string]int)
steps := make(map[string]string)
我们也可以用 map 字面值的语法创建 map ,同时还可以指定一些最初的 key/value :
var steps2 map[string]string = map[string]string{
"第一步": "Go语言极简一本通",
"第二步": "Go语言微服务架构师核心22讲",
"第三步": "从0到Go语言微服务架构师",
}
fmt.Println(steps2)
或者
steps3 := map[string]string{
"第一步": "Go语言极简一本通",
"第二步": "Go语言微服务架构师核心22讲",
"第三步": "从0到Go语言微服务架构师",
}
fmt.Println(steps3)
Map 操作
- 添加元素
// 可以使用 `map[key] = value` 向 map 添加元素。
steps3["第四步"] = "总监"
更新元素
// 若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值。
steps3["第四步"] = "CTO"
获取元素
// 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零值。
fmt.Println(steps3["第四步"] )
删除元素
//使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete 函数会静默处理,不会报错。
delete(steps3, "第四步")
判断 key 是否存在
// 如果我们想知道 map 中的某个 key 是否存在,可以使用下面的语法:value, ok := map[key]
v3, ok := steps3["第三步"]
fmt.Println(ok)
fmt.Println(v3)
v4, ok := steps3["第四步"]
fmt.Println(ok)
fmt.Println(v4)
- 这个语句说明
map的下标读取可以返回两个值,第一个值为当前key的value值,第二个值表示对应的key是否存在,若存在ok为true,若不存在,则ok为false。 - 遍历 map
// 遍历 map 中所有的元素需要用 for range 循环。
for key, value := range steps3 {
fmt.Printf("key: %s, value: %d\n", key, value)
}
获取 map 长度
fmt.Println(len(steps3))
package main
import "fmt"
func main() {
MapOperate()
}
func MapOperate() {
steps3 := map[string]string{
"第一步": "Go语言极简一本通",
"第二步": "Go语言微服务架构师核心22讲",
"第三步": "从0到Go语言微服务架构师",
}
fmt.Println(steps3)
fmt.Println("向 map 添加元素-------------")
// 可以使用 `map[key] = value` 向 map 添加元素。
steps3["第四步"] = "总监"
fmt.Println(steps3)
fmt.Println("若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值-------------")
// 若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值。
steps3["第四步"] = "CTO"
fmt.Println(steps3)
fmt.Println(" 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零-------------")
// 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零值。
fmt.Println(steps3["第四步"])
fmt.Println(steps3)
fmt.Println("使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete 函数会静默处理,不会报错-------------")
//使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete 函数会静默处理,不会报错。
delete(steps3, "第四步")
fmt.Println(steps3)
// 如果我们想知道 map 中的某个 key 是否存在,可以使用下面的语法:value, ok := map[key]
}
map 是引用类型
当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。
func mapByReference() {
steps4 := map[string]string{
"第一步": "Go语言极简一本通",
"第二步": "Go语言微服务架构师核心22讲",
"第三步": "从0到Go语言微服务架构师",
}
fmt.Println("steps4: ", steps4)
// steps4: map[第一步:Go语言极简一本通 第三步:从0到Go语言微服务架构师 第二步:Go语言微服务架构师核心22讲]
newSteps4 := steps4
newSteps4["第一步"] = "Go语言极简一本通-222"
newSteps4["第二步"] = "Go语言微服务架构师核心22讲-222"
newSteps4["第三步"] = "从0到Go语言微服务架构师-222"
fmt.Println("steps4: ", steps4)
// steps4: map[第一步:Go语言极简一本通-222 第三步:从0到Go语言微服务架构师-222 第二步:Go语言微服务架构师核心22讲-222]
fmt.Println("newSteps4: ", newSteps4)
// newSteps4: map[第一步:Go语言极简一本通-222 第三步:从0到Go语言微服务架构师-222 第二步:Go语言微服务架构师核心22讲-222]
}
当 map 作为函数参数传递时也会发生同样的情况。