一、数组
- 数组作为函数参数,传值的;
- 只有长度和类型相同,才是同一类型,才可以相互赋值;
var arr = [10]int{1, 2, 3}//声明长度才是数组,没声明长度的是切片
//切片可以append,数组不可以
//[]int 和 [10]int是不能相互赋值的。
复制代码
二、切片
切片是引用类型, 什么是引用类型?
"引用类型" 有两个特征:1、多个变量引用一块内存数据,不创建变量的副本,2、修改任意变量的数据,其它变量可见。
1、slice内存结构
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
//在64位架构的机器上,一个切片需要24字节的内存:指针字段需要8字节,长度和容量字段分别需要8字节。
//可以看出,切片就类似指针一样,只不过切片是指向底层数组;
//记住一句话,切片传值时,只能查看修改切片的元素,不能添加和删除切片元素
复制代码
2、切片长度和容量
- 长度:len是你可以访问的下标范围。
- 容量:
- cap是切片指向的底层数组的长度;
cap - len
是你可以append的个数,超过这个数量时,说明底层数组占满了。再append时,底层会给你扩容。在切片的容量小于 1000 个元素时,总是会成倍地增加容量。
func main() {
slice := make([]int, 3, 5) //长度为3,容量为5
slice[0] = 0
slice[1] = 1
slice[2] = 2
slice = append(slice, 3)
slice = append(slice, 4)
//slice[3] = 4 //起初切片长度为3,在没append之前不可以访问第四个元素,否则会报错:下标越界
fmt.Println("slice = ", slice)
fmt.Println("切片长度:", len(slice))
fmt.Println("切片容量:", cap(slice))
}
//slice = [0 1 2 3 4]
//切片长度: 5
//切片容量: 5
复制代码
3、如何设置容量
-
make不指定容量时,长度就是容量;
-
合理地设置存储能力的值,可以大幅度降低数组切片内部重新分配内存和搬送内存块的频率,从而提高程序的性能;
-
切片的扩容底层:重新分配一块“够大”的内存,把内容从原来的内存块复制到新的内存块,这回产生比较明显的开销;
-
先提前设置好合理的存储能力,就不会发生“扩容”这样非常耗费CPU的动作,从而达到空间换时间的目的;
4、切片的坑
func main() {
slice := make([]int, 3, 5)
slice[0] = 0
slice[1] = 1
slice[2] = 2
test1(slice) //只修改值
test2(slice) //追加值
fmt.Println("原slice = ", slice)
test3(&slice) //追加值
fmt.Println("原slice = ", slice)
fmt.Println("切片长度:", len(slice))
fmt.Println("切片容量:", cap(slice))
}
func test1(s []int) {//传切片的值
s[0] = 10
fmt.Println("test1()中修改:", s)
}
func test2(s []int) {//传切片的值
s = append(s, 3)
fmt.Println("test2()中修改:", s)
}
func test3(s *[]int) {//传切片的地址
*s = append(*s, 3)
fmt.Println("test3()中修改:", s)
}
//运行结果
//test1()中修改: [10 1 2]
//test2()中修改: [10 1 2 3] //可以看出,传切片的值,并不能修改切片的长度,只能修改值
//原slice = [10 1 2]
//切片长度: 3
//切片容量: 5
//test3()中修改: &[10 1 2 3] //传地址才能修改切片的长度
//原slice = [10 1 2 3]
//切片长度: 4
//切片容量: 5
复制代码
-
外部切片和切片参数,本身就是不一样的对象,其内存地址都不一样;所以传值时,在函数内部append对外部的切片是不影响的;
-
append会改变切片的长度字段;实际上slice作为函数参数时也是值拷贝,在函数中对slice修改是通过slice中保存的地址对底层数组进行修改,所以函数外的slice被修改了;
-
但是需要对slice做插入和删除时,由于需要改变长度字段,值拷贝就不行了,需要slice本身在内存中的地址。
三、映射
-
go的map是散列表,所以是无序的,长度理论上是不受限制的;
-
映射的键可以是任何值。只要这个值可以使用==运算符做比较。
-
map在函数间的传递是传引用。不会发生值拷贝。
1、map容量
make(map[string]int,10)
创建的时候可以指定容量;
-
如果容量太小,冲突就比较严重。数据查询速度难免降低;如果需要提供数据查询速度,需要以空间换时间,加大容量。
-
如果初始容量太小,而你需要存入大量的数据,一定就会发生数据复制和rehash,会影响性能;
-
容量不够时,底层会自动扩容;
2、常规操作
//var m map[string]int //只是声明的话,这个一个空的map(类似空指针),不可以进行操作
//要创建一个map才能进行操作,没指定长度时,长度为0
m := make(map[string]int)
//直接赋值,就可以添加新的key
m["alan"] = 10
//删除key
delete(m, "alan")
//判断key是否存在
n, ok := m["alan"] //可以不加判断,如果key不存在,则返回零值
if !ok {
fmt.Println("the key is not exist")
} else {
fmt.Println(n)
}
//迭代map
for key,value:=range m{
fmt.Println(key,"--",value)
}
复制代码
3、如何利用map实现set
- map,是字典,存的是key-value;
- set,是集合,只存value且value是不重复的,能够用来判断要给val是否存在set中。
- map和set都可以用
哈希表
或者红黑树
来实现,哈希表是无序的,红黑树是有序的;
func main() {
//空结构体,不占内存
//且多个空结构体的实例,地址是相同的
a := struct{}{}
b := struct{}{}
fmt.Println(a == b) // true
fmt.Printf("%p, %p\n", &a, &b) // 0x55a988, 0x55a988
set := make(map[int]struct{})
set[1] = struct{}{} //实例化一个空结构体
if _, ok := set[1]; ok {
fmt.Println("the key 1 is exist")
}
}
复制代码
四、接口
1、方法集规则
每个类型都有与之关联的方法集,这会影响到接口实现的规则。
-
从调用者的角度看规则:
- 类型 T 方法集包含全部 receiver T 方法。
- 类型 *T 方法集包含全部 receiver T + *T 方法。
- 如类型 S 包含匿名字段 T,则 S 方法集包含 T 方法。
- 如类型 S 包含匿名字段 *T,则 S 方法集包含 T + *T 方法。
-
从接收者的角度看规则:
- receiver T 方法,属于类型T和*T
- receiver *T 方法,只属于类型 *T
- 使用指针接收者来实现一个接口,那么只有指向类型的指针才能够实现对应的接口
- 使用值接收者来实现一个接口,那么类型的值和指针都能够实现对应的接口
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a *Integer) Add(b Integer) {
*a += b
}
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}
var a Integer = 1
var b1 LessAdder = &a //编译OK
var b2 LessAdder = a //编译not OK
对于上例:
*Integer实现了Less()、Add()
Integer只实现了Less()
也就是说*Intrger实现了接口,而Integer没有实现接口
复制代码
五、通道
1、chan类型
-
chan int
,可以写入和读取int类型的数据。 -
chan<- int
,只可以写入。 -
<- chan int
,只可以读取。 -
make(chan int, 100)
,channel也是需要make之后才能使用,只是声明的话相当于一个空指针。- 100代表缓存容量大小,也就是并发数。
- 如果没设置容量,或者容量为0,则代表无缓存,也就是同步的,表现为写入一个数后,这个数要被读取消费了,才能继续写入。
- 当缓存空间为0时写入会阻塞,当chan没有数据时读取会阻塞。
2、基本使用
select
range
close
timeout、Timer、Ticker
并发模型等
复制代码
3、读写close了的chan
-
读取
- 读取关闭后的无缓存通道,不管通道中是否有数据,返回值都为0和false;
- 读取关闭后的有缓存通道,将缓存数据读取完后,再读取返回值为0和false;
-
写入
- 通道关闭后再写入则会panic;
六、结构体
可见性:
-
不管是结构体名还是字段名,首字母大写的都是可导出的,也就是包外可见,也就是公有的;
-
首字母小写的都是不可导出的,也就是包外不可见,也就是私有的;
1、结构体比较
-
如果结构体的所有成员变量都可以比较,那么这个结构体是可以比较的。
-
两个结构体的比较可以使用==或者!=;
-
那么这种可比较的结构体就可以作为map的键;
func main() {
type C struct {
A int
B string
}
c1 := C{A: 1, B: "abc"}
c2 := C{A: 1, B: "abc"}
c3 := C{A: 2, B: "abc"}
fmt.Println(c1 == c2) //true,等价于挨个比较字段
fmt.Println(c1 == c3) //false
}
复制代码
2、继承(结构体嵌套)
type Point struct {
X int
Y int
}
type circle struct {
//匿名成员,将另一个结构体嵌入本结构体,就可以直接访问叶子属性而不需要给出完整的路径;要给出也行
//匿名成员有自己的名字————就是命名的类型名字,因此不能同时包含两个类型相同的匿名成员
Point
}
type Wheel struct {
circle //不可导出
}
var c circle
c.X = 10 //等价于 c.Point.X = 10
c.Y = 10 //等价于 c.Point.Y = 10
var w Wheel
w.X = 8 // 成立,虽然circle不可导出
//w.X是可导出的,w.circle.Point.X是不可导出的;w.circle是不可见的,w.X是可见的
//也就是说有些匿名成员是不可导出的,但匿名成员它自己的可导出成员仍然是可见的
复制代码
外层的结构体不仅仅是获得了匿名成员类型的所有成员,而 且也获得了该类型导出的全部的方法。 这个机制可以用于将一个有简单行为的对象组合成有 复杂行为的对象;
3、结构体方法
- 在关键字 func 和函数名之间的参数被称作接收者,将函数与接收者的类型绑在一起。 如果一个函数有接收者,这个函数就被称为方法。
- 两种类型的接收者:值接收者和指针接收者。 使用值接收者声明方法,调用时会使用这个值的一个副本来执行。(当调用者太大时,要考虑使用指针接收者方法) 当调用使用指针接收者声明的方法时,这个方法会共享调用方法时接收者所指向的值;(所以指针接收者方法可以修改调用者)
//成功调用的条件
type A struct {
name string
}
func (this * A) Print(){ //指针接收者
fmt.Println(this.name)
}
func (this A) Print1(){ //值接收者
fmt.Println(this.name)
}
func main() {
a:=A{name:"a"} //值变量
a.Print()
a.Print1()
aa:=new(A) //指针变量
aa.name="aa"
aa.Print()
aa.Print1()
//指针类型的临时变量
(&A{name:"testname"}).Print()
(&A{name:"testname"}).Print1()
//值类型的临时变量
A{name:"testname1"}.Print1() //正确,因为可以获取临时变量的值
//下面语句会报错:cannot call pointer method on A literal(字面量);因为无法获取临时变量的地址
//A{name:"testname1"}.Print()
}
值变量调用指针接收者方法时,编译器会隐式的获取变量的地址;
指针变量调用值接收者方法时,编译器会隐式的获取实际的取值;
var a *A
a.Print()
//空指针是允许调用指针接收者方法的,前提是方法内部不通过该指针去访问成员(如这里的Print()),因为指针为空;
//显然,空指针不允许调用值接收者方法,因为编译器会隐式的获取实际的取值;
复制代码
七、string
1、内存结构
string是8比特字节的集合,底层是一个[]byte
runtime包中定义的String的结构
type stringStruct struct {
str unsafe.Pointer //字符串的首地址
len int //字符串长度
}
string在runtime包中就是stringStruct,对外呈现叫做string
可见string就是一个指向[]byte的指针,因此string是无法直接修改的,如str[0]='A'
字符串可以为空,但不能为 nil
复制代码
2、两个小特性
//1、字面量可被复用
func main() {
str := "hello"
str1 := "hello"
fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&str)))
fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&str1)))
//result:
//&{4999307 5}
//&{4999307 5}
}
//针对字面量(就是直接写在程序中的字符串),会创建在只读空间上,并且被复用。
//相同的字面量会被复用,但是子串是不会复用空间的,这是编译器给我们带来的福利了,可以减少字面量字符串占用的内存空间。
str和str1是两个变量,指向同一块内存。
复制代码
//2、字符串是不能修改的,拼接是什么操作,不是修改了吗?
//前面收到string实现是保存一个指向[]byte的指针,字符串不能修改是说这个[]byte不能修改,而不是说string的值不能修改。
func main() {
str := "hello"
fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&str1)))
str = "world"
fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&str)))
//result:地址变了
//&{4999307 5}
//&{4999372 5}
}
//要是字符串拼接呢?也是一样的,会开辟新的内存。就的内存没惹引用了,等着GC呗
func main() {
str := "world"
fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&str)))
//两次遍历,遍历出两个子串长度。根据总长度申请新的内存,然后把子串复制进去
str = str + "good"
fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&str)))
//result:
//&{4999371 5}
//&{824634081488 9}
}
复制代码
3、byte和rune
str := "ben生而平凡" //字节长度为15
fmt.Println([]byte(str))
[98 101 110 231 148 159 232 128 140 229 185 179 229 135 161]
str := "ben生而平凡" //字符长度为7,unicode编码中,一个汉字算一个字符,但大小不是一个字节
fmt.Println([]rune(str))
[98 101 110 29983 32780 24179 20961]
fmt.Println(string(29983))//生,int32可以直接转化为string的
字符:就是各种符号。字母,汉子,+——)(*&……%等等
一个字符要有唯一一个编码啊。有得字符对应的编码很大如29999,一个字节放不下,那只能多字节咯
如果str只包含字符数字标点符号等
则[]rune(str)和[]byte(str)是一样的
len(str)是byte长度,不是rune长度。
str[3],直接取,取的是byte
str[1:6],取范围,也是取字节。
tmp:="3344234"
l, r := 0, len(tmp)-1
for l < r {
[]byte(tmp)[l], []byte(tmp)[r] = []byte(tmp)[r], []byte(tmp)[l]
l++
r--
}//这样反转是没效的,要先转化成[]byte再进行反转;
golang是有字符的: fmt.Println('省')//30465
复制代码
有疑问加站长微信联系(非本文作者)