一、数组
- 数组作为函数参数,传值的;
- 只有长度和类型相同,才是同一类型,才可以相互赋值;
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、切片长度和容量
cap - len
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
哈希表红黑树
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、chan类型
chan intchan<- int<- chan intmake(chan int, 100)
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()),因为指针为空;
//显然,空指针不允许调用值接收者方法,因为编译器会隐式的获取实际的取值;
复制代码
六、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
复制代码
本文使用 mdnice 排版