Go基础
Go 语言的主要特征
1.自动立即回收。 2.更丰富的内置类型。 3.函数多返回值。 4.错误处理。 5.匿名函数和闭包。 6.类型和接口。 7.并发编程。 8.反射。 9.语言交互性。
Golang文件名
所有的go源码都是以 ".go" 结尾。
Go语言命名
1.Go的函数、变量、常量、自定义类型、包(package)的命名方式遵循以下规则:
1)首字符可以是任意的Unicode字符或者下划线
2)剩余字符可以是Unicode字符、下划线、数字
3)字符长度不限
2.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
3.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
panic recover
4.可见性: 1)声明在函数内部,是函数的本地值,类似private 2)声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect 3)声明在函数外部且首字母大写是所有包可见的全局值,类似public
Go语言声明
var(声明变量), const(声明常量), type(声明类型) ,func(声明函数)
Go的程序是保存在多个.go文件中,文件的第一行就是package XXX声明,用来说明该文件属于哪个包(package),package声明下来就是import声明,再下来是类型,变量,常量,函数的声明。
Go项目构建及编译
一个Go工程中主要包含以下三个目录:
src:源代码文件
pkg:包文件
bin:相关bin文件
创建项目
如果选的第一个 需要用下面这条命令初始化
go mod init ***
选第二个(推荐)
设置
代理

