数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素(element),这种类型可以是任意的原始类型,比如 int、string 等,也可以是用户自定义的类型。一个数组包含的元素个数被称为数组的长度。在 Golang 中数组是一个长度固定的数据类型,数组的长度是类型的一部分,也就是说 [5]int 和 [10]int 是两个不同的类型。Golang 中数组的另一个特点是占用内存的连续性,也就是说数组中的元素是被分配到连续的内存地址中的,因而索引数组元素的速度非常快。

本文将介绍 Golang 数组的基本概念和用法,演示环境为 ubuntu 18.04 & go1.10.1。

Golang 数组的特点

我们可以把 Golang 数组的特征归纳为以下三点:

  • 固定长度:这意味着数组不可增长、不可缩减。想要扩展数组,只能创建新数组,将原数组的元素复制到新数组。
  • 内存连续:这意味可以在缓存中保留的时间更长,搜索速度更快,是一种非常高效的数据结构,同时还意味着可以通过数值的方式(arr[index])索引数组中的元素。
  • 固定类型:固定类型意味着限制了每个数组元素可以存放什么样的数据,以及每个元素可以存放多少字节的数据。

数组是个固定长度的数据类型,其长度和存储元素的数据类型都在声明数组时确定,并且不能更改。如果需要存储更多的元素,必须先创建一个更长的数组,然后把原来数组里的数据复制到新数组中。
数组占用的内存是连续分配的,比如我们创建一个包含 5 个整数元素的数组:

arr1 := []int{,,,,}

数组在内存中的结构类似下图:

由于内存连续,CPU 能把正在使用的数据缓存更久的时间。而且在内存连续的情况下非常容易计算索引,也就是说可以快速迭代数组里的所有元素。原因是数组的类型信息可以提供每次访问一个元素时需要在内存中移动的距离,既然数组的每个元素的类型都相同,又是连续分配,因此就可以以固定的速度索引数组中的任意元素,并且速度非常快!

数组的声明与初始化

声明数组
声明数组时需要指定数组的长度和数组中元素的类型,比如声明一个包含 5 个元素,类型为 int 的数组:

var arr1 []int

这里强调一点,数组的类型是包含数组长度的,因此 [5]int 和 [10]int 是两个不同类型的数组。
在 Go 语言中声明变量时,总会使用对应类型的零值来初始化变量,数组也不例外。当声明数组变量时,数组内的每个元素被初始化为对应类型的零值。比如变量 arr1,它的 5 个元素都被初始化成了 int 类型的零值 0。使用 fmt.Println(arr1) 可以看到数组中元素的值:

package main
import "fmt"
func main(){
var arr1 []int
fmt.Println(arr1) // 输出为:[0 0 0 0 0]
}

你可以把此时数组在内存中的状态想象为下图所示的样子:

使用字面量初始化数组
我们可以通过字面量在声明数组的同时快速的初始化数组:

arr2 := []int{,,,,}

对于这种情况,还可以使用 … 代替数组的长度,让编译器根据实际的元素个数自行推断数组的长度:

arr3 := […]int{,,,,}

如果设置了数组的长度,还可以通过指定下标的方式初始化部分元素:

//  用具体值初始化索引为 1 和 3 的元素
arr4 := []int{:,:}

数组的内容如下图所示:

访问与修改数组元素

和其它类 C 语言一样,Go 语言数组通过数组下标(索引位置)来读取或者修改数组元素。下标(索引)从 0 开始,第一个元素的索引为 0,第二个索引为 1,依次类推。元素的数目(数组长度)必须是固定的并且在声明数组时就指定(编译器需要知道数组的长度以便分配内存),数组长度最大为 2G。

访问数组元素
对于数组 arr 来说,第一个元素就是 arr[0],第二个元素是 arr[1],最后一个元素则是 arr[len(arr)-1]。下面的代码定义一个整型数组,然后通过 for 循环打印数组中的每个元素:

package main
import "fmt"
func main(){
arr := []int{,,,,}
for i := ; i < len(arr); i++ {
fmt.Printf("At index %d is %d\n", i, arr[i])
}
}

运行上面的代码,输出如下:

At index  is
At index is
At index is
At index is
At index is

除了使用 len() 函数通过索引遍历数组,还可以使用更方便的 range,结果都是一样的:

for index,value := range arr {
fmt.Printf("At index %d is %d\n", index, value)
}

修改数组元素
要修改单个元素的值,直接通过下标访问元素并赋值就可以了:

arr := []int{,,,,}
arr[] =

指针数组
数组的元素除了是某个类型外,还可以是某个类型的指针,下面声明一个所有元素都是指针的数组,然后使用 * 运算符就可以访问元素指针所指向的值:

arr := []*int{: new(int), : new(int)}

new(TYPE) 函数会为一个 TYPE 类型的数据结构划分内存并执行默认的初始化操作,然后返回这个数据对象的指针,所以 new(int) 表示创建一个 int 类型的数据对象,同时返回指向这个对象的指针。

// 为索引为 0 和 1 的元素赋值
*arr[] =
*arr[] =

完成赋值后的结果如下:

我们还可以接着初始化剩下的元素并赋值:

