Golang | 高级数据类型

463 阅读11分钟

一、数组

  • 数组作为函数参数,传值的;
  • 只有长度和类型相同,才是同一类型,才可以相互赋值;
var arr = [10]int{123}//声明长度才是数组,没声明长度的是切片
//切片可以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([]int35//长度为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([]int35)
 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、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()),因为指针为空;
//显然,空指针不允许调用值接收者方法,因为编译器会隐式的获取实际的取值;

六、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 := 0len(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 排版