Go语言学习笔记
学习自菜鸟教程
文章目录
编译运行Go程序
// 要执行 Go 语言代码可以使用 go run 命令。
$ go run hello.go
Hello, World!
// 此外我们还可以使用 go build 命令来生成二进制文件:
$ go build hello.go
$ ls
hello hello.go
$ ./hello
Hello, World!
Go 语言结构
Go 语言的基础组成有以下几个部分:
- 声明
- 引入包
- 函数
- 变量
- 语句 & 表达式
- 注释
Go 程序结构组成关系 应用程序->文件夹->文件->包->函数
Go应用程序包含多个包,其中必有一个名为main的包,也只有此包可以包含 main 函数
main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有init函数则会先执行该函数)
文件夹包含文件,文件夹和文件和包名没有任何关系,同意文件夹内的所有文件只能有一个包名
包包含多个文件,文件通过package 包名,说明隶属于哪个包,通过import 包名,说明引用哪个包
Go 语言基础语法
Go标记
Go程序由标记组成,或者说程序由字母组成,标记可以是关键字,标识符,常量,字符串,符号
功能角度讲,程序一般由关键字、常量、变量、运算符、类型、分隔符、标点符号和函数组成。
行分隔符
在 Go 程序中,一行代表一个语句结束
注释
同C++/C
标识符
标识符用来命名变量、类型等程序实体
字符串连接
+
关键字
关键字,保留字,预定义标识符
Go语言数据类型
在 Go 编程语言中,数据类型用于声明函数和变量。
数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。
几种数据类型
布尔型,数字类型,字符串类型,派生类型
其中派生类型包含,
(a) 指针类型(Pointer)
(b) 数组类型
© 结构化类型(struct)
(d) Channel 类型
(e) 函数类型
(f) 切片类型
(g) 接口类型(interface)
(h) Map 类型
Go语言变量
变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念。
变量可以通过变量名访问
三种变量声明
// 1. 指定变量类型,如果没有初始化,则变量默认为零值
var v_name v_type
v_name = value
/*
零值就是变量没有做初始化时系统默认设置的值
数值类型(包括complex64/128)为 0
布尔类型为 false
字符串为 ""(空字符串)
以下几种类型为 nil:
*/
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error 是接口
// 2. 根据值自行判定变量类型
var v_name = value
// 3. 省略 var, 注意 := 左侧如果没有声明新的变量,就产生编译错误
v_name := value
a := 50是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 := 可以高效地创建一个新的变量,称之为初始化声明。
多变量声明
//类型相同多个变量, 非全局变量
var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3
var vname1, vname2, vname3 = v1, v2, v3 // 和 python 很像,不需要显示声明类型,自动推断
vname1, vname2, vname3 := v1, v2, v3 // 出现在 := 左侧的变量不应该是已经被声明过的,否则会导致编译错误
// 这种因式分解关键字的写法一般用于声明全局变量
var (
vname1 v_type1
vname2 v_type2
)
并行赋值
如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a,两个变量的类型必须是相同。
空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。
_ 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。
并行赋值也被用于当一个函数返回多个返回值时,比如这里的 val 和错误 err 是通过调用 Func1 函数同时得到:val, err = Func1(var1)。
变量注意事项
-
如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20 就是不被允许的,编译器会提示错误 no new variables on left side of :=,但是 a = 20 是可以的,因为这是给相同的变量赋予一个新的值。
-
如果你在定义变量 a 之前使用它,则会得到编译错误 undefined: a。
-
如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误。但是全局变量是允许声明但不使用。
Go语言常量
常量是一个简单值的标识符,在程序运行时,不会被修改的量。
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型
常量的定义
显示类型定义、隐式类型定义、多个同类型定义
const identifier [type] = value
const identifier = value
const c_name1, c_name2 = value1, value2
常量可以用len(), cap(),
unsafe.Sizeof()函数计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过:
package main
import "unsafe"
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)
func main(){
println(a, b, c)
}
abc 3 16
字符串类型在 go 里是个结构, 包含指向底层数组的指针和长度,这两部分每部分都是 8 个字节,所以字符串类型大小为 16 个字节。
iota 特殊常量
iota 只是在同一个 const 常量组内逐行递增,不管是否显式使用,每当有新的 const 关键字时,iota 计数会重新开始
可以认为是一个可以被编译器修改的常量
package main
import "fmt"
func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}
常量组的定义
在定义常量组时,如果不提供初始值,则表示将使用上行的表达式。
iota 只是在同一个 const 常量组内递增,每当有新的 const 关键字时,iota 计数会重新开始
Go 语言运算符
运算符用于在程序运行时执行数学或逻辑运算。
Go 语言内置的运算符有:
算术运算符
关系运算符
逻辑运算符
位运算符
赋值运算符
其他运算符 &*
运算符优先级
有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:
优先级 | 运算符 * / % << >> & &^ + - | ^ == != < <= > >= && || |
---|---|
5 | * / % << >> & &^ |
4 | + - |
3 | == != < <= > >= |
2 | && |
1 | || |
怎么记,赋值运算符基本属于最底层,只大于&&和||,之上加减|^是一类,再往上*/&等是一类
指针变量 * 和地址值 & 的区别:指针变量保存的是一个地址值,会分配独立的内存来存储一个整型数字。当变量前面有 * 标识时,才等同于 & 的用法,否则会直接输出一个整型数字
func main() {
var a int = 4
var ptr *int
ptr = &a
println("a的值为", a); // 4
println("*ptr为", *ptr); // 4
println("ptr为", ptr); // 824633794744
}
Go 的自增,自减只能作为表达式使用,而不能用于赋值语句。
a++ // 这是允许的,类似 a = a + 1,结果与 a++ 相同
a-- //与 a++ 相似
a = a++ // 这是不允许的,会出现变异错误 syntax error: unexpected ++ at end of statement
Go 语言条件语句
Go 没有三目运算符,所以不支持 ?: 形式的条件判断
五种条件判断语句
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
} else {
/* 在布尔表达式为 false 时执行 */
}
if 布尔表达式 1 {
/* 在布尔表达式 1 为 true 时执行 */
if 布尔表达式 2 {
/* 在布尔表达式 2 为 true 时执行 */
}
}
switch
\1. switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止
\2. switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough
变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。
switch var1 {
case val1:
...
case val2:
...
case val3, val4, val5:
...
default:
...
}
type-switch
switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
switch x.(type){
case type:
statement(s);
case type:
statement(s);
/* 你可以定义任意个数的case */
default: /* 可选 */
statement(s);
}
package main
import "fmt"
func main() {
var x interface{}
switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T",i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型" )
default:
fmt.Printf("未知型")
}
}
fallthrough
使用 fallthrough 会强制执行后面的一条 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。
break和fallthrough技巧
switch{
case 1:
...
if(...){
break
}
fallthrough // 此时switch(1)会执行case1和case2,但是如果满足if条件,则只执行case1
case 2:
...
case 3:
}
Go 语言循环语句
两种循环类型
for
// 和 C 语言的 for 一样:
for init; condition; post { }
// 和 C 的 while 一样:
for condition { }
// 和 C 的 for(;;) 一样:
for { }
// for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap {
newMap[key] = value
}
循环嵌套
for [condition | ( init; condition; increment ) | Range]
{
for [condition | ( init; condition; increment ) | Range]
{
statement(s);
}
statement(s);
}
三种循环控制语句
break
continue
Go 语言的 continue 语句 有点像 break 语句。但是 continue 不是跳出循环,而是跳过当前循环执行下一次循环语句。或者说执行 continue 语句会触发for增量语句的执行。
goto
Go 语言的 goto 语句可以无条件地转移到过程中指定的行。
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 10
/* 循环 */
LOOP: for a < 20 {
if a == 15 {
/* 跳过迭代 */
a = a + 1
goto LOOP
}
fmt.Printf("a的值为 : %d\n", a)
a++
}
}
Go 语言函数
函数定义
func function_name( [parameter list] ) [return_types] {
函数体
}
func max(num1, num2 int) int {
/* 声明局部变量 */
var result int
if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}
func swap(x, y string) (string, string) {
return y, x
}
函数的创建和调用
func max(num1, num2 int) int {
/* 定义局部变量 */
var result int
if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}
func main(){
/* 1. 直接使用函数实体 */
/* 每次使用max代表创建新的函数实体 */
fmt.Println(max(1,2))
/* 2. MaxOne 是一个函数,而不是max()函数的返回值result */
/* 每次使用MaxOne代表的都是同一个函数实体,在下面的闭包代码可以看到区别*/
/* 下面哪种正确,问题来自,下面的 闭包携带参数 */
MaxOne := max()
fmt.Println(MaxOne(1,2))
MaxOne := max(1,2)
fmt.Println(MaxOne())
/* 3. 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
/* 使用函数 */
fmt.Println(getSquareRoot(9))
}
函数参数
函数如果使用参数,该变量可称为函数的形参。
形参就像定义在函数体内的局部变量。
调用函数,可以通过两种方式来传递参数
\1. 值传递:值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
\2. 引用传递:引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
两者的区别就在于,1,函数调用时,传递的是变量的地址 同时2. 函数参数列表使用的是指针类型
函数用法
\1. 函数作为另外一个函数的实参
Go 语言可以很灵活的创建函数,并作为另外一个函数的实参。
func main(){
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
/* 使用函数 */
fmt.Println(getSquareRoot(9))
}
\1. 闭包
Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必声明。
以下实例中,我们创建了函数 getSequence() ,返回另外一个函数。该函数的目的是在闭包中递增 i 变量
还是看代码吧,实践中理解这样写的作用
package main
import "fmt"
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
func main(){
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber()) // 1
fmt.Println(nextNumber()) // 2
fmt.Println(nextNumber()) // 3
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1()) // 1
fmt.Println(nextNumber1()) // 2
}
闭包携带参数
package main
import "fmt"
func main() {
add_func := add(1,2)
fmt.Println(add_func())
fmt.Println(add_func())
fmt.Println(add_func())
}
// 闭包使用方法
func add(x1, x2 int) func()(int,int) {
i := 0
return func() (int,int){
i++
return i,x1+x2
}
}
func main() {
add_func := add(1,2)
fmt.Println(add_func(1,1))
fmt.Println(add_func(0,0))
fmt.Println(add_func(2,2))
}
// 闭包使用方法
func add(x1, x2 int) func(x3 int,x4 int)(int,int,int) {
i := 0
return func(x3 int,x4 int) (int,int,int){
i++
return i,x1+x2,x3+x4
}
}
\1. 方法
Go 语言中同时有函数和方法
一个方法就是一个包含了接受者的函数
接受者可以是自定义类型(命名类型或者结构体类型)的一个值或者是一个指针。
所有给定类型的方法属于该类型的方法集。就像C++中类的方法一样
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
package main
import (
"fmt"
)
/* 定义结构体 */
type Circle struct {
radius float64
}
func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
C++与Go在方法上的区别和联系
C++ 等语言中,实现类的方法做法都是编译器隐式的给函数加一个 this 指针,而在 Go 里,这个 this 指针需要明确的申明出来,其实和其它 OO 语言并没有很大的区别。
在 C++ 中是这样的:
// c++
class Circle {
public:
float getArea() {
return 3.14 * radius * radius;
}
private:
float radius;
}
// 其中 getArea 经过编译器处理大致变为
float getArea(Circle *const c) {
...
}
// go
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
Go 语言变量作用域
Go 语言中变量可以在三个地方声明:
· 函数内定义的变量称为局部变量
· 函数外定义的变量称为全局变量
· 函数定义中的变量称为形式参数
局部变量
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,(形式参数)参数和返回值变量也是局部变量。
全局变量
package main
import "fmt"
/* 声明全局变量 */
var g int
func main() {
}
在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用
Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑
Go 语言数组
声明数组
var variable_name [SIZE] variable_type
初始化数组
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// 初始化数组中 {} 中的元素个数不能大于 [] 中的数字
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var balance = [5] int {1,2} // 1,2,0,0,0
// 在初始化时没有指定初值的元素将会赋值为其元素类型 int 的默认值0,string 的默认值是 ""。
var balance = [...] int {2:1,4:3} // 0,0,1,0,3
访问数组元素
数组更多用法
\1. 多维数组
var variable_name [SIZE][SIZE2]...[SIZEN] variable_type
初始化二维数组
a = [3][4]int{
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11}, /* 第三行索引为 2 */
}
// 以上代码中倒数第二行的 } 必须要有逗号,因为最后一行的 } 不能单独一行,也可以写成这样:
a = [3][4]int{
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11}} /* 第三行索引为 2 */
\1. 向函数传递数组
如果你想向函数传递数组参数,你需要在函数定义时,声明形参为数组,我们可以通过以下两种方式来声明
void myFunction(param [10]int)
{...}
void myFunction(param []int)
{...}
Go 语言指针
指针
变量是一种使用方便的占位符,用于引用计算机内存地址
一个指针变量指向了一个值的内存地址。类似于变量和常量,在使用指针前你需要声明指针。
总体来说指针是一种变量,这种变量的值是地址值,当然不是无意义的地址值,而是某种类型某种值的地址值,同样的指针可以赋值,和一般变量赋值没什么不同,但是赋的值是变量的地址值&var。比其他变量多的一点是可以可以访问指针指向的值(那块地址的值),有个*运算,同时可以改变那块地址存储的值,*var = 20
var var_name *var-type
使用指针
· 定义指针变量。
· 为指针变量赋值。
· 访问指针变量中指向地址的值
package main
import "fmt"
func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}
空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil。nil 指针也称为空指针。nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
一个指针变量通常缩写为 ptr
package main
import "fmt"
func main() {
var ptr *int
fmt.Printf("ptr 的值为 : %x\n", ptr ) // 0
}
if(ptr != nil) /* ptr 不是空指针 */
指针更多用法
\1. Go 语言指针数组
var ptr [MAX]*int;
\1. Go 语言指向指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。
当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址
var ptr **int;
\1. Go 语言指针作为函数参数
Go 语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可
Go 语言结构体
Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
定义结构体
type struct_variable_type struct {
member definition;
member definition;
...
member definition;
}
variable_name := structure_variable_type {value1, value2...valuen}
或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
// 创建一个新的结构体
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})
// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
}
访问结构体成员
\1. 通过 struct 变量.成员 变量来访问。
\2. 通过 struct 指针.成员 变量来访问。
结构体作为函数参数
同其他类型变量一样
结构体指针
注意和C++结构体指针访问成员变量的区别
Go 语言切片(Slice)
Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大
切片,实际的是获取数组的某一部分,len切片<=cap切片<=len数组,切片由三部分组成:指向底层数组的指针、len、cap
定义切片/声明切片
var identifier []type
var slice1 []type = make([]T, length, capacity) // 这里 len 是数组的长度并且也是切片的初始长度,也可以指定容量,其中capacity为可选参数
// 注意定义切片的时候,长度不能写在[]里,[]代表的是切片类型,而数组[]里可以写数字or…表示数组长度
创建切片/初始化切片
s := [] int {1,2,3}
// 直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s := [3] int {1,2,3}
s := […] int {1,2,3}
// 以上两者都是数组的初始化,和切片分清
s := arr[:]
// 初始化切片s,是数组arr的引用
s := arr[startIndex:endIndex]
// 将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:]
// 默认 endIndex 时将表示一直到arr的最后一个元素
s := arr[:endIndex]
// 默认 startIndex 时将表示从arr的第一个元素开始
s1 := s[startIndex:endIndex]
// 通过切片s初始化切片s1
s := make([]int, len, cap)
// 通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
len和cap函数
切片是可索引的,并且可以由 len() 方法获取长度。
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
package main
import "fmt"
func main() {
var numbers = make([]int,3,5)
printSlice(numbers)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
// len=3 cap=5 slice=[0 0 0]
}
空切片
一个切片在未初始化之前默认为 nil,长度为 0
var numbers []int
// len=0 cap=0 slice=[]
切片截取
可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound]
表示索引lower-bound/0 到 upper-bound – 1/max-1
append和 copy函数
增加元素append,前提是容量范围内
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
/* 允许追加空切片 */
numbers = append(numbers, 0)
printSlice(numbers)
/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers)
/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers)
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
/*
len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]
*/
切片其他问题
\1. 我们基于原数组或者切片创建一个新的切片后,那么新的切片的大小和容量是多少呢?
这里有个公式,对于底层数组容量是 k 的切片 slice[i:j] 来说:长度: j-i,容量: k-i
\2. 切片是引用传递,数组是值传递,在函数调用时
\3. 使用 copy 函数要注意对于 copy(dst, src),要初始化 dst 的 size,否则无法复制
\4. 切片内部结构:
第一个字段表示 array 的指针,是真实数据的指针第二个是表示 slice 的长度,第三个是表示 slice 的容量。
所以 unsafe.Sizeof(切片)永远都是 24。
当把 slice 作为参数,本身传递的是值,但其内容就 byte* array,实际传递的是引用,所以可以在函数内部修改,但如果对 slice 本身做 append,而且导致 slice 进行了扩容,实际扩容的是函数内复制的一份切片,对于函数外面的切片没有变化。
struct Slice
{
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
func slice_value(slice_test []int) {
slice_test[1] = 100 // 函数外的slice确实有被修改
slice_test = append(slice_test, 6) // 函数外的不变
fmt.Printf("slice_value:%#v,%#v,%#v\n", slice_test, len(slice_test), cap(slice_test))
}
func slice_ptr(slice_test *[]int) { // 这样才能修改函数外的slice
*slice_test = append(*slice_test, 7)
fmt.Printf("slice_ptr:%#v,%#v,%#v\n", *slice_test, len(*slice_test), cap(*slice_test))
}
5. slice 的底层是数组指针,所以 slice a 和 s 指向的是同一个底层数组,所以当修改 s[0] 时,a 也会被修改
package main
import "fmt"
func main() {
s := []int{1, 2, 3} // len=3, cap=3
a := s
s[0] = 888
s = append(s, 4)
fmt.Println(a, len(a), cap(a)) // 输出:[888 2 3] 3 3
fmt.Println(s, len(s), cap(s)) // 输出:[888 2 3 4] 4 6
}