arr[] = new(int)
arr[] = new(int)
arr[] = new(int)
*arr[] =

最后打印整个指针数组指向的内容:

for i := ; i < len(arr); i++ {
fmt.Printf("At index %d is %d\n", i, *arr[i])
}

结果如下:

At index  is
At index is
At index is
At index is
At index is

数组是值类型

在 Golang 中,数组是值类型,这意味着数组也可以用在赋值操作中。变量名代表整个数组,同类型的数组可以赋值给另一个数组:

var arr1 []string
arr2 := []string{"nick", "jack", "mark"}
// 把 arr2 的赋值(其实本质上是复制)到 arr1
arr1 = arr2

复制完成后两个数组的值完全一样,但是彼此之间没有任何关系:

前面我们不止一次地提到:数组的类型包括数组的长度和数组元素的类型。只有这两部分都一样才是相同类型的数组,也才能够互相赋值。下面的代码中,在类型不同的数组间赋值,编译器会阻止这样的操作并报错:

// 声明第一个包含 4 个元素的字符串数组
var arr1 []string
// 声明第二个包含 3 个元素的字符串数组,并初始化
arr2 := []string{"nick", "jack", "mark"}
// 将 arr2 赋值给 arr1
arr1 = arr2

编译器表示在赋值时不能把 type [3]string 当 type [4]string 用。

把数组赋值给其它数组时,实际上是完整地复制一个数组。所以,如果数组是一个指针型的数组,那么复制的将是指针,而不会复制指针所指向的对象。看下面的代码:

// 声明第一个包含 4 个元素的字符串数组
var arr1 []*string
// 声明第二个包含 3 个元素的字符串数组,并初始化
arr2 := []*string{new(string), new(string), new(string)}
*arr2[] = "nick"
*arr2[] = "jack"
*arr2[] = "mark"
// 将 arr2 赋值给 arr1
arr1 = arr2

在赋值完成后,两个数组指向的是同一组字符串:

把数组传递给函数

在 Golang 中数组是一个值类型,所有的值类型变量在赋值和作为参数传递时都将产生一次复制操作。如果直接将数组作为函数的参数,则在函数调用时数组会被复制一份传递给函数。因此,在函数体中无法修改源数组的内容,因为函数内操作的只是源数组的一个副本。
如此一来,从内存和性能上来看,在函数间传递数组是一个开销很大的操作。因为无论这个数组有多长,都会完整复制,并传递给函数。下面的 demo 中会声明一个包含 100 万个 int64 类型元素的数组,这会消耗掉 8MB 的内存:

func showArray(array [1e6]int64){
// do something
}
var arr [1e6]int64
showArray(arr)

每次函数 showArray 被调用时,必须在栈上分配 8MB 的内存。之后整个数组的值(8MB 内存) 被复制到刚刚分配的内存中。虽然 Golang 的运行时会自动处理这个复制操作,但这样做的效率实在是太低了,也太耗费内存!合理且高效的方式是只传入指向数组的指针,这样只需复制 8 个字节的数据到函数的栈上就可以了:

func showArray(array *[1e6]int64){
// do something
}
var arr [1e6]int64
showArray(&arr)

这段代码中的 showArray 函数接收一个指向包含 100 万个 int64 值的数组的指针,调用函数时传入的参数则是指向数组的指针。现在只需在栈上分配 8 个字节的内存给这个指针就行了。
这个方法能够更有效地利用内存,性能也更好。需要注意的是,此时在函数内外操作的都是同一个数组中的元素,会互相影响。

多维数组

多维数组的典型用例是平面坐标(二维数组)和三维坐标(三维数组),这里我们简单介绍一下二维数组。
Golang 的数组本身只有一个维度,但是我们可以组合多个数组从而创建出多维数组,下面是声明二维数组的实例代码:

// 声明一个二维整型数组,两个维度分别存储 4 个元素和 2 个元素
var arr [][]int
// 使用数组字面量来声明并初始化一个二维整型数组
arr1 := [][]int{{, }, {, }, {, }, {, }}
// 声明并初始化外层数组中索引为 1 和 3 的元素
arr2 := [][]int{: {, }, : {, }}
// 声明并初始化外层数组和内层数组的单个元素
arr3 := [][]int{: {: }, : {: }}

下图展示了上面代码声明的二维数组在每次声明并初始化后包含的值:

为了访问单个元素,需要反复组合使用 [] 运算符,比如:

arr1[][] = 

因为每个数组都是一个值,所以可以独立复制某个维度:

// 将 arr1 的索引为 1 的维度复制到一个同类型的新数组里
var arr4 []int = arr1[]
// 将外层数组的索引为 1、内层数组的索引为 0 的整型值复制到新的整型变量里
var value int = arr1[][]

总结

数组在 Golang 中是作为高性能的基础类型设计的,因此对用户来说使用起来并不是特别方便,这一点在众多的开源代码中(数组用的少,slice 用的多)可以得到印证。其实基于数组实现的 slice 以其简单灵活的特性更易于被大家接受,这也正是 Golang 设计 slice 的初衷。本文介绍了数组这个幕后大英雄,后面的文章会介绍 slice 的用法。

