环境变量:
GOPATH:
window下默认值路径为%USERPROFILE%/go,可以删掉新建,然后所有的项目代码放在src子目录下
GOPATH路径下有三个目录src pkg bin
具体的子代码放在src/xxx/xxx.go,这样就可以go mod init了
GOROOT:
是我们安装go开发包的路径
默认情况下 GOROOT下的bin目录及GOPATH下的bin目录都已经添加到环境变量中,我们可能需要修改对应的path为自定义GOPATH/bin
Go1.14版本之后,都推荐使用go mod模式来管理依赖环境了,不再强制把代码必须写在GOPATH下面的src目录
在任意路径go mod init即可
项目目录结构:
个人开发:
GOPATH/src/项目1/模块A
流行的项目结构:
GOPATH/src/github.com/czl/项目1/模块A
GOPATH/src/golang.org/czl/项目1/模块B
项目的具体模块划分:
api:
configs
database
docs
middleware
model
repository
router
service
utils
client
cmd
config
scripts
build
utils
企业:
GOPATH/src/code.xxx.com/前端组/项目1/模块A
基础知识:
1.变量定义后必须使用
var age int;
如果没有赋值,则是零值
场景:
推荐使用较多
2.省略类型:
变量定义并初始化时,可以省略掉type,自动推断 var name = "string"
场景:
使用得很少,除非同时声明多个变量。
3.简短声明:
:= 是一个声明语句,左侧如果没有声明新的变量,就产生编译错误
简短变量声明语句中必须至少要声明一个新的变量
场景:
只能用在一个函数内部,而package级别的变量不应该这么做
4.匿名变量:
_ := func(),避免必须用到这个值
5.声明并初始化:
var age int = 29
a :=[3]int {1,2,3} # 不用使用new和make来创建了
a := []xxx{yyy} # 可以直接使用[]type{type{value1,value2},type{value1,value2}}这种声明并初始化的方式
6.编码风格:
换行:
不需要分号作为语句或者声明结束,除非要在一行中将多个语句、声明隔开
在编译时,编译器会主动在一些特定的符号(译注:比如行末是,一个标识符、一个整数、浮点数、虚数、字符或字符串文字、关键字break、continue、fallthrough或return中的一个、运算符和分隔符++、--、)、]或}中的一个) 后添加分号,所以在哪里加分号合适是取决于Go语言代码的。
go语言编译器会自动在以标识符、数字字面量、字母字面量、字符串字面量、特定的关键字(break、continue、fallthrough和return)、增减操作符(++和--)、或者一个右括号、右方括号和右大括号(即)、]、})结束的非空行的末尾自动加上分号。
所以,要注意多行的写法问题,比如下面的写法是不对的。
x := []int{
1, 2, 3,
4, 5, 6
}
gofmt工具进行格式化:
手动运行:go fmt xxx.go
goimports:
会自动地添加你代码里需要用到的import声明以及需要移除的import声明。
go get golang.org/x/tools/cmd/goimports
许多编辑器都可以集成goimports工具,然后在保存文件的时候自动运行。
go vet工具:
会做代码静态检查发现可能的bug或者可疑的构造。vet是Go tool套件的一部分
7.注释:
//
多行注释/* ... */
8.命名:
名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的.
包本身的名字一般总是用小写字母。
推荐使用 驼峰式 命名
9.作用域:
短声明:
不可在package作用域内使用
局部的短声明变量将屏蔽外部的声明
var cwd string
func init(){cwd, err := os.Getwd()}
语法块:由花括弧所包含的一系列语句
全局语法块
包语法决
每个for、if和switch语句的语法决,显式的部分是for的循环体部分词法域,另外一个隐式的部分则是循环的初始化部分
每个switch或select的分支也有独立的语法决
显式书写的语法块
控制流标号:
就是break、continue或goto语句后面跟着的那种标号,则是函数级的作用域。
次序:
在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。
10.生命周期:
对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,在局部变量的声明周期则是动态的:从每次创建一个新变量的声明语句开始,直到该变量不再被引用为止
11.元素赋值:
map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功
v, ok = m[key]
v, ok = x.(T)
v, ok = <-ch
并不一定是产生两个结果,也可能只产生一个结果。对于值产生一个结果的情形,map查找失败时会返回零值,类型断言失败时会发送运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)。
v = m[key] // map查找,失败时返回零值
v = x.(T) // type断言,失败时panic异常
v = <-ch // 管道接收,失败时返回零值(阻塞不算是失败)
_, ok = m[key] // map返回2个值
_, ok = mm[""], false // map返回1个值
_ = mm[""] // map返回1个值
new和make:
概述:
new和make都是用来内存分配的原语。
make:
定义:
make用于引用类型的初始化,申请堆内存空间,如slice map channel
但是接口(多态,指向子类实现)、指针、函数除外(引用类型,依据:可以是nil)
示例:
slice := make([]int, 0, 100)
hash := make(map[int]bool, 10)
ch := make(chan int, 5)
参数传递:
作为参数类型时,传递的是指针的值,相当于传了引用
栈引用指向堆内存空间
如何回收?
指向nil即可0号堆内存
new:
定义:
分配一片内存空间并返回指向这片内存空间的指针,用于对象struct
对于值类型:
new函数使用常见相对比较少,因为对应结构体来说,可以直接用字面量语法创建新变量的方法会更灵活
var a = new(int) 得到不为nil的指针,稍后即可*a = 3来使用。
不等同于var a *int,该写法得到的是指针类型,值为nil。
对比var a int,得到的是零值
对于引用类型:
new可以用于引用类型,返回的是申请内存空间后的对应类型的非nil指针,是可以直接使用len和cap。
对于slice和map,返回的是nil slice和nil map,申请内存长度为0,cap为0
区别:
var a *[]int返回的是空指针,可以append,例如*list = append(*list, 1)
var a []int返回的是空slice(零值),内存地址为空0x0,两种写法都可以len,cap,只是第一种写法需要取值*
等价于:
var f *[]int
var h []int
f = &h
对于channel,new之后返回的是nil channel,读写会阻塞,panic错误为deadlock!
示例:
c := new(Person)
c = &Person{"xuxiaofeng",26}
相当于
var c *Person # 看起来两者没区别,但 new(T) 返回 T 的指针 *T 并指向 T 的零值。注意是返回指针(区别所在)
c = &Person{"xuxiaofeng",26}
数据类型:
概述:
基础类型、复合类型、引用类型和接口类型
基础类型:
数字、字符串和布尔型
复合数据类型:
数组和结构体
引用类型:
指针、切片、字典、函数、通道,对程序中一个变量或状态的间接引用
接口类型:
bool:
int uinit:
int8、int16、int32和int64 1 Byte = 8 Bits,分别占据8\16\32\64bits,占据1\2\4\8bytes
uint8、uint16、uint32和uint64
还有两种一般对应特定CPU平台机器字大小的有符号和无符号整数int和uint,32或64bit,因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。
整数环绕
比较大的数可以用uint64\float64\big包setstring
示例:d := new(big.Int) d.SetString("240000",10)10进制
尽管Go语言提供了无符号数和运算,即使数值本身不可能出现负数我们还是倾向于使用有符号的int类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。
无符号类型i>= 0则永远为真
出于这个原因,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。
float:
float32和float64
浮点数的范围极限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大数值
一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;
通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大。
很小或很大的数最好用科学计数法书写,通过e或E来指定指数部分:
math.IsNaN用于测试一个数是否是非数NaN,math.NaN和任何数都是不相等的
复数:
complex64和complex128
内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部
var x complex128 = complex(1, 2)
fmt.Println(real(x*y))
x := 1 + 2i
uintptr:
一种无符号的整数类型,没有指定具体的bit大小但是足以容纳指针。
字符byte:
写法:
单引号
字符串字面值(包含转义字符)
类型:
byte:代表了ASCII码的一个字符,uint8的别名,变长字节。
rune:unicode字符,采用4个字节存储,utf8.RuneCountInString()适合统计多字节的字符的长度,int32的一个类型别名。
需要注意的是对于非ASCII,索引更新的步长将超过1个字节。
Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。不然需要r, size :=utf8.DecodeRuneInString(s[i:])取出字节的长度
错误的UTF8编码输入生成一个特别的Unicode字符'\uFFFD'
UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。
r := []rune(s) # 解码为Unicode字符序列
string(r) # UTF8编码
默认字符推断类型
string:
写法:
双引号""
反引号``:转义字符,也可以用于定义多行字符串
len函数:
内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目),对于非ASCII字符的UTF8编码会要两个或多个字节
常量池:
需要自己用map实现,不像java,string interning(字符串驻留)是内置模式。
标准库:
bytes参数是[]byte类型,还提供了Buffer类型用于字节slice的缓存,bytes.Buffer的WriteRune/WriteByte
strings提供了Contains、Compare等方法
strconv提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
strconv.Itoa(“整数到ASCII”)
FormatInt和FormatUint函数可以用不同的进制来格式化数字
Sprintf的%b、%d、%o和%x等参数提供功能更强大
strconv包的Atoi或ParseInt、ParseUint
unicode包,提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,参数为rune类型
path和path/filepath包提供了关于文件路径名更一般的函数操作
索引:
"sss"[0]返回的是字节uint8
[0:]返回子字符串
遍历:
for _,one := range "ssss"{}返回的也是字节
不变性:
不变性意味如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。
修改:
转为[]byte,完成后再转为string
如何转?类型转换:[]byte("test")
format:
%c 打印字符
%v 打印实际类型的值。
%d int变量
%x, %o, %b 分别为16进制,8进制,2进制形式的int
%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00
%t 布尔变量:true 或 false
%c rune (Unicode码点),Go语言里特有的Unicode字符类型
%s string
%q 带双引号的字符串 "abc" 或 带单引号的 rune 'c'
%v 会将任意变量以易读的形式打印出来
%T 打印变量的类型
%% 字符型百分比标志(%符号本身,没有其他操作)
零值:
声明却不初始化时,使用默认值
基础类型与引用类型区别:
int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值
通过 &i 来获取变量 i 的内存地址
一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。
自定义类型:
定义:
type Newint int
var a Newint
type 类型名字 底层类型 类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部包也可以使用。
注意:
不能直接赋值
var a int = 8
var i1 MyInt1 = a,需要类型强转才行。
但var i1 MyInt1 = 9是可以的,解释器隐式转换。
方法:
不共享原类型的方法
类型别名:
type xxx = int
只存在代码编写中,编译时不存在
共享原类型的所有方法
类型转换:
基本格式:
type_name(expression)
在任何情况下,运行时不会发生转换失败的错误(译注: 错误只会发生在编译阶段)。
命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。
环绕行为:
超过最大值时,从最小值开始继续
可以用math.MinInt16等常量来判断
数字转字符串:
strconv.Itoa
Sprintf函数%v
字符串转数值:
strconv.Atoi
布尔转字符串:
string(false)会报错,只能通过sprintf
内存拷贝:
字符串转成切片,会产生拷贝。严格来说,只要是发生类型强转都会发生内存拷贝。
指针:
定义:
一个指针变量指向了一个值的内存地址。
取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
声明:
在使用指针前你需要声明指针。* 号用于指定变量是作为一个指针
var var_name *var-type
取值:
在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。
*ip
作用域:
在Go语言中,返回函数中局部变量的地址也是安全的。
使用规范:
每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。要找到一个变量的所有访问者并不容易,我们必须知道变量全部的别名。
空指针:
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
一个指针变量通常缩写为 ptr。
任何类型的指针的零值都是nil。如果p != nil测试为真,那么p是指向某个有效变量。
相等性:
指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。
指针数组:
var ptr [MAX]*int;
指向指针的指针:
var ptr **int;
访问指向指针的指针变量值需要使用两个 * 号
一种是内置类型uintptr,本质是一个整型,另一种是unsafe包提供的Pointer,表示可以指向任意类型的指针。
通常uintptr用来进行指针计算,因为它是整型,所以很容易计算出下一个指针所指向的位置,而unsafe.Pointer用来进行桥接,用于不同类型的指针进行互相转换。
常量const:
概述:
var换为const,定义的时候必须赋值
常量表达式的值在编译期计算,而不是在运行期,
不能取址,const修饰的全局变量和static变量存储在全局的只读空间中,这时候的地址,在运行阶段不可以更改
类型限制:
只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
批量声明:
除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法
const (
a = 1
b
)
iota:
特殊常量,可以认为是一个可以被编译器修改的常量。在第一个声明的常量所在的行,iota这个变量将会被置为0,然后在每一个有常量声明的行加一。
(iota 可理解为 const 语句块中的行索引)。
const (
a = iota //0,如果没有第一行使用,而是后面使用了,也会从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
)
应用:
结合位运算符<<,可以表示KiB,MiB,GiB单位 KiB=1<<(10*iota) 1024,常量表达式,赋值一次即可,后续的可以省略
但不能用于产生1000的幂(KB、MB等)
无类型常量:
概述:
const后面接的是基础类型的常量,但是许多常量并没有一个明确的基础类型。分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。
只有常量可以是无类型的。
声明:
var x float32 = math.Pi
var z complex128 = math.Pi
右边的math.Pi即为无类型常量
const (
Pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
类型转换:
隐式转换:
当一个无类型的常量被赋值给一个变量的时候,会隐式转换。
var i int8 = 0
对于一个没有显式类型的变量声明语法(包括短变量声明语法),无类型的常量会被隐式转为默认的变量类型。
无类型的整数常量默认转换为int,对应不确定的内存大小,但是浮点数和复数常量则默认转换为float64和complex128。
显式转换:
var i = int8(0)
运算符:
一元的加法和减法运算符:
自增表达式:
i++,语句,非表达式,所以j = i++是非法的
二元比较运算符:
bit位操作运算符:
& 位运算 AND
| 位运算 OR
^ 位运算 XOR
&^ 位清空 (AND NOT)
<< 左移
>> 右移
逻辑运算符:
&&对应逻辑乘法,||对应逻辑加法,乘法比加法优先级要高
流程控制:
条件语句:
if(){
}else{ //记得同一行
}
switch
switch coinflip() { # switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较);
case "heads":
heads++
case "tails":
tails++
default:
fmt.Println("landed on edge!")
}
select用于channel
循环语句:
for:
写法一:
for initialization; condition; post {
// zero or more statements
}
initialization部分是可选的,在for循环之前这部分的逻辑会被执行,通常是一些简短的变量声明,一个赋值语句,或是一个函数调用
condition部分必须是一个结果为boolean值的表达式,在每次循环之前,语言都会检查当前是否满足这个条件,若不满足的话便会结束循环;
post部分的语句则是在每次循环迭代结束之后被执行,
示例:
for i:=0;i<5;i++{
}
写法二:
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
可以在循环里往原切片添加元素,在循环开始前会获取切片的长度 len(切片),然后再执行len(切片)次数的循环。
s := []int{1,2,3,4,5}
for _, v:=range s {
s =append(s, v)
fmt.Printf("len(s)=%v\n",len(s))
}
goto语句:
无条件地转移到过程中指定的行
如何标注代码块
OnHead:{
fmt.Println("sss")
}
break tag也可以使用
死循环的写法:
for ;; {}
for {}
注意go没有while
范围range:
概述:
用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。
在数组和切片中它返回元素的索引和索引对应的值(拷贝),在集合中返回 key-value 对。
因为每一次遍历都是对列表中元素的拷贝.
for _, num := range nums {
sum += num
}
for name := range ages { # 自动忽略第二个参数
names = append(names, name)
}
注意:在迭代时,返回的变量是一个迭代过程中根据切片依次赋值的新变量,所以值的地址总是相同的。
range中等号左边的变量,是提前定义好的,并不是临时创建.
对比python:
list会动态添加,yield,所以无穷遍历
map会报错dictionary changed size during iteration
对golang:
slice会提前求值,提前将len(infos)的值计算出来的
修改后面的元素值,会动态生效
map会动态添加
删除,会动态生效。循环次数减少。
内部实现:
range本质是for语句的一个语法糖。
编译器会在循环开始前copy一次循环对象。
switch语句:
格式:
switch var1 {
case val1:
...
case val2:
...
default:
...
}
变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。
数组:
概述:
值类型
声明:
var variable_name [SIZE] variable_type
# 不声明size则为切片
初始化数组:
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 a [3] int # 默认情况下,数组的每个元素都被初始化为元素类型对应的零值
a =[3]int {1,2,3}
或
a :=[3]int {1,2,3}
也可以指定一个索引和对应值列表的方式初始化
symbol := [...]string{0: "$", 1: "€", 2: "£", 3: "¥"} # 索引的顺序是无关紧要的,可以不连续
索引:
balance[4] = 50.0
var salary float32 = balance[9]
比较:
如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的
只有当两个数组的所有元素都是相等的时候数组才是相等的
多维数组:
var a [3][2]int
a[1][1]=2
二维数组的初始化:
a := [][]int {
{1,2,3}}
作为函数参数:
因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。
可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以直接反馈到调用者
func zero(ptr *[32]byte)
缺点:
数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息。
切片:
概述:
引用类型
要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。
定义:
一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。
一个slice由三个部分构成:指针、长度和容量。
指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。
长度对应slice中元素的数目;长度不能超过容量
容量一般是从slice的开始位置到底层数据的结尾位置。
内置的len和cap函数分别返回slice的长度和容量。
如:
months := [...]string{1: "January", /* ... */, 12: "December"}
Q2 := months[4:7] # len为3,cap为9
summer := months[6:9] # len为3,cap为7
声明:
var identifier []type # 类似数组,唯一不同的是不用指定长度,但需要先初始化才能使用(因为这个时候还不知道长度和容量)
初始化:
var slice1 []type = make([]type, len) # 初始化,容量部分可以省略,在这种情况下,容量将等于长度。在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。
slice1 := make([]type, len)
slice1 := make([]T, length, capacity) # 避免扩容时申请内存空间,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。
s :=[] int {1,2,3 } # 自动识别当前初始化的长度和容量,会隐式地创建一个对应长度的数组,然后slice的指针指向底层的数组。
s := arr[startIndex:endIndex] # 切片修改时,数组也随之修改(扩容后,则是新的内存地址,修改不影响原来的)
s := arr[startIndex:]
s := []int(nil)
切片:
slice可以由数组或者slice切片而来
如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,新slice的长度会变大
切片操作对应常量时间复杂度
len() 和 cap() 函数:
长度和容量
排序:
内置方法:
sort.Ints
sort.Float64s
sort.Strings
。。。
自定义比较器:
sort.Slice(b,func(i,j int) bool {return b[i]<b[j]})
稳定排序:
sort.SliceStable()
实现排序接口:
type Interface interface {
Len() int
Less(i, j int) bool // i, j 是元素的索引
Swap(i, j int)
}
删除特定元素:
for range遍历,找到该位置后,将后面的元素赋值到j-1的位置
函数参数:
因为是引用类型,作为函数参数将会传递引用(或者说是对底层的引用复制了)。
原理:
slice本身是对底层数组的引用,slice值包含指向第一个slice元素的指针,复制一个slice只是对底层的数组创建了一个新的slice别名。slice本身决定的。
示例:
func reverse(s []int)
相等性:
slice之间不能比较
slice的元素是间接引用的,slice甚至可以包含自身。处理麻烦
slice的元素是间接引用的,一个固定值的slice在不同的时间可能包含不同的元素。并且Go语言中map等哈希表之类的数据结构的key只做简单的浅拷贝,它要求在整个声明周期中相等的key必须对相同的元素。
slice唯一合法的比较操作是和nil比较
标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte)
其他类型的slice需要展开每个元素进行比较
空(nil)切片:
定义:
var slice []int
一个切片在未初始化之前默认为 nil,长度为 0,容量为0(也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。)
与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。
判断:
if summer == nil { /* ... */ }
操作:
除了和nil相等比较外,一个nil值的slice的行为和其它任意0长度的slice一样;例如reverse(nil)
除了文档已经明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。
长度为0的切片:
如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。
空slice:长度为 0,容量为0,区别是未初始化的。
append()函数:
用法:
numbers = append(numbers, 2,3,4) # 不能确定是否同一个slice,通常是将append返回的结果直接赋值给输入的slice变量
x = append(x, x...) # “...”省略号表示接收变长的参数为slice
扩容:
每次调用appendInt函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。
1.如果有足够空间的话,
直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间
2.如果没有足够的增长空间的话,
appendInt函数则会先分配一个足够大的slice用于保存新的结果,这个slice与输入的slice将会引用不同的底层数组。同时旧的数组内存地址仍然存在。
自动扩容,每次扩容变为2倍(在1024前直接翻倍,cap超过1024的,新cap变为老cap的1.25倍),避免了多次内存分配,也确保了添加单个元素操的平均时间是一个常数时间。
数组的内存是连续的
底层结构:
从这个角度看,slice并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型
type IntSlice struct {
ptr *int
len, cap int
}
copy()函数:
copy(numbers1,numbers) /* 拷贝 numbers 的内容到 numbers1 */
返回成功复制的元素的个数,等于两个slice中较小的长度
Map(集合):
概述:
引用类型
可以像迭代数组和切片那样迭代它。不过,Map 是无序的
声明:
var map_variable map[key_data_type]value_data_type # map[K]V,K需要支持==比较运算符,最好不要用浮点数。
初始化:
map_variable = make(map[key_data_type]value_data_typ)
声明并初始化:
var a = map[string]int{"ss":1}
map_variable := make(map[key_data_type]value_data_type) #
未初始化的字典:
如果不初始化 map,那么就会创建一个 nil map,ages == nil。长度为0。len(ages) == 0
nil map 不能用来存放键值对,其他大部分操作,包括查找(返回类型零值)、删除、len和range循环都可以安全工作在nil值的map上(slice不同,可以进行任何操作)
字面值语法:
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
赋值操作:
map中的元素不是变量,因此不能寻址,只能直接找到元素的值。
如何修改?
1.在结构体比较大的时候,用指针效率会更好,因为不需要值copy
2.先整体取出来,赋值给临时变量,然后再修改。最后放回去。
使用:
countryCapitalMap[ "Japan" ] = "东京"
v,ok := m2["xxx"] # ok在取到值时返回true,false时v为对应类型的零值
delete(ages, "alice")函数用于删除集合的元素, 参数为 map 和其对应的 key。
key的要求:
必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。
比如:bool, 数字,string, 指针, channel(原则是结构体), 还有 只包含前面几个类型的 interface types, structs, arrays
不能的类型:
slice不能做相等性比较(底层元素可能变化)
map类似
function不能取址,虽然reflect.Value.Pointer可以取到地址。
相等性比较:
和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。
通过循环判断值
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
元素取址:
map中的元素并不是一个变量(hash后的东西?),而是一个值,因此我们不能对map的元素进行取址操作。
禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效,旧的指针变量可能无效。
数组和slice可以对元素取址,因为数组的内存地址是固定的,slice内存增长时,先创建更大的内存地址,然后迁移旧数据,同时旧的数组仍旧会存在,旧的slice的copy仍可读到值。
示例:
var b = make([]int,1,1)
c := &b[0]
e := b # 复制引用,深复制使用copy函数(注意与e内存地址有关)
fmt.Println(&b[0])
b = append(b,1)
fmt.Println(&b[0])
fmt.Println(c,e)
而对于字典:
a := make(map[string]interface{},1)
a["s"]= struct{}{}
d := a
a["ss"]= struct{}{}
fmt.Println(a["s"])
fmt.Println(d) # d会随着a的变化而变化,这点和slice不同,字典要复制只能通过for range的方式
key如何用slice等不可比较类型:
定义一个辅助函数k,将slice转为map可以用的可比较类型(整数、数组或结构体等)的key,每次先用辅助函数k转化一下key即可。
示例:
var m = make(map[string]int)
func k(list []string) string { return fmt.Sprintf("%q", list) }
func Add(list []string) { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }
存储:
哈希函数:又称散列算法、散列函数。主要作用是通过特定算法将数据根据一定规则组合重新生成得到一个散列值,在哈希表中,其生成的散列值常用于寻找其键映射到哪一个桶上。
链地址法:采用的就是 "链地址法 " 去解决哈希冲突,又称 "拉链法"。其主要做法是数组(内存结构) + 链表的数据结构,其溢出节点的存储内存都是动态申请的,因此相对更灵活。映射在内存层面仍然是数组。
Go map 中的桶和溢出桶的概念,在其桶中只能存储 8 个键值对元素。当超过 8 个时,将会使用溢出桶进行存储或进行扩容
内存本身是晶体管数组。
扩容:
触发时机:
触发 load factor 的最大值,负载因子已达到当前界限
溢出桶 overflow buckets 过多
流程:
1.确定扩容容量规则。
若不是负载因子 load factor 超过当前界限,也就是属于溢出桶 overflow buckets 过多的情况。因此本次扩容规则将是 sameSizeGrow,即是不改变大小的扩容动作,重新映射hash
若是负载因子 load factor 达到当前界限,将会动态扩容当前大小的两倍作为其新容量大小
2.初始化、交换新旧 桶/溢出桶
主要是针对扩容的相关数据前置处理,涉及 buckets/oldbuckets、overflow/oldoverflow 之类与存储相关的字段
内部只会先进行预分配,当使用的时候才会真正的去初始化
3.扩容
扩容是采取增量扩容的方式,并非一步到位。当有访问到具体 bukcet 时,才会逐渐的进行迁移(将 oldbucket 迁移到 bucket)
迁移:计算得到所需数据的位置。再根据当前的迁移状态、扩容规则进行数据分流迁移。结束后进行清理,促进 GC 的回收
扩展:
若正在进行扩容,就会不断地进行迁移。待迁移完毕后才会开始进行下一次的扩容动作
删除:
delete()是安全的,可以一边delete一边遍历。
结构体struct:
概述:
一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。
不支持类,继承,结构体+接口实现面向对象
值类型
定义:
type struct_variable_type struct {
member definition
member definition
...
member definition # 如果相邻的成员类型如果相同的话可以被合并到一行
}
声明:
var Book1 Books
初始化:
1.p := Point{1, 2} # 要记住结构体的每个成员的类型和顺序,一般只在定义结构体的包内部使用
2.anim := gif.GIF{LoopCount: nframes} # 以成员名字和相应的值来初始化
访问:
结构体.成员名
结构体指针:
定义:
1.var struct_pointer *Books
2.var struct_pointer = new(Books)
初始化:
1.struct_pointer = &Book1
2.*struct_pointer = Point{1, 2}
使用结构体指针访问结构体成员,使用 “.” 操作符:
struct_pointer.title 访问到值,而不是内存地址
相当于(*struct_pointer).title
函数参数:
func EmployeeByID(id int) *Employee { /* ... */ }
不会写结构体类型,而是写指针(但写了结构体类型也没问题,注意使用场合就行)
原因:
1.因为在赋值语句的左边并不确定是一个变量(译注:调用函数返回的是值,并不是一个可取地址的变量)。比如return Employee{}时,EmployeeByID(id).Salary=1的左边是一个值,而不是变量,不能赋值。
2.return Employee{}虽然可以返回一个新的结构体,但是效率不如操作原结构体的指针高。如果要在函数内部修改结构体成员的话,用指针传入是必须的。
3.作为传入参数时,也一般传指针,拷贝指针,空间和时间的开销都很小,效率较高。
内存布局:
占用一块连续的内存
构造函数:
自己实现一个函数,返回实例结构体即可。
值类型,返回时会拷贝,所以返回指针
比较:
如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的。如果成员不可比较或者仅比较字段内容,可以用reflect.DeepEqual(sm1, sm2)这种方法。
可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。
相同struct类型的可以比较,不同struct类型的不可以比较,编译都不过,类型不匹配。不但与属性类型个数有关,还与属性顺序相关。
匿名字段:
string类型作为名字,注意类型不能重复,匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突
因为成员的名字是由其类型隐式地决定的,所有匿名成员也有可见性的规则约束
一般应用场景:自定义struct+嵌套
示例:
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
var w Circle
w.X = 8 # 这样即可直接访问,也可以w.Point.X = 8,在访问子成员的时候可以忽略任何匿名成员部分
不幸的是,结构体字面值并没有简短表示匿名成员的语法
只能w = Circle{Point{8, 8}, 5}, 20
作用:
匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。
比如w.strings.lowcase()
嵌套结构体:
可以使用匿名字段,并且没有重复字段的情况下,支持直接访问而不用写父级字段
父层也有相同字段的情况下,访问的是父层,如果是子层内调用其他方法,调用的是子层的方法而不是父类的
子层都有该字段时,不能直接访问,需显示调用