go fmt
goimports
注释
package main
import "fmt"
// 我是单行注释
/*
我是多行注释
我是多行注释
我是多行注释
*/
func main() {
fmt.Println("Hello World!")
}
内置类型和函数
内置类型
值类型
bool
int(32 or 64), int8, int16, int32, int64
uint(32 or 64), uint8(byte), uint16, uint32, uint64
float32, float64
string
complex64, complex128
array -- 固定长度的数组
引用类型:(指针类型)
slice -- 序列数组(最常用)
map -- 映射
chan -- 管道
内置函数
Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。
append -- 用来追加元素到数组、slice中,返回修改后的数组、slice
close -- 主要用来关闭channel
delete -- 从map中删除key对应的value
panic -- 停止常规的goroutine (panic和recover:用来做错误处理)
recover -- 允许程序定义goroutine的panic动作
real -- 返回complex的实部 (complex、real imag:用于创建和操作复数)
imag -- 返回complex的虚部
make -- 用来分配内存,返回Type本身(只能应用于slice, map, channel)
new -- 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针
cap -- capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
copy -- 用于复制和连接slice,返回复制的数目
len -- 来求长度,比如string、array、slice、map、channel ,返回长度
print、println -- 底层打印函数,在部署环境中建议使用 fmt 包
内置接口error
type error interface { //只要实现了Error()函数,返回值为String的都实现了err接口
Error() String
}
Init函数和main函数
init函数
go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性。 有下面的特征: 1、init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等。 2、每个包可以拥有多个init函数 3、包的每个源文件也可以拥有多个init函数 4、同一个包中多个init函数的执行顺序go语言没有明确的定义(说明)。 5、不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序 6、init函数不能被其他函数调用,而是在main函数执行之前,自动被调用
main函数
go语言程序的默认入口函数(主函数),func main() 函数体用 { } 一对大括号包裹 main在一个包中有且只有一个
package main
import "fmt"
var log string
func main() {
fmt.Println("main()")
fmt.Println(log)
}
func init() {
fmt.Println("init()")
log = "log"
}
init函数和main函数的异同
相同点 两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。 不同点 init可以应用于任意包中,且可以重复定义多个。 main函数只能用于main包中,且只能定义一个。 两个函数的执行顺序 对同一个go文件的init()调用顺序是从上到下的。 对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init ()函数。 对于不同的package,如果不相互依赖的话,按照main包中"先import的后调用"的顺序调用其包中的init() 如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。 如果init函数中使用了println()或者print()你会发现在执行过程中这两个不会按照你想象中的顺序执行。 这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用。 执行顺序 全局声明 --> init() --> main()
命令
go env用于打印Go语言的环境信息。 go run命令可以编译并运行命令源码文件。 go get可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。 go build命令用于编译我们指定的源码文件或代码包以及它们的依赖包。 go install用于编译并安装指定的代码包及它们的依赖包。 go clean命令会删除掉执行其它命令时产生的一些文件和目录。 go doc命令可以打印附于Go语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的。 go test命令用于对Go语言编写的程序进行测试。 go list命令的作用是列出指定的代码包的信息。 go fix会把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码。 go vet是一个用于检查Go语言源码中静态错误的简单工具。 go tool pprof命令来交互式的访问概要文件的内容。
运算符
算数运算符
运算符 | 描述 |
---|---|
+ | 相加 |
- | 相减 |
* | 相乘 |
/ | 相除 |
% | 求余 |
注意: ++(自增)和–(自减)在Go语言中是单独的语句,并不是运算符。
关系运算符
运算符 | 描述 |
---|---|
== | 检查两个值是否相等,如果相等返回 True 否则返回 False。 |
!= | 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 |
> | 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 |
>= | 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 |
< | 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 |
<= | 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 |
逻辑运算符
运算符 | 描述 |
---|---|
&& | 逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。 |
ll | 逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。 |
! | 逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。 |
位运算符
位运算符对整数在内存中的二进制位进行操作。
运算符 | 描述 |
---|---|
& | 参与运算的两数各对应的二进位相与。(两位均为1才为1) |
l | 参与运算的两数各对应的二进位相或。(两位有一个为1就为1) |
^ | 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。(两位不一样则为1) |
<< | 左移n位就是乘以2的n次方。“a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。 |
赋值运算符
运算符 | 描述 |
---|---|
= | 简单的赋值运算符,将一个表达式的值赋给一个左值 |
+= | 相加后再赋值 |
-= | 相减后再赋值 |
*= | 相乘后再赋值 |
/= | 相除后再赋值 |
%= | 求余后再赋值 |
<<= | 左移后赋值 |
>>= | 右移后赋值 |
&= | 按位与后赋值 |
l= | 按位或后赋值 |
^= | 按位异或后赋值 |
占位符
占位符 | 含义 |
---|---|
%v | 只输出所有的值 |
%+v | 先输出结构体字段类型,在输出字段值 |
%#v | 先输出结构体名称,再输出结构体字段类型+值 |
%T | 输出结构体名称,或输出目标的类型 |
%% | 输出字面上的百分号 |
%b | 二进制表示 |
%c | 相应的Unicode码所表示的字符 |
%d | 十进制表示 |
%o | 八进制表示 |
%x | 十六进制表示,字母形式a-f |
%X | 十六进制,字母形式A-F |
%q | 双引号围绕的字符串 |
%e | 科学计数法1.020000e+01 |
%E | 科学计数法1.020000E+01 |
%f | 小数输出,有小数点而无指数 |
%p | 十六进制输出,输出指针类型 |
%g | 末尾无零的小数输出 |
%G | 末尾无零的小数输出 |
%t | 布尔占位符 |
%p跟%x区别 %x打印的是十六进制数,%p打印地址,也是十六进制,但%p以0x作为前缀,也就是说%p等于0x拼接%x。例如: 同一个指针,用%x输出:c00000a0c8 用%p输出:0xc00000a0c8
下划线
“_”是特殊标识符,用来忽略结果。
下划线在import中
在Golang里,import的作用是导入其他package。 import 下划线(如:import _ hello/imp)的作用:当导入一个包时,该包下的文件里所有init()函数都会被执行,然而,有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。这个时候就可以使用 import _ 引用该包。即使用【import _ 包路径】只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。 代码结构
src
|
+--- main.go
|
+--- hello1
|
+--- hello.go
package main
import (
_ "hello/hello1"
)
func main() {
//fmt.Println("Hello World!")
hello1.Print()
}
package hello1
import "fmt"
func init() {
fmt.Println("imp-init() come here.")
}
func Print() {
fmt.Println("Hello!")
}
输出结果:
imp-init() come here.
在代码中
package main
import (
"os"
)
func main() {
buf := make([]byte, 1024)
f, _ := os.Open("/Users/***/Desktop/text.txt")
defer f.Close()
for {
n, _ := f.Read(buf)
if n == 0 {
break
}
os.Stdout.Write(buf[:n])
}
}
解释1: 下划线意思是忽略这个变量. 比如os.Open,返回值为*os.File,error 普通写法是f,err := os.Open("xxxxxxx") 如果此时不需要知道返回的错误值 就可以用f, _ := os.Open("xxxxxx") 如此则忽略了error变量 解释2: 占位符,意思是那个位置本应赋给某个值,但是咱们不需要这个值。 所以就把该值赋给下划线,意思是丢掉不要。 这样编译器可以更好的优化,任何类型的单个值都可以丢给下划线。 这种情况是占位用的,方法返回两个结果,而你只想要一个结果。 那另一个就用 "_" 占位,而如果用变量的话,不使用,编译器是会报错的。 补充:
import "database/sql"
import _ "github.com/go-sql-driver/mysql"
第二个import就是不直接使用mysql包,只是执行一下这个包的init函数,把mysql的驱动注册到sql包里,然后程序里就可以使用sql包来访问mysql数据库了。
变量和常量
变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型、浮点型、布尔型等。 Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。 Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。并且Go语言的变量声明后必须使用。
变量
标准声明
var 变量名 变量类型
变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。 举个例子:
var name string
var age int
var isOk bool
批量声明变量
package main
import "fmt"
func main() {
//批量声明
var (
name string
age int
)
name = "LiAng"
age = 21
fmt.Println(name, age)
}
三种定义方法
package main
import "fmt"
func main() {
// 第1种
var num1 int = 100
// 第2种 最常用
num2 := 200
// 第3种 最不常用,定义全局变量
var num3 int
num3 = 300
fmt.Println(num1)
fmt.Println(num2)
fmt.Println(num3)
//同时定义多个变量
var x, y int
x = 1
y = 2
fmt.Println(x, y)
}
类型推导
有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。
var name = "pprof.cn"
var sex = 1
匿名变量
在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_表示,例如:
func foo() (int, string) {
return 10, "Q1mi"
}
func main() {
x, _ := foo()
_, y := foo()
fmt.Println("x=", x)
fmt.Println("y=", y)
}
注意事项:
匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua等编程语言里,匿名变量也被叫做哑元变量。)
函数外的每个语句都必须以关键字开始(var、const、func等)
:=不能使用在函数外。
_多用于占位,表示忽略值。
常量
相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。
const PI = 3.1415
const e = 2.7182
//同时声明多个常量
const (
PI2 = 3.1415
e2 = 2.7182
)
const同时声明多个常量时,如果省略了值则表示和上面一行的值相同(常量n1、n2、n3的值都是100)。 例如:
const (
n1 = 100
n2
n3
)
iota
iota是go语言的常量计数器,只能在常量的表达式中使用。 iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)
几个常见的iota示例:
使用_跳过某些值
const (
n1 = iota //0
n2 //1
_
n4 //3
)
iota声明中间插队
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0
定义数量级 (这里的<<表示左移操作,1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000,也就是十进制的1024。同理2<<2表示将2的二进制表示向左移2位,也就是由10变成了1000,也就是十进制的8。)
const (
_ = iota
KB = 1 << (10 * iota)
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)
多个iota定义在一行
const (
a, b = iota + 1, iota + 2 //1,2
c, d //2,3
e, f //3,4
)
基本类型
介绍
Golang 更明确的数字类型命名,支持 Unicode,支持常用数据结构。
类型 | 长度(字节) | 默认值 | 说明 |
---|---|---|---|
bool | 1 | false | |
byte | 1 | 0 | uint8 |
rune | 4 | 0 | Unicode Code Point, int32 |
int, uint | 4或8 | 0 | 32 或 64 位 |
int8, uint8 | 1 | 0 | -128 ~ 127, 0 ~ 255,byte是uint8 的别名 |
int16, uint16 | 2 | 0 | -32768 ~ 32767, 0 ~ 65535 |
int32, uint32 | 4 | 0 | -21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名 |
int64, uint64 | 8 | 0 | |
float32 | 4 | 0.0 | |
float64 | 8 | 0.0 | |
complex64 | 8 | ||
complex128 | 16 | ||
uintptr | 4或8 | 以存储指针的 uint32 或 uint64 整数 | |
array | 值类型 | ||
struct | 值类型 | ||
string | “” | UTF-8 字符串 | |
slice | nil | 引用类型 | |
map | nil | 引用类型 | |
channel | nil | 引用类型 | |
interface | nil | 接口 | |
function | nil | 函数 |
支持八进制、 六进制,以及科学记数法。标准库 math 定义了各数字类型取值范围。
a, b, c, d := 071, 0x1F, 1e9, math.MinInt16
空指针值 nil,而非C/C++ NULL。
整型
整型分为以下两个大类: 按长度分为:int8、int16、int32、int64对应的无符号整型:uint8、uint16、uint32、uint64 其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。
浮点型
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 语言中不允许将整型强制转换为布尔型.
布尔型无法参与数值运算,也无法与其他类型进行转换。
字符串
Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。 Go 语言里的字符串的内部实现使用UTF-8编码。 字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCII码字符,例如:
s1 := "hello"
s2 := "你好"
字符串转义符
Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。
转义 | 含义 |
---|---|
\r | 回车符(返回行首) |
\n | 换行符(直接跳到下一行的同列位置) |
\t | 制表符 |
' | 单引号 |
" | 双引号 |
\ | 反斜杠 |
举个例子,我们要打印一个Windows平台下的一个文件路径:
package main
import (
"fmt"
)
func main() {
fmt.Println("str := \"c:\\pprof\\main.exe\"")
}
多行字符串
Go语言中要定义一个多行字符串时,就必须使用反引号字符:
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操作 |
byte和rune类型
组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(')包裹起来,如:
var a := '中'
var b := 'x'
Go 语言的字符有以下两种:
- uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
- rune类型,代表一个 UTF-8字符。
当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。 Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾
func traversalString() {
s := "LiAng, 你好"
for i := 0; i < len(s); i++ {
fmt.Printf("%v(%c)", s[i], s[i])
}
fmt.Println()
for _, r := range s {
fmt.Printf("%v(%c)", r, r)
}
fmt.Println()
}
输出:
76(L)105(i)65(A)110(n)103(g)44(,)32( )228(ä)189(½)160( )229(å)165(¥)189(½)
76(L)105(i)65(A)110(n)103(g)44(,)32( )20320(你)22909(好)
因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果。 字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。
修改字符串
要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。
func changeString() {
s1 := "hello"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'H'
fmt.Println(string(byteS1))
s2 := "猫猫"
runeS2 := []rune(s2)
runeS2[1] = '毛'
fmt.Println(string(runeS2))
}
类型转换
Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。 强制类型转换的基本语法如下:
T(表达式)
其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等. 比如计算直角三角形的斜边长时使用math包的Sqrt()函数,该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制类型转换为float64类型。
func sqrtDemo() {
var a, b = 3, 4
var c int
// math.Sqrt()接收的参数是float64类型,需要强制转换
c = int(math.Sqrt(float64(a*a + b*b)))
fmt.Println(c)
}
数组Array
Golang Array和以往认知的数组有很大不同。 1. 数组:是同一种数据类型的固定长度的序列。 2. 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。 3. 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。 4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
for i := 0; i < len(a); i++ {
}
for index, v := range a {
}
5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
6. 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
7. 支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
8. 指针数组 [n]*T,数组指针 *[n]T。
数组初始化
一维数组
package main
import "fmt"
// 全局:
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}
func main() {
// 局部
a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4: 200} // 使用冒号初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素类型。
{"user2", 20}, // 别忘了最后一行的逗号。
}
fmt.Println(arr0, arr1, arr2, str)
fmt.Println(a, b, c, d)
}
输出结果
[1 2 3 0 0] [1 2 3 4 5] [1 2 3 4 5 6] [ hello world tom]
[1 2 0] [1 2 3 4] [0 0 100 0 200] [{user1 10} {user2 20}]
多维数组
package main
import "fmt"
// 全局
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
func main() {
// 局部
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
fmt.Println(arr0, arr1)
fmt.Println(a, b)
}
输出结果
[[0 0 0] [0 0 0] [0 0 0] [0 0 0] [0 0 0]] [[1 2 3] [7 8 9]]
[[1 2 3] [4 5 6]] [[1 1] [2 2] [3 3]]
值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。
package main
import (
"fmt"
)
func test(x [2]int) {
fmt.Printf("x: %p\n", &x)
x[1] = 1000
fmt.Println("x: ", x)
}
func main() {
a := [2]int{}
fmt.Printf("a: %p\n", &a)
test(a)
fmt.Println("a: ", a)
}
输出结果
a: 0xc00001a0d0
x: 0xc00001a100
x: [0 1000]
a: [0 0]
内置函数 len 和 cap 都返回数组长度 (元素数量)。
package main
func main() {
a := [2]int{}
println(len(a), cap(a))
}
输出
2 2
多维数组遍历
package main
import "fmt"
func main() {
var f [2][3]int = [...][3]int{{1, 2, 3}, {4, 5, 6}}
for k1, v1 := range f {
for k2, v2 := range v1 {
fmt.Printf(" (%d,%d)=%d", k1, k2, v2)
}
fmt.Println()
}
}
输出结果
(0,0)=1 (0,1)=2 (0,2)=3
(1,0)=4 (1,1)=5 (1,2)=6
数组拷贝和传参
package main
import "fmt"
func printArr(arr *[5]int) {
arr[0] = 10
for i, v := range arr {
fmt.Println(i, v)
}
}
func main() {
var arr1 [5]int
printArr(&arr1)
fmt.Println(arr1)
arr2 := [...]int{2, 4, 6, 8, 10}
printArr(&arr2)
fmt.Println(arr2)
}
输出结果
0 10
1 0
2 0
3 0
4 0
[10 0 0 0 0]
0 10
1 4
2 6
3 8
4 10
[10 4 6 8 10]
数组练习
求数组所有元素之和
package main
import (
"fmt"
"math/rand"
"time"
)
func sumArr(a [5]int) int {
sum := 0
for i := 0; i < len(a); i++ {
sum += a[i]
}
return sum
}
func main() {
// 若想做一个真正的随机数,要种子
// seed()种子默认是1
//rand.Seed(1)
rand.Seed(time.Now().Unix())
var b [5]int
for i := 0; i < len(b); i++ {
// 产生一个0到100随机数
b[i] = rand.Intn(100)
}
fmt.Println(b)
sum := sumArr(b)
fmt.Printf("sum=%d\n", sum)
}
输出结果
[38 88 55 27 88]
sum=296
找出数组中和为给定值的两个元素的下标,例如数组[1,3,5,8,7],找出两个元素之和等于8的下标分别是(0,4)和(1,2)
package main
import (
"fmt"
)
func myTest(a [5]int, target int) {
//遍历数组
for i := 0; i < len(a); i++ {
other := target - a[i]
for j := i + 1; j < len(a); j++ {
if a[j] == other {
fmt.Printf("%d + %d = %d\n", a[i], a[j], target)
}
}
}
}
func main() {
b := [5]int{1, 3, 5, 8, 7}
myTest(b, 8)
}
输出结果
1 + 7 = 8
3 + 5 = 8
切片Slice
需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。 1. 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。 2. 切片的长度可以改变,因此,切片是一个可变的数组。 3. 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。 4. cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。 5. 切片的定义:var 变量名 []类型,比如 var str []string , var arr []int。 6. 如果 slice == nil,那么 len、cap 结果都等于 0。
创建切片的各种方式
package main
import "fmt"
func main() {
// 1.声明切片
var s1 []int
if s1 == nil {
fmt.Println("是空")
} else {
fmt.Println("不是空")
}
// 2. :=
s2 := []int{}
// 3.make()
var s3 []int = make([]int, 0)
fmt.Println(s1, s2, s3)
// 4.初始化赋值
var s4 []int = make([]int, 0, 0)
fmt.Println(s4)
s5 := []int{1, 2, 3}
fmt.Println(s5)
// 5.从数组切片
arr := [5]int{1, 2, 3, 4, 5}
var s6 []int
s6 = arr[1:4] // 左闭右开
fmt.Println(s6)
}
切片初始化
全局:
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[start:end]
var slice1 []int = arr[:end]
var slice2 []int = arr[start:]
var slice3 []int = arr[:]
var slice4 = arr[:len(arr)-1] //去掉切片的最后一个元素
局部:
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr[start:end]
slice6 := arr[:end]
slice7 := arr[start:]
slice8 := arr[:]
slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素

