数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素(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. Data Source与数据库连接池简介 JDBC简介(八)

    DataSource是作为DriverManager的替代品而推出的,DataSource 对象是获取连接的首选方法. 起源 为何放弃DriverManager DriverManager负责管理驱动 ...

  2. 【转】MVC HtmlHelper用法大全

    HtmlHelper用来在视图中呈现 HTML 控件. 以下列表显示了当前可用的一些 HTML 帮助器. 本主题演示所列出的带有星号 (*) 的帮助器. ActionLink - 链接到操作方法. B ...

  3. vb.net連接SQL数据库

    '導入命名空間Imports System.Data.SqlClient '定義變量 Dim Sql As String 'SQL字串 Dim Sqlado As SqlConnection '连接数 ...

  4. 结合JDK源码看设计模式——享元模式

    前言 在说享元模式之前,你一定见到过这样的面试题 public class Test { public static void main(String[] args) { Integer a=Inte ...

  5. Android安全–加强版Smali Log注入

    有的时候我们需要注入smali调用Log输出,打印字符串的值. 比如说: 如果我们要打印下面v1的值. new-instance v1, Ljava/lang/String; const-string ...

  6. Spring笔记02_注解_IOC

    目录 Spring笔记02 1. Spring整合连接池 1.1 Spring整合C3P0 1.2 Spring整合DBCP 1.3 最终版 2. 基于注解的IOC配置 2.1 导包 2.2 配置文件 ...

  7. Asp.Net MVC WebAPI的创建与前台Jquery ajax后台HttpClient调用详解

    1.什么是WebApi,它有什么用途? Web API是一个比较宽泛的概念.这里我们提到Web API特指ASP.NET MVC Web API.在新出的MVC中,增加了WebAPI,用于提供REST ...

  8. 亚马逊 amazon connect(呼叫中心)

    背景 公司为提高客服部门沟通效率对接电话呼叫中心,调研后选择了亚马逊的Amazon Connect服务,因为是国外业务没有选择用阿里云,怕有坑. Amazon Connect后台 需要在后台创建“联系 ...

  9. Flutter数据持久化入门以及与Web开发的对比

    对于大部分安卓或者IOS开发人员来说,App的数据持久化可能是很平常的一个话题.但是对于Web开发人员来说,可能紧紧意味着localStorage和sessionStorage. Web开发 loca ...

  10. 设计模式系列之策略模式(Strategy Pattern)

    意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换. 主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护. 何时使用:一个系统有许多许多类,而区分它 ...