文章目录
- 环境安装
- GoLand 安装
- GO基础
- GO特点
- 类型和函数
- Init函数和main函数
- GO命令
- 下划线
- 变量和常量
- 数组
- 切片Slice
- 引用
环境安装
下载地址:https://www.golangroadmap.com/
安装目录文件说明:
api:每个版本的 api 变更差异。
bin:go 源码包编译出的编译器(go)、文档工具(godoc)、格式化工具(gofmt)。
doc:英文版的 Go 文档。
lib:引用的一些库文件。
misc 杂项用途的文件,例如 Android 平台的编译、git 的提交钩子等。
pkg:Windows 平台编译好的中间文件。
src:标准库的源码。
test:测试用例。
检测是否安装成功:cmd输入命令go env
安装成功!
配置GOPATH环境:
高级系统设置=》环境变量=》新建系统变量
同时在path
里面添加go
的安装目录和GOPATH目录
在GOPATH目录下新建3个文件夹
- bin:用来存放编译后生成的可执行文件
- pkg:用来存放编译后生成的归档文件
- src:用来存放源码文件
GoLand 安装
IDE为GoLand JetBrains IDEs
下载地址:https://www.jetbrains.com/go/download
new project=》Edit Configuration=》Add New Configuration=》Go Build=》OK(无需填写其他)
Name:为本条配置信息的名称,可以自定义,也可以使用系统默认的值;
Output directory:用来设置编译后生成的可执行文件的存放目录,可以为空,为空时默认不生成可执行文件;
Working directory:用来设置程序的运行目录,不能为空。
测试运行GoLand默认生成的main.go
:
// 声明 main 包,表明当前是一个可执行程序
package main
import (
"fmt" // 导入一个系统包fmt用来输出的
)
// 创建函数 main函数 func 函数 main 函数的名字 () 没有参数
func main() {
s := "gopher"
fmt.Printf("Hello and welcome, %s!\n", s)
for i := 1; i <= 5; i++ {
fmt.Println("i =", 100/i)
}
}
Hello and welcome, gopher!
i = 100
i = 50
i = 33
i = 25
i = 20
Process finished with the exit code 0
运行成功,同时需要注意:在 Go 语言中,fmt.Println
是用于打印文本和变量的函数,但它并不支持像某些其他语言那样的格式化字符串。你使用的 %s
是 Go 语言的格式化占位符,但它只能在 fmt.Printf
或类似函数中使用。
在命令行执行,go代码跑起来的命令就是 go run后面跟go代码:go run \XXX\XXX.go
GO基础
GO特点
GO优点:
- 自带gc。
- 静态编译,编译好后,扔服务器直接运行。
- 简单的思想,没有继承,多态,类等。
- 丰富的库和详细的开发文档。
- 语法层支持并发,和拥有同步并发的channel类型,使并发开发变得非常方便。
- 简洁的语法,提高开发效率,同时提高代码的阅读性和可维护性。
- 超级简单的交叉编译,仅需更改环境变量。
- Go 语言是谷歌 2009 年首次推出并在 2012 年正式发布的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去10多年间软件开发的难度令人沮丧。Google 对 Go 寄予厚望,其设计是让软件充分发挥多核心处理器同步多工的优点,并可解决面向对象程序设计的麻烦。它具有现代的程序语言特色,如垃圾回收,帮助开发者处理琐碎但重要的内存管理问题。Go 的速度也非常快,几乎和 C 或 C++ 程序一样快,且能够快速开发应用程序。
Go语言的主要特征:
1.自动立即回收。
2.更丰富的内置类型。
3.函数多返回值。
4.错误处理。
5.匿名函数和闭包。
6.类型和接口。
7.并发编程。
8.反射。
9.语言交互性。
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
panic recover
可见性:
1)声明在函数内部,是函数的本地值,类似private
2)声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect
3)声明在函数外部且首字母大写是所有包可见的全局值,类似public
有四种主要声明方式:
var(声明变量), const(声明常量), type(声明类型) ,func(声明函数)
一个Go工程中主要包含以下三个目录:
- src:源代码文件
- pkg:包文件
- bin:相关bin文件
编译问题
1.系统编译时 go install abc_name时,系统会到GOPATH的src目录中寻找abc_name目录,然后编译其下的go文件;
2.同一个目录中所有的go文件的package声明必须相同,所以main方法要单独放一个文件,否则在eclipse和liteide中都会报错;
编译报错如下:(假设test目录中有个main.go 和mymath.go,其中main.go声明package为main,mymath.go声明packag 为test);can't load package: package test: found packages main (main.go) and test (mymath.go) in /home/wanjm/go/src/test
3.对于main方法,只能在bin目录下运行 go build path_tomain.go; 可以用-o参数指出输出文件名;
4.可以添加参数 go build -gcflags “-N -l” ****,可以更好的便于gdb;详细参见 http://golang.org/doc/gdb
5.gdb全局变量主一点。 如有全局变量 a;则应写为 p ‘main.a’;注意但引号不可少;
类型和函数
- 值类型:
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 -- 管道
类型 | 长度(字节) | 默认值 | 说明 |
---|---|---|---|
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。
关于复数:
complex64和complex128
复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。
关于字符串的常见操作:
方法 | 介绍 |
---|---|
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类型
uint8
类型,或者叫 byte 型,代表了ASCII码的一个字符。
rune
类型,代表一个 UTF-8字符。
第一个循环以字节为单位遍历字符串,而第二个循环以 Unicode 字符为单位遍历字符串。(UTF-8 编码中的每个中文字符通常占用 3 个字节,以字节为单位遍历,中文肯定会乱码)
// 遍历字符串
func traversalString() {
s := "hello world!你好"
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()
}
输出:
当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。
Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾
104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 119(w) 111(o) 114(r) 108(l) 100(d) 33(!) 228(ä) 189(½) 160( ) 229(å) 165(¥) 189(½)
104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 119(w) 111(o) 114(r) 108(l) 100(d) 33(!) 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[0] = '狗'
fmt.Println(string(runeS2))
}
类型转换
Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。
强制类型转换的基本语法如下:T(表达式)
其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等.
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)
}
- 内置函数
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()
函数体用{}一对括号包裹。
func main(){
//函数体
}
init
函数和main
函数的异同
相同点:
两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
不同点:
init可以应用于任意包中,且可以重复定义多个。
main函数只能用于main包中,且只能定义一个。
两个函数的执行顺序:
对同一个go文件的init()调用顺序是从上到下的。
对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数。
对于不同的package,如果不相互依赖的话,按照main包中”先import的后调用”的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。
如果init函数中使用了println()或者print()你会发现在执行过程中这两个不会按照你想象中的顺序执行。这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用。
GO命令
$ go
Go is a tool for managing Go source code.
Usage:
go command [arguments]
The commands are:
build compile packages and dependencies
clean remove object files
doc show documentation for package or symbol
env print Go environment information
bug start a bug report
fix run go tool fix on packages
fmt run gofmt on package sources
generate generate Go files by processing source
get download and install packages and dependencies
install compile and install packages and dependencies
list list packages
run compile and run Go program
test test packages
tool run specified go tool
version print Go version
vet run go tool vet on packages
Use "go help [command]" for more information about a command.
Additional help topics:
c calling between Go and C
buildmode description of build modes
filetype file types
gopath GOPATH environment variable
environment environment variables
importpath import path syntax
packages description of package lists
testflag description of testing flags
testfunc description of testing functions
Use "go help [topic]" for more information about that topic.
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
命令来交互式的访问概要文件的内容。
下划线
import 下划线
(如:import _ hello/imp
)的作用:当导入一个包时,该包下的文件里所有init()函数都会被执行,然而,有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。这个时候就可以使用 import _
引用该包。即使用【import _ 包路径
】只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。
示例:
对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数
demo.go
package foundation
import (
"fmt"
)
func init() {
fmt.Println("Init Function demo==1==")
}
main.go
package main
import (
_ "GoStudy/foundation"
"fmt"
)
func init() {
fmt.Println("Main init()")
}
func main() {
fmt.Println("hello world")
}
输出:
Init Function demo==1==
Init Function demo==2==
Init Function demo==3==
Main init()
hello world
Process finished with the exit code 0
变量和常量
- 变量声明:
var 变量名 变量类型
var name string
var age int
var isOk bool
// 批量声明
var (
a string
b int
c bool
d float32
)
- 变量初始化
(1)Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0
。 字符串变量的默认值为空字符串。 布尔型变量默认为false
。 切片、函数、指针变量的默认为nil
。
(2)声明变量,指定初始值var 变量名 类型 = 表达式
var name string = "hetongxue"
var sex int = 1
// 批量
var name, sex = "hetongxue", 1
(3)类型推导
// 编译器会根据等号右边的值来推导变量的类型完成初始化
var name = "hetongxue"
var sex = 1
- 短变量声明
在函数内部,可以使用更简略的 := 方式声明并初始化变量。
package main
import (
"fmt"
)
// 全局变量m
var m = 100
func main() {
n := 10
m := 200 // 此处声明局部变量m
fmt.Println(m, n)
}
函数外的每个语句都必须以关键字开始(var、const、func等)
:=不能使用在函数外。
_多用于占位,表示忽略值。
- 常量
把var
换成了const
,常量在定义的时候必须赋值
const pi = 3.1415
const e = 2.7182
// 一起声明
const (
pi = 3.1415
e = 2.7182
)
// const同时声明多个常量时,如果省略了值则表示和上面一行的值相同
const (
n1 = 100
n2
n3
)
声明了pi和e这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。
iota
iota
是go语言的常量计数器,只能在常量的表达式中使用。
iota
在const
关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)
// 跳过某个值
const (
n1 = iota //0
n2 //1
_
n4 //3
)
// 插队
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0
数组
- 数组:是同一种数据类型的固定长度的序列。
- 数组定义:
var a [len]int
,比如:var a [5]int
,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。 - 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。
- 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
for i := 0; i < len(a); i++ {
}
for index, v := range a {
}
- 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
- 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。【JAVA数组是引用类型,如果函数内部修改了数组的元素,这些修改会影响到原始数组。】
- 支持 “
==
”、“!=
” 操作符,因为内存总是被初始化过的。 - 指针数组
[n]*T
,数组指针*[n]T
。
参考代码理解:
package main
import "fmt"
func changeArray(x []int) {
//遍历数组
for i := 0; i < len(x); i++ {
fmt.Println(x[i])
}
// 遍历数组
for index, value := range x {
fmt.Println(index, value)
}
// 修改数组的值
for index, value := range x {
x[index] = value + 1
}
}
func main() {
var x = [5]int{1, 2, 3, 5, 7}
changeArray(x[:])
fmt.Println(x) // [2 3 4 6 8]
}
需要注意的点:
changeArray
函数传入的切片,(切片是引用类型,函数修改时会影响原本的值的),这里相当于重新定义了一个切片x[:]
传到了该函数,原本数组的值发生了改变。- 其次注意全局变量和局部变量的声明方式。局部变量声明:正确的写法是直接使用 :=,不需要 var 关键字。
全局:
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"}
局部:
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}, // 别忘了最后一行的逗号。
}
多维
全局
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
局部:
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
传入数组指针,传入的是引用,地址,会影响原本的值。
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 [10]int) int {
var sum int = 0
for i := 0; i < len(a); i++ {
sum += a[i]
}
return sum
}
func main() {
// 若想做一个真正的随机数,要种子
// seed()种子默认是1
//rand.Seed(1)v 已弃用
//rand.Seed(time.Now().Unix())
rand.New(rand.NewSource(time.Now().Unix()))
var b [10]int
for i := 0; i < len(b); i++ {
// 产生一个0到1000随机数
b[i] = rand.Intn(1000)
}
fmt.Println(b)
sum := sumArr(b)
fmt.Printf("sum=%d\n", sum)
}
找出数组中和为给定值的两个元素的下标,例如数组[1,3,5,8,7],找出两个元素之和等于8的下标分别是(0,4)和(1,2)
package main
import "fmt"
// 找出数组中和为给定值的两个元素的下标,例如数组[1,3,5,8,7],找出两个元素之和等于8的下标分别是(0,4)和(1,2)
func sumTarget(x []int, target int) {
for i := 0; i < len(x); i++ {
for j := i + 1; j < len(x); j++ {
if x[i]+x[j] == target {
fmt.Printf("(%d, %d)\n", i, j)
}
}
}
}
func main() {
b := [5]int{1, 3, 5, 8, 7}
sumTarget(b[:], 8)
}
切片Slice
值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。
切片的创建:
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] //去掉切片的最后一个元素
注意
在函数内部,无论是使用 var
还是 :=
声明的变量都是局部变量,仅在该函数体内有效。如果需要定义全局变量,应该在包级别进行定义,而不是在函数内部。使用 var
和 :=
的主要区别在于声明方式和类型推断,而可见性和作用域是相同的。
在 Go 语言中,切片(slice)的内存布局包含三个主要字段:指针(ptr
)、长度(len
)和容量(cap
)。
- 指针(ptr):
ptr
是指向切片底层数组的指针。切片本质上是一个对数组的引用,指针指向数组中第一个元素的地址。 - 长度(len):
len
表示切片当前包含的元素数量,也就是切片的实际长度。这个值是动态的,可以随着切片的变化而变化。通过len(slice)
函数可以获取切片的长度。 - 容量(cap):
cap
表示切片的容量,即底层数组中从切片的开始位置到数组末尾的元素数量。这是切片可以使用的最大元素数量,不会随切片的大小变化而变化,直到底层数组的大小不够为止。通过cap(slice)
函数可以获取切片的容量。
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // 切片包含 arr[1], arr[2], arr[3]
此时,切片的内存布局如下:
ptr
指向 arr 中的元素 2(即 arr[1])len
为 3(切片当前包含的元素为 2, 3, 4)cap
为 4(从 arr[1] 到 arr[4] 的元素数量)- 切片的容量是由其起始点到原数组末尾的距离决定的,而不是由切片当前包含的元素数量决定的。
len
和 cap
的区别
len:
- 表示切片当前包含的元素数量。
- 当切片进行追加(append)操作时,如果切片的长度小于容量,可以直接增加长度;如果切片的长度等于容量,底层数组需要扩展,新的底层数组将被分配,len 会增加,但 cap 也可能会增加。
cap:
- 表示切片的最大容量,是切片可以容纳的元素数量,不会随着切片长度的增加而自动改变,直到底层数组不够大。
- 可以通过切片操作(如切片扩展、追加元素)改变,但只在底层数组重新分配时。
package main
import (
"fmt"
)
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // slice = [2, 3, 4]
fmt.Println("slice:", slice) // 输出 [2 3 4]
fmt.Println("len(slice):", len(slice)) // 输出 3
fmt.Println("cap(slice):", cap(slice)) // 输出 4
slice = append(slice, 6) // 尝试添加元素
fmt.Println("after append:", slice) // 输出 [2 3 4 6]
fmt.Println("len(slice):", len(slice)) // 输出 4
fmt.Println("cap(slice):", cap(slice)) // cap 可能增加 == 4
}
通过make来创建切片 var slice []type = make([]type, len)
、slice := make([]type, len)
、slice := make([]type, len, cap)
var slice0 []int = make([]int, 10) // [0 0 0 0 0 0 0 0 0 0]
var slice1 = make([]int, 10) // [0 0 0 0 0 0 0 0 0 0]
var slice2 = make([]int, 10, 10) // [0 0 0 0 0 0 0 0 0 0]
切片和数组
package main
import "fmt"
func main() {
s1 := []int{0, 1, 2, 3, 8: 100}
arr := [...]int{0, 1, 2, 3, 8: 100}
fmt.Println("s1:", s1)
fmt.Println("Type of s1:", fmt.Sprintf("%T", s1))
fmt.Println("arr:", arr)
fmt.Println("Type of arr:", fmt.Sprintf("%T", arr))
}
数组指针:
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3}
p := &s[2] // *int, 获取底层数组元素指针。
*p += 100
fmt.Println(s) // [0 1 102 3]
}
改变切片影响原数组的值:
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) //[102 203]
fmt.Println(data) // [0 1 102 203 4 5]
}
用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]
*/
超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满。
package main
import (
"fmt"
)
func main() {
data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3] // slice[start:end:cap]
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。===>相同
s[0] = 78
fmt.Println(s, data) // 没有重新分配底层数组,与原数组相关。
s = append(s, 100, 200) // 一次 append 两个值,超出 s.cap 限制。
s[0] = 67
fmt.Println(s, data) // 重新分配底层数组,与原数组无关。
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。
}
/*
0xc0000760c0 0xc0000760c0
[78 1] [78 1 2 3 4 0 0 0 0 0 0]
[67 1 100 200] [78 1 2 3 4 0 0 0 0 0 0]
0xc00000e2d0 0xc0000760c0
*/
从输出结果可以看出,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]
*/
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("index : %v , value : %v\n", index, value)
}
}
/*
index : 0 , value : 0
index : 1 , value : 1
index : 2 , value : 2
index : 3 , value : 3
index : 4 , value : 4
index : 5 , value : 5
index : 6 , value : 6
index : 7 , value : 7
index : 8 , value : 8
index : 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)) // slice a : [1 3 4 5] , len(a) : 4
b := a[1:2]
b[0] = 100
fmt.Printf("slice b : %v , len(b) : %v\n", b, len(b)) // slice b : [100] , len(b) : 1
fmt.Println(a) // [1 100 4 5]
c := b[0:3]
c[0] = 200
fmt.Printf("slice c : %v , len(c) : %v\n", c, len(c)) // slice c : [200 4 5] , len(c) : 3
fmt.Println(a) // [1 200 4 5]
fmt.Println(b) // [200]
fmt.Printf("cap === a : %v ,b : %v , c : %v\n", cap(a), cap(b), cap(c)) // cap === a : 4 ,b : 3 , c : 3
}
切片共用一个底层的指针数组ptr
字符串和切片(string and slice):
string底层就是一个byte的数组,因此,也可以进行切片操作。
package main
import (
"fmt"
)
func main() {
str := "hello world"
s1 := str[0:5]
fmt.Println(s1) // hello
s2 := str[6:]
fmt.Println(s2) // 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!
}
数组or切片转字符串:
strings.Replace(strings.Trim(fmt.Sprint(array_or_slice), "[]"), " ", ",", -1)
引用
- https://www.runoob.com/go/go-tutorial.html
- https://www.topgoer.cn/
- https://gopl-zh.github.io