参考:
Golang Array  types
《Go语言编程》
《Go语言实战》
go基础系列:数组

Golang 入门 : 数组的更多相关文章

  1. Java程序员的Golang入门指南(上)

    Java程序员的Golang入门指南 1.序言 Golang作为一门出身名门望族的编程语言新星,像豆瓣的Redis平台Codis.类Evernote的云笔记leanote等. 1.1 为什么要学习 如 ...

  2. Golang入门(2):一天学完GO的基本语法

    摘要 在配置好环境之后,要研究的就是这个语言的语法了.在这篇文章中,作者希望可以简单的介绍一下Golang的各种语法,并与C和Java作一些简单的对比以加深记忆.因为这篇文章只是入门Golang的第二 ...

  3. Java程序员的Golang入门指南(下)

    Java程序员的Golang入门指南(下) 4.高级特性 上面介绍的只是Golang的基本语法和特性,尽管像控制语句的条件不用圆括号.函数多返回值.switch-case默认break.函数闭包.集合 ...

  4. Golang 入门 : 竞争条件

    笔者在前文<Golang 入门 : 理解并发与并行>和<Golang 入门 : goroutine(协程)>中介绍了 Golang 对并发的原生支持以及 goroutine 的 ...

  5. Golang 入门 : goroutine(协程)

    在操作系统中,执行体是个抽象的概念.与之对应的实体有进程.线程以及协程(coroutine).协程也叫轻量级的线程,与传统的进程和线程相比,协程的最大特点是 "轻"!可以轻松创建上 ...

  6. Golang 入门 : channel(通道)

    笔者在<Golang 入门 : 竞争条件>一文中介绍了 Golang 并发编程中需要面对的竞争条件.本文我们就介绍如何使用 Golang 提供的 channel(通道) 消除竞争条件. C ...

  7. 推荐一个GOLANG入门很好的网址

    推荐一个GOLANG入门很好的网址,栗子很全 https://books.studygolang.com/gobyexample/

  8. Golang入门(4):并发

    摘要 并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要.Web服务器会一次处理成千上万的请求,这也是并发的必要性之一.Golang的并发控制比起Java来说,简单了不少.在Go ...

  9. Golang入门(3):一天学完GO的进阶语法

    摘要 在上一篇文章中,我们聊了聊Golang中的一些基础的语法,如变量的定义.条件语句.循环语句等等.他们和其他语言很相似,我们只需要看一看它们之间的区别,就差不多可以掌握了,所以作者称它们为&quo ...

随机推荐

  1. 【憩园】C#并发编程之概述

    写在前面 并发编程一直都存在,只不过过去的很长时间里,比较难以实现,随着互联网的发展,人口红利的释放,更加友好的支持并发编程已经成了主流编程语言的标配,而对于软件开发人员来说,没有玩过并发编程都会有点 ...

  2. #7 找出数组中第k小的数

    「HW面试题」 [题目] 给定一个整数数组,如何快速地求出该数组中第k小的数.假如数组为[4,0,1,0,2,3],那么第三小的元素是1 [题目分析] 这道题涉及整数列表排序问题,直接使用sort方法 ...

  3. JDK动态代理深入理解分析并手写简易JDK动态代理(下)

    原文同步发表至个人博客[夜月归途] 原文链接:http://www.guitu18.com/se/java/2019-01-05/27.html 作者:夜月归途 出处:http://www.guitu ...

  4. Spring Cloud Alibaba基础教程:Nacos配置的多环境管理

    前情回顾: <Spring Cloud Alibaba基础教程:使用Nacos实现服务注册与发现> <Spring Cloud Alibaba基础教程:支持的几种服务消费方式> ...

  5. C# 如何获取Url的host以及是否是http

    参考资料:https://sites.google.com/site/netcorenote/asp-net-core/get-scheme-url-host Example there's an g ...

  6. Java中float型最大值大于long型?

    float型在内存中占用的是4个字节的空间,而long型占用的是8个字节的空间. 注:float类型的范围是:一3.403E38~3.403E38.而long类型的范围是:-2^63~2^63-1(大 ...

  7. 原生js获取元素非行内样式属性的方法

    获取当前对象的样式DOM标准中的全局方法 getComputedStyle(obj).width (获取元素的宽度),但在非标准IE浏览器(IE8)以下有兼容问题IE8以下要这样写 obj.curre ...

  8. 使用Git进行版本管理

    参考:http://www.runoob.com/git/git-tutorial.html 一.Git简介 1.Git 和 SVN 比较 (1)GIT是分布式的,SVN不是; (2)GIT把内容按元 ...

  9. Apex 中操作用户和组

    用户和组概述 Salesforce中对于用户的定义主要体现于两个对象:用户(User)和组(Group).组的成员可以是用户也可以是另一个组. Salesforce中的组可以有多种表示方法,比如队列( ...

  10. SQLServer之函数简介

    用户定义函数定义 与编程语言中的函数类似,SQL Server 用户定义函数是接受参数.执行操作(例如复杂计算)并将操作结果以值的形式返回的例程. 返回值可以是单个标量值或结果集. 用户定义函数准则 ...