马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
本帖最后由 赚小钱 于 2020-5-13 11:25 编辑
Go 语言基础 之 类型系统
这是在我发布之后,变成的奇怪排版。如果是几百字,我也就改了。15k 的文字,我写了连个半小时,实在没有精力,没有激情修改了。
很难过基本没有人讨论 go,可能是没有人来科普基础吧。这篇文章先来介绍一下 go 的类型系统吧。
golang 是一门静态类型,强类型的语言。在我的身边有很多人分不清这两个概念,我相信论坛里也是一样的。
静态类型,指在编译时期即可确定类型是什么。与之对应的是动态类型,最常见的动态类型语言,就是大家非常熟悉的 python 和 javascript。
强类型,指类型之间不能隐式转换。与之对应的就是弱类型。很多人将这一点和静态类型混淆。比如 c 语言,是一门静态类型语言,但却是一门弱类型的。
数值类型
在 go 的数值类型非常多,或者说,因为是强类型的,所以划分非常细致。
<div><span style='display: inline !important; float: none; background-color: rgb(247, 247, 247); color: rgb(68, 68, 68); font-family: Tahoma,"Microsoft Yahei","Simsun"; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; orphans: 2; overflow-wrap: break-word; text-align: left; text-decoration: none; text-indent: 0px; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;'>有</span>符号: int8, int16, int32, int64, int</div><div><span style="background-color: rgb(247, 247, 247); color: rgb(68, 68, 68); display: inline; float: none; font-family: Tahoma,"Microsoft Yahei","Simsun"; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; orphans: 2; overflow-wrap: break-word; padding-bottom: 0px; padding-left: 0px; padding-right: 0px; padding-top: 0px; text-align: left; text-decoration: none; text-indent: 0px; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;"></span><span style='display: inline !important; float: none; background-color: rgb(247, 247, 247); color: rgb(68, 68, 68); font-family: Tahoma,"Microsoft Yahei","Simsun"; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; orphans: 2; overflow-wrap: break-word; text-align: left; text-decoration: none; text-indent: 0px; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;'>无</span>符号: uint8, uint16, uint32, uint64, uint</div><div>浮点数: float32, float64</div>
类型名称一目了然,其中有两个需要注意一下这两个类型中,不带有数字来表示其类型大小,因为这两个类型的大小与 cpu 和操作系统相关,在 32 位系统中,占用 4 个字节,64 位系统中,占用 8 个字节。
使用这两个类型的时候需要特别注意,要思考,在不同的机器上运行的时候,是否会导致不同的现象,比如在 32 位机器,是否会溢出。
如果不想思考这些,那么还是使用带有具体大小的类型吧。
字符类型
有三种类型与字符相关<div>byte: 占用一个字节,无符号,可以理解为 c 语言的 char 类型</div><div>rune: 占用四个字节,无符号,表示一个 utf8 编码的字符</div><div>string: 字符串</div></div>
进一步说明一下 byte 与 rune 的区别<div>// 定义一个字符串</div><div>s := "你好"</div><div>// 如果将字符串表示为 byte 切片,其长度为 6</div><div>bs := []byte(s)</div><div>fmt.Println(len(bs)) // 6</div><div>// 如果将字符串表示为 rune 切片, 其长度为 2</div><div>rs := []rune(s)</div><div>fmt.Println(len(rs)) // 2
</div>
使用 rune 切片表示,长度为 2,比较好理解,为什么用 byte 切片表示,长度为 6 呢?因为 go 中的默认的编码为 utf8,一个字符占用 3 个字节。
布尔类型
完成布尔运算的类型
<div>var (</div><div> right, wrong bool</div><div>)</div><div>right = true</div><div>wrong = false</div>
数组类型
在 go 语言里面,数据类型是被明确提出来的。
定义两个数组变量:
<div>var (</div><div> first [5]int</div><div> second [6]int</div><div>)
</div>
变量 first 和 变量 second 的类型是不一样的,因为,数组长度也是类型的一部分。这一点,和 c 是一样的。
PS: 插一句,大家知道 c 是可以定义数组类型的吗,及如何使用 typedef 来定义一个数组类型。
但是,有一点和 c 是不一样的。在函数调用,数组作为实参传递给被调函数的时候,在 c 语言中,数组会退化为一个指向数组首地址的指针,实际上,发生的数据拷贝量只有一个指针的大小。而在 go 中,数组作为参数传递的时候,拷贝的是整个数组的内容,是很可怕的。那如果要使用数组作为参数,就只能这么糟糕了吗?
切片类型
切片的出现,避免了每一次传递数组都要造成大量性能消耗的结果。
切片,很多语言中都有的类型,可以理解为是数组的一个引用。
<div>// 编译器中的表示形式</div><div>type SliceHeader struct {</div><div> Data uintptr // 底层数据地址,可以理解为 c 中的数组首地址</div><div> Len int // 数据的长度,在 c 中传递数组的时候,都需要一个额外的参数来表示其元素个数,就是这个</div><div> Cap int // 实际获得内存的长度,可以和 c++ 的 std::vector 做类比</div><div>}</div>
将数组转换成为切片<div>var array [100]int // 定义长度为 100 的整形数组</div><div>slice := array[:] // 将数组转换为切片,":" 的两端分别为起始下标与终止下标,为左闭右开,左边没有则为0,右边没有则为 array 的长度</div><div>anotherSlice := array[1:10] // 将 array 中,从下标 1 开始,到下标 10 终止的一段切片赋值给变量 anotherSlice, 因为是左闭右开,所以,实际包含的数据为 array 下标 1 到 9
</div>
同样地,也可以将切片再切片<div>var array [100]int</div><div>slice := array[:]</div><div>anotherSlice := slice[1:10] // 切片和数组一样,可以取出一段作为一个新的切片</div>
切片,因为是一个复杂的类型,所以存在一些内置的处理函数
创建
创建一个切片,除了可以从一段切片中划分一段,也可以使用内置函数创建,[] 是创建 slice 时,必须有的,后面跟着,想要创建的 slice 中的元素的类型
<div>slice := make([]int, 0, 16)
</div>
如果想创建一个二维的 slice (可以想象成一个 二维数组),那么只需要将上述的 int 修改为 []int 即可, 因为,每一个元素的类型为 []int <span style='display: inline !important; float: none; background-color: rgb(247, 247, 247); color: rgb(68, 68, 68); font-family: Tahoma,"Microsoft Yahei","Simsun"; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; orphans: 2; overflow-wrap: break-word; text-align: left; text-decoration: none; text-indent: 0px; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;'>matrix := make([][]int, 0, 16)</span><b></b><i></i><u></u><sub></sub><sup></sup><strike></strike>
第二个参数,表示创建出来的 slice 的 Len,第三个参数表示 Cap,如果当 Len == Cap 的时候,可以忽略掉第三个参数,即 slice := make([]int, 16, 16)
等价于slice := make([]int, 16)<b>
</b>
此外,还有一种方式在创建 slice 的同时赋值slice := []int{1, 2, 3, 4, 5, 6}
读取
与其他语言一种,从数组、切片读取都是通过 [] 的方式<div>slice := []int{1, 2, 3, 4, 5, 6}</div><div>value := slice[3] // value = 4</div>
如果,传入的索引越界(超过 Len),则会发生运行时 panic。
遍历
遍历数组是非常常见的需求。在 go 中提供了两种方式。
方式一<div>slice := []int{1, 2, 3, 4, 5, 6}</div><div>for i:=0; i<len(slice); i++ {</div><div> doSomething(i)</div><div>}</div>
方式二<div>slice := []int{1, 2, 3, 4, 5, 6}</div><div>for index, value := range slice {</div><div> // index 就是每一次的下标,从 0 开始,一直到 len(slice)-1</div><div> // value 就是每一次的数据,从 slice[0] 开始,一直到 slice[len(slice)-1]</div><div>}</div>
方式二还有一个变种,如果在遍历的时候只需要 index,或者是某种原因不需要直接得到 value 时,那么可以 <div>slice := []int{1, 2, 3, 4, 5, 6}</div><div>for index := range slice {</div><div> // 这里的index和上面提到的一样,只不过,在使用一个变量接收的时候,只返回index</div><div>}</div>
注意: 在使用方式二遍历的时候,index 与 value 在遍历的过程中,一直都是一个变量。如果需要使用其地址,需要十分十分的注意了。
长度与容量
获取 slice 或者 array 的长度<div>slice := make([]int, 4, 8)</div><div>length := len(slice) // 4</div><div>capacity := cap(slice) // 8</div>
访问的下标只有在[0, length) 才是有效的。
追加
go 允许,也只允许在尾端追加元素,这里的尾端是指 Len 之后,如果 cap 不够的时候会发生扩容,具体扩容算法,以后再说。<div>slice := make([]int, 0, 16) // len = 0, cap = 16, print => []</div><div>slice = append(slice, 1) // len = 1, cap = 16, print => [1]</div>
如果,Len 不为 0,那么<div>slice := make([]int, 4, 16) // len = 4, cap = 16, print => [0, 0, 0, 0]</div><div><span style='display: inline !important; float: none; background-color: rgb(247, 247, 247); color: rgb(68, 68, 68); font-family: Tahoma,"Microsoft Yahei","Simsun"; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; orphans: 2; overflow-wrap: break-word; text-align: left; text-decoration: none; text-indent: 0px; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;'>slice = append(slice, 1) // len = 5, cap = 16, print => [0, 0, 0 , 0, 1]</span><b></b><i></i><u></u><sub></sub><sup></sup><strike></strike>
</div>
append 函数一定会改变 SliceHeader 中的 Len 元素,当超过 Cap 的时候,会先扩容。
注意: append 的第一个参数是要追加的目标 slice,第二个参数是追加的元素,返回值为追加之后的结果。必须要接收返回值,否则,无法得到本次追加的改变内容。
Map 类型
底层为 hash table 的键值存储类型。c++ 在 STL 中,rust 为 std::collections::HashMap,其他语言不熟悉,但是,现代编程语言都会有等价的实现。
创建
创建一个 key 为 string 类型,value 为 int 类型,初始容量为 10 的 mapm := make(map[string]int, 10)
如果不写第二个参数m := make(map[string]int)
则默认为 0
写入
使用 [] 运算符,向指定的 key 中写入数据<div>m := make(map[string]int)</div><div>m["go"] = 0</div>
读取
同样使用 [] 运算符,从指定的 key 中读取数据,当 key 不存在时,会获得到 value 类型的默认值<div>m := make(map[string]int)</div><div></div><div>m["go"] = 3</div><div>a := m["go"] // 3</div><div>notExistsKey := m["xxxx"] // 0</div>
那么,有什么办法来区分,如果获取的值是默认值,是因为未找到,还是本来就设置为默认值呢?
map 在使用 [] 读取数据的时候,有两种用法,一种是上面的形式,使用一个变量来接收 value,另一种,是使用两个变量,一个变量接收 value,另一个变量表示是否找到<div>m := make(map[string]int)</div><div>m["go"] = 3</div><div>value, exists := m["go"] // value = 3, exists = true</div><div></div><div></div><div>value, exists := m["c++"] // value = 0, exists = false</div><div>m["c++"] = 0</div><div>value, exists := m["c++"] // value = 0, exists = true</div>
遍历
map 的遍历只能通过 for range 语法完成,与 slice 类似<div>m := make(map[string]int)</div><div>
</div><div>// 同时得到 key,value</div><div>for k, v := range m {</div><div>}</div><div>
</div><div>// 只获取 key</div><div>for k := range m {</div><div>}</div><div>
</div>
删除
从 map 中删除指定的 key<div>m := make(map[string]int)</div><div>m["go"] = 1</div><div>delete(m, "go")</div>
注意: map 的 key 可以为任意类型,但是,因为 go 中缺少类似于 equals 的接口,所以,使用自定义类型,指针类型作为 key 是很难完成预期目标的。一般而言,会使用整型,字符类型,布尔类型。不会使用浮点类型。
Channel 类型
channel 经常出现在 go 的各种宣传文章中,算是 go 的一个主要卖点,虽然,其他语言中也有,比如 rust 的 std::sync::mpsc::channel。
channel 常用于在多线程 (goroutines) 之间共享数据,虽然,go 中没有所有权的概念,但是,为了程序的安全性,可维护性等,在使用 channel 的时候,建议带入所有权的概念。当变量通过 channel 发送之后,理应不在当前上下文中继续使用。
同时,在某些场景,使用 channel 能大大简化编程的复杂性。比如,生产者-消费者模型。
创建<div>c := make(chan int) // 不带缓冲区</div><div>c := make(chan int, 10) // 带有 10 个容量的缓冲区</div>
缓冲区的作用是,当缓冲区有空余时,允许写入者将数据放入到缓冲区中理解返回。当生产效率,告诉消费效率时,缓冲区可以提高一定的效率(类比于有持久化的消息队列)。但是,也会带来一定的问题,当程序退出时,缓冲区的数据将会丢失。
读写
channel 的写入有两种方式,读取有三种方式,二种变形,先说写入<div>c := make(chan int)</div><div>c <- 3</div>
通过一个非常形象的符号 <- 来表示,将 3 这个数字发送到 channel 中,如果 c 的缓冲区已满,或者无缓冲区,且消费者未准备好时,将进入阻塞状态。
另一种写入,需要用到 select 关键字<div>c := make(chan int)</div><div>select {</div><div>case c <- 3:</div><div>default:</div><div>}</div>
select 语句块,看起来与 switch 是非常相似的。select 允许同时监听多个 channel,当所有的 channel 都不可用时,如果有 default,则执行 default,否则将阻塞。
从 channel 读取,三种方法中,有两种方法与上述的写入是对应的<div>c := make(chan int)</div><div>v := <-c // 形象的小箭头,从 channel 中取出一个元素</div><div>
</div><div>select {</div><div>case i := <-c:</div><div>default:</div><div>}</div>
同时,上述两种方法,各有一个变形,因为,channel 是可以被关闭的。当 channel 被关闭,读取到的数据,都将是元素类型的默认值,因此,与 map 一样<div>v, ok := <-c // 如果 channel 正常,ok == true, 如果被关闭了 ok == false</div><div>
</div><div>select {</div><div>case v, ok := <-c:</div><div> if !ok {</div><div> // closed</div><div> return </div><div> }</div><div>default:</div><div>}</div>
下面来说第三种读取数据的方法<div>c := make(chan int)</div><div>for v := range c {</div><div>}</div>
当 channel 被关闭时才会被动退出,否则,只能在 for-range 语句块中,显式通过 break 跳出循环。
自定义类型
go 中的自定义类型被称作 struct,其实就是 class 的概念(我怀疑是 go 的作者不喜欢 c++,所以,用了 c 里面的 struct。而且,作者中还有 c 的作者)。<div>type CustomStructName struct {</div><div> fieldName Type</div><div>}</div>
比如<div>type TestReport struct {</div><div> Name string</div><div> Math int</div><div> Literature int</div><div>}</div>
如果类型名,或者字段名的首字母为大写,则访问控制权限为 public(所有 packages 可见),小写为 protected (当前 package 可见)。
总结
以上就是关于 go 的基本类型的内容,复杂类型 (interface{}) 将在后续更新。15834 字纯原创手打。
|