Golang 入门 : 数组
数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素(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 入门 : 数组的更多相关文章
- Java程序员的Golang入门指南(上)
Java程序员的Golang入门指南 1.序言 Golang作为一门出身名门望族的编程语言新星,像豆瓣的Redis平台Codis.类Evernote的云笔记leanote等. 1.1 为什么要学习 如 ...
- Golang入门(2):一天学完GO的基本语法
摘要 在配置好环境之后,要研究的就是这个语言的语法了.在这篇文章中,作者希望可以简单的介绍一下Golang的各种语法,并与C和Java作一些简单的对比以加深记忆.因为这篇文章只是入门Golang的第二 ...
- Java程序员的Golang入门指南(下)
Java程序员的Golang入门指南(下) 4.高级特性 上面介绍的只是Golang的基本语法和特性,尽管像控制语句的条件不用圆括号.函数多返回值.switch-case默认break.函数闭包.集合 ...
- Golang 入门 : 竞争条件
笔者在前文<Golang 入门 : 理解并发与并行>和<Golang 入门 : goroutine(协程)>中介绍了 Golang 对并发的原生支持以及 goroutine 的 ...
- Golang 入门 : goroutine(协程)
在操作系统中,执行体是个抽象的概念.与之对应的实体有进程.线程以及协程(coroutine).协程也叫轻量级的线程,与传统的进程和线程相比,协程的最大特点是 "轻"!可以轻松创建上 ...
- Golang 入门 : channel(通道)
笔者在<Golang 入门 : 竞争条件>一文中介绍了 Golang 并发编程中需要面对的竞争条件.本文我们就介绍如何使用 Golang 提供的 channel(通道) 消除竞争条件. C ...
- 推荐一个GOLANG入门很好的网址
推荐一个GOLANG入门很好的网址,栗子很全 https://books.studygolang.com/gobyexample/
- Golang入门(4):并发
摘要 并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要.Web服务器会一次处理成千上万的请求,这也是并发的必要性之一.Golang的并发控制比起Java来说,简单了不少.在Go ...
- Golang入门(3):一天学完GO的进阶语法
摘要 在上一篇文章中,我们聊了聊Golang中的一些基础的语法,如变量的定义.条件语句.循环语句等等.他们和其他语言很相似,我们只需要看一看它们之间的区别,就差不多可以掌握了,所以作者称它们为&quo ...
随机推荐
- 【深度学习系列】用PaddlePaddle进行车牌识别(二)
上节我们讲了第一部分,如何用生成简易的车牌,这节课中我们会用PaddlePaddle来识别生成的车牌. 数据读取 在上一节生成车牌时,我们可以分别生成训练数据和测试数据,方法如下(完整代码在这里): ...
- 冒泡排序/选择排序/插入排序(c#)
---恢复内容开始--- 每次看这些排序都像没见过一样,完全理解不了,可是不久前明明了解的十分透彻.记下来记下来记下来! 1>>>冒泡排序:相邻的两两相比 把大的(或者小的)放后边, ...
- 视频拉流 Linux安装FFmpeg
1 下载最新源码包并解压 $ wget http://ffmpeg.org/releases/ffmpeg-3.1.3.tar.bz2 $ tar jxvf ffmpeg-.tar.bz2 2安装ya ...
- #WEB安全基础 : HTML/CSS | 文章索引
本系列讲解WEB安全所需要的HTML和CSS #WEB安全基础 : HTML/CSS | 0x0 我的第一个网页 #WEB安全基础 : HTML/CSS | 0x1初识CSS #WEB安全基础 : H ...
- 安装完成Dynamics 365 CE后别忘了更改维护作业的运行时间
摘要: 微软动态CRM专家罗勇 ,回复309或者20190308可方便获取本文,同时可以在第一间得到我发布的最新博文信息,follow me!我的网站是 www.luoyong.me . 安装完毕Dy ...
- C# MessageBox自动关闭
本文以一个简单的小例子,介绍如何让MessageBox弹出的对话框,在几秒钟内自动关闭.特别是一些第三方插件(如:dll)弹出的对话框,最为适用.本文仅供学习分享使用,如有不足之处,还请指正. 概述 ...
- Android为TV端助力(转载)
作者地址http://www.jianshu.com/u/63915ef020e2 针对Android Tv的自定义RecyclerView 作者 wenju_song 关注 2016.12.09 1 ...
- nginx配置proxy_pass URL末尾加与不加/(斜线)的区别
nginx在配置proxy_pass的时候 URL结尾加斜线(/)与不加的区别和注意事项 假设访问路径的 /pss/bill.html 加/斜线的情况 location /pss/ { proxy_p ...
- Spark之谓词下推
谓词下推就是指将各个条件先应用到对应的数据上,而不是根据写入的顺序执行,这样就可以先过滤掉部分数据,降低join等一系列操作的数据量级,提高运算速度,如下图:
- Github速度慢的解决方法
首先ping一下github.global.ssl.fastly.net 得到相应的ip,例如我现在ping的ip是151.101.41.194 151.101.41.194 github.globa ...