Golang | 高级数据类型

_Liu_ · · 520 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一、数组

  • 数组作为函数参数,传值的;
  • 只有长度和类型相同,才是同一类型,才可以相互赋值;
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、内存结构
string8比特字节的集合,底层是一个[]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
复制代码

有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:_Liu_

查看原文:Golang | 高级数据类型

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

520 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传