package main
import "fmt"
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[2:8]
var slice1 []int = arr[0:6] //可以简写为 var slice []int = arr[:end]
var slice2 []int = arr[5:10] //可以简写为 var slice[]int = arr[start:]
var slice3 []int = arr[0:len(arr)] //var slice []int = arr[:]
var slice4 = arr[:len(arr)-1] //去掉切片的最后一个元素
func main() {
fmt.Printf("全局变量:arr %v\n", arr)
fmt.Printf("全局变量:slice0 %v\n", slice0)
fmt.Printf("全局变量:slice1 %v\n", slice1)
fmt.Printf("全局变量:slice2 %v\n", slice2)
fmt.Printf("全局变量:slice3 %v\n", slice3)
fmt.Printf("全局变量:slice4 %v\n", slice4)
fmt.Printf("-----------------------------------\n")
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr[2:8]
slice6 := arr[0:6] //可以简写为 slice := arr[:end]
slice7 := arr[5:10] //可以简写为 slice := arr[start:]
slice8 := arr[0:len(arr)] //slice := arr[:]
slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
fmt.Printf("局部变量: arr2 %v\n", arr2)
fmt.Printf("局部变量: slice5 %v\n", slice5)
fmt.Printf("局部变量: slice6 %v\n", slice6)
fmt.Printf("局部变量: slice7 %v\n", slice7)
fmt.Printf("局部变量: slice8 %v\n", slice8)
fmt.Printf("局部变量: slice9 %v\n", slice9)
}
输出结果
全局变量:arr [0 1 2 3 4 5 6 7 8 9]
全局变量:slice0 [2 3 4 5 6 7]
全局变量:slice1 [0 1 2 3 4 5]
全局变量:slice2 [5 6 7 8 9]
全局变量:slice3 [0 1 2 3 4 5 6 7 8 9]
全局变量:slice4 [0 1 2 3 4 5 6 7 8]
局部变量: arr2 [9 8 7 6 5 4 3 2 1 0]
局部变量: slice5 [2 3 4 5 6 7]
局部变量: slice6 [0 1 2 3 4 5]
局部变量: slice7 [5 6 7 8 9]
局部变量: slice8 [0 1 2 3 4 5 6 7 8 9]
局部变量: slice9 [0 1 2 3 4 5 6 7 8]
通过make来创建切片
var slice []type = make([]type, len)
slice := make([]type, len)
slice := make([]type, len, cap)

