Go语言核心之美 3.1-数组

上一章我们深入学习了基本数据类型,它们是构建复杂数据类型的基础,是组成Go语言世界的原子。本章,我们将学习复合数据类型:通过不同的方式将基本类型组合起来。主要有四种复合类型--数组,切片(slice),map,结构体(struct),在本章末尾,我们将展示如何通过struct来进行JSON编解码,同时配合template模板来生成HTML页面。

数组和结构体都是聚合类型:数组是由元素组成,结构体由字段组成,无论是元素还是字段,在内存中都是连续排列的(这个极大的增加了内存的连续访问性,也是Go的一个重要优点,内存排列很紧密)。数组是同构类型--每个元素的类型都是相同的;结构体是异构类型--每个字段的类型都可以不同。数组和结构体的内存大小在初始化后都是固定的,相比之下,切片和map则是动态数据结构,它们的内存会按需增长。

数组是同一类型元素组成的序列,长度是固定的,一个数组可以由零个或多个元素组成。因为数组长度是固定的,所以Go语言中很少直接使用数组,更多使用的是slice(切片)。slice是长度可变的元素序列,使用起来非常灵活,但是要理解切片,首先要理解数组(slice是底层数组的引用)。

数组元素可以通过下标来访问,下标的取值范围是0到数组长度减1,内置len函数可以获取数组的长度(元素个数):

var a [3]int             // 整形数组,包含3个元素
fmt.Println(a[0])        // 打印第一个元素
fmt.Println(a[len(a)-1]) // 打印最后一个元素, a[2]

// 打印索引和元素
for i, v := range a {
    fmt.Printf("%d %d\n", i, v)
}

// 只打印元素
for _, v := range a {
    fmt.Printf("%d\n", v)
}

如果没有显式初始化数组,那么默认情况下,数组中的元素会初始化为相应元素类型的零值,对于int类型来说就是0。也可以使用这种数组字面值语法来初始化:

var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"

如果用...来指定数组的长度,那么数组的长度就是初始化元素的个数。因此,上面q数组的初始化可以简化为:

q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

数组长度也是数组类型的一部分,所以[3]int和[4]int是不同的数组类型。数组长度是在编译期确定的,因此必须是常量:

q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int

学到后面大家就会发现,数组、切片、map及结构体的初始化写法是很像的。上面的初始化是直接通过值序列来完成的,也可以通过索引:值这种键值对列表来实现:

type Currency int

const (
    USD Currency = iota // 美元
    EUR                 // 欧元
    GBP                 // 英镑
    RMB                 // 人民币
)

symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

fmt.Println(RMB, symbol[RMB]) // "3 ¥"

上面的初始化中,索引的顺序是不重要的,甚至可以省略一些索引,还记得前面的内容吗:未指定初始值的元素将用零值进行默认初始化,例如:

r := [...]int{99: -1}

上条语句定义一个包含了100个元素的数组r,其中最后一个元素被初始化为-1,其它元素都是0。

如果数组元素的类型是可比较的,那么数组类型也是可比较的,可以使用==或者!=来比较两个数组。只有当两个数组的所有元素都相等时,数组才相等:

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int,类型不同,不能比较

看一个真实的例子,对于一条任意长度的消息(byte slice类型),crypto/sha256包中的Sum256函数会对其进行摘要或者hash。摘要是256bit长度,因此它的类型是[32]byte数组(32字节 * 8 = 256bit)。如果两条消息摘要相同,那么消息就可以认为是相同的(实际上,两条不同的消息也可以有相同的摘要,但是很少见,摘要攻击就是利用了不同的密码可能生成同一个摘要,直接进行摘要破解,而不是进行密码破解,这样可以减少很多种组合情况);如果消息摘要不同,那消息也必然不同。下面的例子中,用SHA256算法分别生成"x"和"X"的摘要:

import "crypto/sha256"

func main() {
    c1 := sha256.Sum256([]byte("x"))
    c2 := sha256.Sum256([]byte("X"))
    fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1)
    // Output:
    // 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
    // 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015
    // false
    // [32]uint8
}

上面的例子中,"x"和"X"只有一个bit位的差异,但是生成的摘要几乎有一半的bit位是不同的。这里Printf中的%x参数,指定了以16进制格式打印数组或slice中全部元素,%t参数用语打印布尔值,%T参数用来显示变量的数据类型。

进行函数调用时,传递给函数参数的值实际上是变量的拷贝,所以函数参数接收的是一个副本,并不是原始的变量,这种就是值传递,Go语言的函数调用默认就是值传递。这种机制在传递较大的数组值时,效率是很低的(数组中的所有元素都会重新拷贝一次),并且对数组的修改实际上是修改拷贝值,而不是原始数组。Go语言的这种机制和其它很多语言是不同的,其它编程语言在函数调用时,可能会隐式地将数组作为引用或者指针进行参数传递(C语言)。

当然,我们可以显示传递一个数组指针,这样函数对数组的修改就会直接修改原始数组。下面的函数将[32]byte类型的数组进行清零:

func zero(ptr *[32]byte) {
    for i := range ptr {
        ptr[i] = 0
    }
}

上面的函数可以修改的更简洁,因为[32]byte{}可以生成一个所有元素都是0的数组:

func zero(ptr *[32]byte) {
    *ptr = [32]byte{}
}

尽管通过指针来传递、修改数组是很高效的,但是数组依然不具有可伸缩性,因为数组的长度是固定的。上面的zero函数就无法处理*[16]byte类型的数组指针,而且也没有任何添加、删除数组元素的办法。因此,除了类似SHA256这种需要固定大小数组的场景,其它时候,我们一般都用slice。


练习 4.1: 编写一个函数,计算两个SHA256哈希码中不同bit的数目。

练习 4.2: 编写一个程序,默认使用SHA256对标准输入进行摘要,同时也可以通过命令行参数指定使用SHA384或SHA512算法。



文章所有权:Golang隐修会 联系人:孙飞,CTO@188.com!


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值