package main
import "fmt"
var slice0 []int = make([]int, 10)
var slice1 = make([]int, 10)
var slice2 = make([]int, 10, 10)
func main() {
fmt.Printf("make全局slice0 :%v\n", slice0)
fmt.Printf("make全局slice1 :%v\n", slice1)
fmt.Printf("make全局slice2 :%v\n", slice2)
fmt.Println("--------------------------------------")
slice3 := make([]int, 10)
slice4 := make([]int, 10)
slice5 := make([]int, 10, 10)
fmt.Printf("make局部slice3 :%v\n", slice3)
fmt.Printf("make局部slice4 :%v\n", slice4)
fmt.Printf("make局部slice5 :%v\n", slice5)
}
输出结果
make全局slice0 :[0 0 0 0 0 0 0 0 0 0]
make全局slice1 :[0 0 0 0 0 0 0 0 0 0]
make全局slice2 :[0 0 0 0 0 0 0 0 0 0]
make局部slice3 :[0 0 0 0 0 0 0 0 0 0]
make局部slice4 :[0 0 0 0 0 0 0 0 0 0]
make局部slice5 :[0 0 0 0 0 0 0 0 0 0]
切片的内存布局
读写操作实际目标是底层数组,只需注意索引号的差别。
package main
import "fmt"
func main() {
data := [...]int{0, 1, 2, 3, 4, 5}
s := data[2:4]
s[0] += 100
s[1] += 200
fmt.Println(s)
fmt.Println(data)
}
输出结果
[102 203]
[0 1 102 203 4 5]
可直接创建 slice 对象,自动分配底层数组。
package main
import "fmt"
func main() {
s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。
fmt.Println(s1, len(s1), cap(s1))
s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
fmt.Println(s2, len(s2), cap(s2))
s3 := make([]int, 6) // 省略 cap,相当于 cap = len。
fmt.Println(s3, len(s3), cap(s3))
}
输出结果
[0 1 2 3 0 0 0 0 100] 9 9
[0 0 0 0 0 0] 6 8
[0 0 0 0 0 0] 6 6
使用 make 动态创建slice,避免了数组必须用常量做长度的麻烦。还可用指针直接访问底层数组,退化成普通数组操作。
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3}
p := &s[2]
*p += 100
fmt.Println(s)
}
输出结果
[0 1 102 3]
至于 [][]T,是指元素类型为 []T 。
package main
import "fmt"
func main() {
data := [][]int{
[]int{1, 2, 3},
[]int{100, 200},
[]int{11, 22, 33, 44},
}
fmt.Println(data)
}
输出结果
[[1 2 3] [100 200] [11 22 33 44]]
可直接修改 struct array/slice 成员。
package main
import "fmt"
func main() {
d := [5]struct {
x int
}{}
s := d[:]
d[1].x = 10
s[2].x = 20
fmt.Println(d)
fmt.Printf("%p, %p\n", &d, &d[0])
}
输出结果
[{0} {10} {20} {0} {0}]
0xc00000e360, 0xc00000e360
用append内置函数操作切片(切片追加)
package main
import "fmt"
func main() {
var a = []int{1, 2, 3}
fmt.Printf("slice a : %v\n", a)
var b = []int{4, 5, 6}
fmt.Printf("slice b : %v\n", b)
c := append(a, b...)
fmt.Printf("slice c : %v\n", c)
d := append(c, 7)
fmt.Printf("slice d : %v\n", d)
e := append(d, 8, 9, 10)
fmt.Printf("slice e : %v\n", e)
}
输出结果
slice a : [1 2 3]
slice b : [4 5 6]
slice c : [1 2 3 4 5 6]
slice d : [1 2 3 4 5 6 7]
slice e : [1 2 3 4 5 6 7 8 9 10]
append :向 slice 尾部添加数据,返回新的 slice 对象。
package main
import "fmt"
func main() {
s1 := make([]int, 0, 5)
fmt.Printf("%p\n", &s1)
s2 := append(s1, 1)
fmt.Printf("%p\n", &s2)
fmt.Println(s1, s2)
}
输出结果
0xc000092060
0xc000092078
[] [1]
超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满。
package main
import "fmt"
func main() {
data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3]
s = append(s, 100, 200) // 一次 append 两个值,超出 s.cap 限制。
fmt.Println(s, data) // 重新分配底层数组,与原数组无关。
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。
}
输出结果
[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
0xc0000c8030 0xc000086060
从输出结果可以看出,append 后的 s 重新分配了底层数组,并复制数据。如果只追加一个值,则不会超过 s.cap 限制,也就不会重新分配。 通常以 2 倍容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。或初始化足够长的 len 属性,改用索引号进行操作。及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收。
slice中cap重新分配规律
package main
import "fmt"
func main() {
s := make([]int, 0, 1)
c := cap(s)
for i := 0; i < 50; i++ {
s = append(s, i)
if n := cap(s); n > c {
fmt.Printf("cap: %d -> %d\n", c, n)
c = n
}
}
}
输出结果
cap: 1 -> 2
cap: 2 -> 4
cap: 4 -> 8
cap: 8 -> 16
cap: 16 -> 32
cap: 32 -> 64
切片拷贝
package main
import (
"fmt"
)
func main() {
s1 := []int{1, 2, 3, 4, 5}
fmt.Printf("slice s1 : %v\n", s1)
s2 := make([]int, 10)
fmt.Printf("slice s2 : %v\n", s2)
copy(s2, s1)
fmt.Printf("copied slice s1 : %v\n", s1)
fmt.Printf("copied slice s2 : %v\n", s2)
s3 := []int{1, 2, 3}
fmt.Printf("slice s3 : %v\n", s3)
s3 = append(s3, s2...)
fmt.Printf("appended slice s3 : %v\n", s3)
s3 = append(s3, 4, 5, 6)
fmt.Printf("last slice s3 : %v\n", s3)
输出结果
slice s1 : [1 2 3 4 5]
slice s2 : [0 0 0 0 0 0 0 0 0 0]
copied slice s1 : [1 2 3 4 5]
copied slice s2 : [1 2 3 4 5 0 0 0 0 0]
slice s3 : [1 2 3]
appended slice s3 : [1 2 3 1 2 3 4 5 0 0 0 0 0]
last slice s3 : [1 2 3 1 2 3 4 5 0 0 0 0 0 4 5 6]
copy :函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。
package main
import (
"fmt"
)
func main() {
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println("array data : ", data)
s1 := data[8:]
s2 := data[:5]
fmt.Printf("slice s1 : %v\n", s1)
fmt.Printf("slice s2 : %v\n", s2)
copy(s2, s1)
fmt.Printf("copied slice s1 : %v\n", s1)
fmt.Printf("copied slice s2 : %v\n", s2)
fmt.Println("last array data : ", data)
}
输出结果
array data : [0 1 2 3 4 5 6 7 8 9]
slice s1 : [8 9]
slice s2 : [0 1 2 3 4]
copied slice s1 : [8 9]
copied slice s2 : [8 9 2 3 4]
last array data : [8 9 2 3 4 5 6 7 8 9]
应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存。
slice遍历
package main
import "fmt"
func main() {
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice := data[:]
for index, value := range slice {
fmt.Printf("inde : %v , value : %v\n", index, value)
}
}
inde : 0 , value : 0
inde : 1 , value : 1
inde : 2 , value : 2
inde : 3 , value : 3
inde : 4 , value : 4
inde : 5 , value : 5
inde : 6 , value : 6
inde : 7 , value : 7
inde : 8 , value : 8
inde : 9 , value : 9
切片resize(调整大小)
package main
import (
"fmt"
)
func main() {
var a = []int{1, 3, 4, 5}
fmt.Printf("slice a : %v , len(a) : %v\n", a, len(a))
b := a[1:2]
fmt.Printf("slice b : %v , len(b) : %v\n", b, len(b))
c := b[0:3]
fmt.Printf("slice c : %v , len(c) : %v\n", c, len(c))
}
slice a : [1 3 4 5] , len(a) : 4
slice b : [3] , len(b) : 1
slice c : [3 4 5] , len(c) : 3
数组和切片的内存布局

字符串和切片(string and slice)
string底层就是一个byte的数组,因此,也可以进行切片操作。
package main
import "fmt"
func main() {
str := "hello world"
s1 := str[0:5]
fmt.Println(s1)
s2 := str[6:]
fmt.Println(s2)
}
hello
world
string本身是不可变的,因此要改变string中字符。需要如下操作: 英文字符串:
package main
import (
"fmt"
)
func main() {
str := "Hello world"
s := []byte(str) //中文字符需要用[]rune(str)
s[6] = 'G'
s = s[:8]
s = append(s, '!')
str = string(s)
fmt.Println(str)
}
Hello Go!
含有中文字符串
package main
import (
"fmt"
)
func main() {
str := "你好,世界!hello world!"
s := []rune(str)
s[3] = '够'
s[4] = '浪'
s[12] = 'g'
s = s[:14]
str = string(s)
fmt.Println(str)
}
你好,够浪!hello go
golang slice data[:6:8] 两个冒号的理解 常规slice , data[6:8],从第6位到第8位(返回6, 7),长度len为2, 最大可扩充长度cap为4(6-9) 另一种写法: data[:6:8] 每个数字前都有个冒号, slice内容为data从0到第6位,长度len为6,最大扩充项cap设置为8 a[x:y:z] 切片内容 [x:y] 切片长度: y-x 切片容量:z-x
package main
import (
"fmt"
)
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
d1 := slice[6:8]
fmt.Println(d1, len(d1), cap(d1))
d2 := slice[:6:8]
fmt.Println(d2, len(d2), cap(d2))
}
[6 7] 2 4
[0 1 2 3 4 5] 6 8
数组or切片转字符串:
strings.Replace(strings.Trim(fmt.Sprint(array_or_slice), "[]"), " ", ",", -1)
Slice底层实现
指针
区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。 要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
Go语言中的指针
Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。
指针地址和指针类型
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。 取变量指针的语法如下:
ptr := &v // v的类型为T
- v:代表被取地址的变量,类型为T
- ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。
举个例子:
func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc000122058
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc000122058 type:*int
fmt.Println(&b) // 0xc000146018
}
我们来看一下b := &a的图示:
指针取值
在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
func main() {
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}
type of b:*int
type of c:int
value of c:10
总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。 变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:\
- 1.对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 2.指针变量的值是指针地址。
- 3.对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
指针传值示例:
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}
func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}
空指针
- 当一个指针被定义后没有分配到任何变量时,它的值为 nil
- 空指针的判断
func main() {
var p *string
fmt.Println(p)
fmt.Printf("p的值是%v\n", p)
if p != nil {
fmt.Println("非空")
} else {
fmt.Println("空值")
}
}
<nil>
p的值是<nil>
空值
new和make
func main() {
var a *int
*a = 100
fmt.Println(*a)
var b map[string]int
b["测试"] = 100
fmt.Println(b)
}
运行报错
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x0 pc=0x89e51a]
执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存
new
new是一个内置的函数,它的函数签名如下:
func new(Type) *Type
- 1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
- 2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}
本节开始的示例代码中var a *int只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:
func main() {
var a *int
a = new(int)
*a = 10
fmt.Println(*a) // 10
}
make
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
func make(t Type, size ...IntegerType) Type
make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。这个我们在上一章中都有说明,关于channel我们会在后续的章节详细说明。 本节开始的示例中var b map[string]int只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:
func main() {
var b map[string]int
b = make(map[string]int, 10)
b["测试"] = 100
fmt.Println(b) // map[测试:100]
}
new与make的区别
- 1.二者都是用来做内存分配的
- 2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身
- 3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针
指针小练习
程序定义一个int变量num的地址并打印 将num的地址赋给指针ptr,并通过ptr去修改num的值
func main() {
var num int
fmt.Println(&num) // 0xc00001a0b8
var p *int = &num
*p = 100
fmt.Println(num) // 100
}
Map
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。 Go语言中 map的定义语法如下
map[KeyType]ValueType
KeyType:表示键的类型。 ValueType:表示键对应的值的类型。 map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
make(map[KeyType]ValueType, [cap])
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
map基本使用
map中的数据都是成对出现的,map的基本使用示例代码如下:
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}
map[小明:100 张三:90]
100
type of a:map[string]int
map也支持在声明的时候填充元素
func main() {
userInfo := map[string]string{
"username": "LiAng",
"password": "123456",
}
fmt.Println(userInfo) // map[password:123456 username:LiAng]
}
判断某个键是否存在
Go语言中有个判断map中键是否存在的特殊写法,格式如下:
value, ok := map[key]
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
}
map的遍历
Go语言中使用for range遍历map。
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
但我们只想遍历key的时候,可以按下面的写法:
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
for k := range scoreMap {
fmt.Println(k)
}
}
但我们只想遍历value的时候
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
for _, v := range scoreMap {
fmt.Println(v)
}
}
注意: 遍历map时的元素顺序与添加键值对的顺序无关。
使用delete()函数删除键值对
使用delete()内建函数从map中删除一组键值对,delete()函数的格式如下:
delete(map, key)
- map:表示要删除键值对的map
- key:表示要删除的键值对的键
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
delete(scoreMap, "小明") //将小明:100从map中删除
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
按照指定顺序遍历map
func main() {
rand.Seed(time.Now().UnixNano()) //初始化随机数种子
var scoreMap = make(map[string]int, 200)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
value := rand.Intn(100) //生成0~99的随机整数
scoreMap[key] = value
}
//取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}
//对切片进行排序
sort.Strings(keys)
//按照排序后的key遍历map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}
元素为map类型的切片
下面的代码演示了切片中的元素为map类型时的操作:
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "王五"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "红旗大街"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
index:0 value:map[]
index:1 value:map[]
index:2 value:map[]
after init
index:0 value:map[address:红旗大街 name:王五 password:123456]
index:1 value:map[]
index:2 value:map[]
值为切片类型的map
下面的代码演示了map中值为切片类型的操作
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}
map[]
after init
map[中国:[北京 上海]]
Map实现原理
结构体
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
类型别名和自定义类型
自定义类型
在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type关键字来定义自定义类型。 自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
//将MyInt定义为int类型
type MyInt int
通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
类型别名
类型别名是Go1.9版本添加的新功能。 类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type
我们之前见过的rune和byte就是类型别名,他们的定义如下:
type byte = uint8
type rune = int32
类型定义和类型别名的区别
类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
//类型定义
type NewInt int
//类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}
结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
结构体
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。 Go语言中通过struct来实现面向对象。
结构体的定义
使用type和struct关键字来定义结构体,具体代码格式如下:
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
- 1.类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 2.字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 3.字段类型:表示结构体字段的具体类型。
举个例子,我们定义一个Person(人)结构体,代码如下:
type person struct {
name string
city string
age int8
}
同样类型的字段也可以写在一行,
type person1 struct {
name, city string
age int8
}
这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。 语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型
结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。 结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。
var 结构体实例 结构体类型
基本实例化
type person struct {
name string
city string
age int8
}
func main() {
var p1 person
p1.name = "LiAng"
p1.city = "郑州"
p1.age = 22
fmt.Printf("p1=%v\n", p1) //p1={LiAng 郑州 22}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"LiAng", city:"郑州", age:22}
}
我们通过.
来访问结构体的字段(成员变量),例如p1.name和p1.age等。
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
func main() {
var user struct {
Name string
Age int
}
user.Name = "LiAng"
user.Age = 22
fmt.Printf("%#v\n", user) // struct { Name string; Age int }{Name:"LiAng", Age:22}
}
创建指针类型结构体
我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}
从打印的结果中我们可以看出p2是一个结构体指针。 需要注意的是在Go语言中支持对结构体指针直接使用.
来访问结构体的成员。
func main() {
var p2 = new(person)
p2.name = "开发"
p2.age = 22
p2.city = "郑州"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"开发", city:"郑州", age:22}
}
取结构体的地址实例化
使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
func main() {
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "博客"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"博客", city:"成都", age:30}
}
p3.name = “博客”其实在底层是(*p3).name = “博客”,这是Go语言帮我们实现的语法糖。
结构体初始化
type person struct {
name string
city string
age int8
}
func main() {
var p4 person
fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}
使用键值对初始化
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
func main() {
p5 := person{
name: "LiAng",
city: "郑州",
age: 18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"LiAng", city:"郑州", age:18}
}
也可以对结构体指针进行键值对初始化,例如:
func main() {
p6 := &person{
name: "LiAng",
city: "郑州",
age: 18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"LiAng", city:"郑州", age:18}
}
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
func main() {
p7 := &person{
city: "郑州",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"郑州", age:0}
}
使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
func main() {
p8 := &person{
"LiAng",
"郑州",
18,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"LiAng", city:"郑州", age:18}
}
使用这种格式初始化时,需要注意:
- 1.必须初始化结构体的所有字段。
- 2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 3.该方式不能和键值初始化方式混用。
结构体内存布局
type test struct {
a int8
b int8
c int8
d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
n.a 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063
构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}
func main() {
p9 := newPerson("pprof.cn", "测试", 90)
fmt.Printf("%#v\n", p9) // &main.person{name:"pprof.cn", city:"测试", age:90}
}
方法和接收者
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。 方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
- 1.接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
- 2.接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 3.方法名、参数列表、返回参数:具体格式与函数定义相同。
举个例子
//Person 结构体
type Person struct {
name string
age int8
}
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p1 := NewPerson("测试", 25)
p1.Dream() // 测试的梦想是学好Go语言!
}
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
指针类型的接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPerson("测试", 25)
fmt.Println(p1.age) // 25
p1.SetAge(30)
fmt.Println(p1.age) // 30
}
值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPerson("测试", 25)
p1.Dream()
fmt.Println(p1.age) // 25
p1.SetAge2(30) // (*p1).SetAge2(30)
fmt.Println(p1.age) // 25
}
什么时候应该使用指针类型接收者
- 1.需要修改接收者中的值
- 2.接收者是拷贝代价比较大的大对象
- 3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
// Person 结构体Person类型
type Person struct {
string
int
}
func main() {
p1 := Person{
"LiAng",
22,
}
fmt.Printf("%#v\n", p1) //main.Person{string:"LiAng", int:22}
fmt.Println(p1.string, p1.int) //LiAng 22
}
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
// Address 地址结构体
type Address struct {
Province string
City string
}
// User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "LiAng",
Gender: "男",
Address: Address{
Province: "河南",
City: "郑州",
},
}
// user1=main.User{Name:"LiAng", Gender:"男", Address:main.Address{Province:"河南", City:"郑州"}}
fmt.Printf("user1=%#v\n", user1)
}
嵌套匿名结构体
// Address 地址结构体
type Address struct {
Province string
City string
}
// User 用户结构体
type User struct {
Name string
Gender string
Address //匿名结构体
}
func main() {
var user2 User
user2.Name = "LiAng"
user2.Gender = "男"
user2.Address.Province = "河南省" //通过匿名结构体.字段名访问
user2.City = "郑州" // 直接访问匿名结构体的字段名
// user2=main.User{Name:"LiAng", Gender:"男", Address:main.Address{Province:"河南省", City:"郑州"}}
fmt.Printf("user2=%#v\n", user2)
}
当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
// Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}
// Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
// User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}
func main() {
var user3 User
user3.Name = "LiAng"
user3.Gender = "女"
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
user3.Address.CreateTime = "2001" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2023" //指定Email结构体中的CreateTime
}
结构体的“继承”
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
// Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s 会动! \n", a.name)
}
// Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s 会汪汪叫~ \n", d.name)
}
func main() {
d1 := Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() // 乐乐 会汪汪叫~
d1.move() // 乐乐 会动!
}
结构体字段的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""
包裹,使用冒号:
分隔,然后紧接着值
;多个键值之间使用英文,
分隔。
// Student 学生
type Student struct {
ID int
Gender string
Name string
}
// Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
json:{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男
","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu0
7"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}
&main.Class{Title:"101", Students:[]*main.Student{(*main.Student)(0xc00007e9f0), (*main.Student)(0xc00007ea20), (*main.Student)(0xc00007ea50), (*main.Student)(0xc0000
7ea80), (*main.Student)(0xc00007eae0), (*main.Student)(0xc00007eb10), (*main.Student)(0xc00007eb40), (*main.Student)(0xc00007eb70), (*main.Student)(0xc00007eba0), (*m
ain.Student)(0xc00007ebd0)}}
结构体标签(Tag)
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下: key1:"value1" key2:"value2"
结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。 例如我们为Student结构体的每个字段定义json序列化时使用的Tag:
// Student 学生
type Student struct {
ID int `json:"id"` // 通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "LiAng",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}
删除map类型的结构体
type student struct {
id int
name string
age int
}
func main() {
ce := make(map[int]student)
ce[1] = student{1, "xiaolizi", 22}
ce[2] = student{2, "wang", 23}
fmt.Println(ce) // map[1:{1 xiaolizi 22} 2:{2 wang 23}]
delete(ce, 2)
fmt.Println(ce) // map[1:{1 xiaolizi 22}]
}
实现map有序输出(面试经常问到)
func main() {
map1 := make(map[int]string, 5)
map1[1] = "www.topgoer.com"
map1[2] = "rpc.topgoer.com"
map1[5] = "ceshi"
map1[3] = "xiaohong"
map1[4] = "xiaohuang"
sli := []int{}
for k, _ := range map1 {
sli = append(sli, k)
}
fmt.Println()
sort.Ints(sli)
for i := 0; i < len(map1); i++ {
fmt.Println(map1[sli[i]])
}
}
www.topgoer.com
rpc.topgoer.com
xiaohong
xiaohuang
ceshi
小案例
采用切片类型的结构体接受查询数据库信息返回的参数 地址:https://github.com/lu569368/struct