标签 数组 下的文章

了解使用数组和切片在 Go 中存储数据的优缺点,以及为什么其中一个更好。

在本系列的第四篇文章中,我将解释 Go 数组和切片,包括如何使用它们,以及为什么你通常要选择其中一个而不是另一个。

数组

数组是编程语言中最流行的数据结构之一,主要原因有两个:一是简单易懂,二是可以存储许多不同类型的数据。

你可以声明一个名为 anArray 的 Go 数组,该数组存储四个整数,如下所示:

anArray := [4]int{-1, 2, 0, -4}

数组的大小应该在它的类型之前声明,而类型应该在声明元素之前定义。len() 函数可以帮助你得到任何数组的长度。上面数组的大小是 4。

如果你熟悉其他编程语言,你可能会尝试使用 for 循环来遍历数组。Go 当然也支持 for 循环,不过,正如你将在下面看到的,Go 的 range 关键字可以让你更优雅地遍历数组或切片。

最后,你也可以定义一个二维数组,如下:

twoD := [3][3]int{
  {1, 2, 3},
  {6, 7, 8},
  {10, 11, 12}}

arrays.go 源文件中包含了 Go 数组的示例代码。其中最重要的部分是:

for i := 0; i < len(twoD); i++ {
  k := twoD[i]
  for j := 0; j < len(k); j++ {
    fmt.Print(k[j], " ")
  }
  fmt.Println()
}

for _, a := range twoD {
  for _, j := range a {
    fmt.Print(j, " ")
  }
  fmt.Println()
}

通过上述代码,我们知道了如何使用 for 循环和 range 关键字迭代数组的元素。arrays.go 的其余代码则展示了如何将数组作为参数传递给函数。

以下是 arrays.go 的输出:

$ go run arrays.go
Before change(): [-1 2 0 -4]
After change(): [-1 2 0 -4]
1 2 3
6 7 8
10 11 12
1 2 3
6 7 8
10 11 12

这个输出告诉我们:对函数内的数组所做的更改,会在函数退出后丢失。

数组的缺点

Go 数组有很多缺点,你应该重新考虑是否要在 Go 项目中使用它们。

首先,数组定义之后,大小就无法改变,这意味着 Go 数组不是动态的。简而言之,如果你需要将一个元素添加到一个没有剩余空间的数组中,你将需要创建一个更大的数组,并将旧数组的所有元素复制到新数组中。

其次,当你将数组作为参数传递给函数时,实际上是传递了数组的副本,这意味着你对函数内部的数组所做的任何更改,都将在函数退出后丢失。

最后,将大数组传递给函数可能会很慢,主要是因为 Go 必须创建数组的副本。

以上这些问题的解决方案,就是使用 Go 切片。

切片

Go 切片与 Go 数组类似,但是它没有后者的缺点。

首先,你可以使用 append() 函数将元素添加到现有切片中。此外,Go 切片在内部使用数组实现,这意味着 Go 中每个切片都有一个底层数组。

切片具有 capacity 属性和 length 属性,它们并不总是相同的。切片的长度与元素个数相同的数组的长度相同,可以使用 len() 函数得到。切片的容量是当前为切片分配的空间,可以使用 cap() 函数得到。

由于切片的大小是动态的,如果切片空间不足(也就是说,当你尝试再向切片中添加一个元素时,底层数组的长度恰好与容量相等),Go 会自动将它的当前容量加倍,使其空间能够容纳更多元素,然后将请求的元素添加到底层数组中。

此外,切片是通过引用传递给函数的,这意味着实际传递给函数的是切片变量的内存地址,这样一来,你对函数内部的切片所做的任何修改,都不会在函数退出后丢失。因此,将大切片传递给函数,要比将具有相同数量元素的数组传递给同一函数快得多。这是因为 Go 不必拷贝切片 —— 它只需传递切片变量的内存地址。

slice.go 源文件中有 Go 切片的代码示例,其中包含以下代码:

package main

import (
  "fmt"
)

func negative(x []int) {
  for i, k := range x {
    x[i] = -k
  }
}

func printSlice(x []int) {
  for _, number := range x {
    fmt.Printf("%d ", number)
  }
  fmt.Println()
}

func main() {
  s := []int{0, 14, 5, 0, 7, 19}
  printSlice(s)
  negative(s)
  printSlice(s)

  fmt.Printf("Before. Cap: %d, length: %d\n", cap(s), len(s))
  s = append(s, -100)
  fmt.Printf("After. Cap: %d, length: %d\n", cap(s), len(s))
  printSlice(s)

  anotherSlice := make([]int, 4)
  fmt.Printf("A new slice with 4 elements: ")
  printSlice(anotherSlice)
}

切片和数组在定义方式上的最大区别就在于:你不需要指定切片的大小。实际上,切片的大小取决于你要放入其中的元素数量。此外,append() 函数允许你将元素添加到现有切片 —— 请注意,即使切片的容量允许你将元素添加到该切片,它的长度也不会被修改,除非你调用 append()。上述代码中的 printSlice() 函数是一个辅助函数,用于打印切片中的所有元素,而 negative() 函数将切片中的每个元素都变为各自的相反数。

运行 slice.go 将得到以下输出:

$ go run slice.go
0 14 5 0 7 19
0 -14 -5 0 -7 -19
Before. Cap: 6, length: 6
After. Cap: 12, length: 7
0 -14 -5 0 -7 -19 -100
A new slice with 4 elements: 0 0 0 0

请注意,当你创建一个新切片,并为给定数量的元素分配内存空间时,Go 会自动地将所有元素都初始化为其类型的零值,在本例中为 0(int 类型的零值)。

使用切片来引用数组

Go 允许你使用 [:] 语法,使用切片来引用现有的数组。在这种情况下,你对切片所做的任何更改都将传播到数组中 —— 详见 refArray.go。请记住,使用 [:] 不会创建数组的副本,它只是对数组的引用。

refArray.go 中最有趣的部分是:

func main() {
  anArray := [5]int{-1, 2, -3, 4, -5}
  refAnArray := anArray[:]

  fmt.Println("Array:", anArray)
  printSlice(refAnArray)
  negative(refAnArray)
  fmt.Println("Array:", anArray)
}

运行 refArray.go,输出如下:

$ go run refArray.go
Array: [-1 2 -3 4 -5]
-1 2 -3 4 -5
Array: [1 -2 3 -4 5]

我们可以发现:对 anArray 数组的切片引用进行了操作后,它本身也被改变了。

总结

尽管 Go 提供了数组和切片两种类型,你很可能还是会使用切片,因为它们比 Go 数组更加通用、强大。只有少数情况需要使用数组而不是切片,特别是当你完全确定元素的数量固定不变时。

你可以在 GitHub 上找到 arrays.goslice.gorefArray.go 的源代码。

如果你有任何问题或反馈,请在下方发表评论或在 Twitter 上与我联系。


via: https://opensource.com/article/18/7/introduction-go-arrays-and-slices

作者:Mihalis Tsoukalos 选题:lkxed 译者:lkxed 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

数组是一种有用的数据类型,用于管理在连续内存位置中建模最好的集合元素。下面是如何有效地使用它们。

 title=

有使用 C 或者 FORTRAN 语言编程经验的人会对数组的概念很熟悉。它们基本上是一个连续的内存块,其中每个位置都是某种数据类型:整型、浮点型或者诸如此类的数据类型。

Java 的情况与此类似,但是有一些额外的问题。

一个数组的示例

让我们在 Java 中创建一个长度为 10 的整型数组:

int[] ia = new int[10];

上面的代码片段会发生什么?从左到右依次是:

  1. 最左边的 int[] 将变量的类型声明为 int 数组(由 [] 表示)。
  2. 它的右边是变量的名称,当前为 ia
  3. 接下来,= 告诉我们,左侧定义的变量赋值为右侧的内容。
  4. = 的右侧,我们看到了 new,它在 Java 中表示一个对象正在被初始化中,这意味着已为其分配存储空间并调用了其构造函数(请参见此处以获取更多信息)。
  5. 然后,我们看到 int[10],它告诉我们正在初始化的这个对象是包含 10 个整型的数组。

因为 Java 是强类型的,所以变量 ia 的类型必须跟 = 右侧表达式的类型兼容。

初始化示例数组

让我们把这个简单的数组放在一段代码中,并尝试运行一下。将以下内容保存到一个名为 Test1.java 的文件中,使用 javac 编译,使用 java 运行(当然是在终端中):

import java.lang.*;

public class Test1 {

    public static void main(String[] args) {
        int[] ia = new int[10];                              // 见下文注 1
        System.out.println("ia is " + ia.getClass());        // 见下文注 2
        for (int i = 0; i < ia.length; i++)                  // 见下文注 3
            System.out.println("ia[" + i + "] = " + ia[i]);  // 见下文注 4
    }

}

让我们来看看最重要的部分。

  1. 我们声明和初始化了长度为 10 的整型数组,即 ia,这显而易见。
  2. 在下面的行中,我们看到表达式 ia.getClass()。没错,ia 是属于一个对象,这行代码将告诉我们是哪个类。
  3. 在紧接的下一行中,我们看到了一个循环 for (int i = 0; i < ia.length; i++),它定义了一个循环索引变量 i,该变量遍历了从 0 到比 ia.length 小 1 的序列,这个表达式告诉我们在数组 ia 中定义了多少个元素。
  4. 接下来,循环体打印出 ia 的每个元素的值。

当这个程序编译和运行时,它产生以下结果:

me@mydesktop:~/Java$ javac Test1.java
me@mydesktop:~/Java$ java Test1
ia is class [I
ia[0] = 0
ia[1] = 0
ia[2] = 0
ia[3] = 0
ia[4] = 0
ia[5] = 0
ia[6] = 0
ia[7] = 0
ia[8] = 0
ia[9] = 0
me@mydesktop:~/Java$

ia.getClass() 的输出的字符串表示形式是 [I,它是“整数数组”的简写。与 C 语言类似,Java 数组以第 0 个元素开始,扩展到第 <数组大小> - 1 个元素。如上所见,我们可以看到数组 ia 的每个元素都(似乎由数组构造函数)设置为零。

所以,就这些吗?声明类型,使用适当的初始化器,就完成了吗?

好吧,并没有。在 Java 中有许多其它方法来初始化数组。

为什么我要初始化一个数组,有其它方式吗?

像所有好的问题一样,这个问题的答案是“视情况而定”。在这种情况下,答案取决于初始化后我们希望对数组做什么。

在某些情况下,数组自然会作为一种累加器出现。例如,假设我们正在编程实现计算小型办公室中一组电话分机接收和拨打的电话数量。一共有 8 个分机,编号为 1 到 8,加上话务员的分机,编号为 0。 因此,我们可以声明两个数组:

int[] callsMade;
int[] callsReceived;

然后,每当我们开始一个新的累计呼叫统计数据的周期时,我们就将每个数组初始化为:

callsMade = new int[9];
callsReceived = new int[9];

在每个累计通话统计数据的最后阶段,我们可以打印出统计数据。粗略地说,我们可能会看到:

import java.lang.*;
import java.io.*;

public class Test2 {

    public static void main(String[] args) {

        int[] callsMade;
        int[] callsReceived;

        // 初始化呼叫计数器

        callsMade = new int[9];
        callsReceived = new int[9];

        // 处理呼叫……
        //   分机拨打电话:callsMade[ext]++
        //   分机接听电话:callsReceived[ext]++

        // 汇总通话统计

        System.out.printf("%3s%25s%25s\n", "ext", " calls made",
                "calls received");
        for (int ext = 0; ext < callsMade.length; ext++) {
            System.out.printf("%3d%25d%25d\n", ext,
                    callsMade[ext], callsReceived[ext]);
        }

    }

}

这会产生这样的输出:

me@mydesktop:~/Java$ javac Test2.java
me@mydesktop:~/Java$ java Test2
ext               calls made           calls received
  0                        0                        0
  1                        0                        0
  2                        0                        0
  3                        0                        0
  4                        0                        0
  5                        0                        0
  6                        0                        0
  7                        0                        0
  8                        0                        0
me@mydesktop:~/Java$

看来这一天呼叫中心不是很忙。

在上面的累加器示例中,我们看到由数组初始化程序设置的零起始值可以满足我们的需求。但是在其它情况下,这个起始值可能不是正确的选择。

例如,在某些几何计算中,我们可能需要将二维数组初始化为单位矩阵(除沿主对角线———左上角到右下角——以外所有全是零)。我们可以选择这样做:

double[][] m = new double[3][3];
for (int d = 0; d < 3; d++) {
    m[d][d] = 1.0;
}

在这种情况下,我们依靠数组初始化器 new double[3][3] 将数组设置为零,然后使用循环将主对角线上的元素设置为 1。在这种简单情况下,我们可以使用 Java 提供的快捷方式:

double[][] m = {
        {1.0, 0.0, 0.0},
        {0.0, 1.0, 0.0},
        {0.0, 0.0, 1.0}};

这种可视结构特别适用于这种应用程序,在这种应用程序中,它便于复查数组的实际布局。但是在这种情况下,行数和列数只在运行时确定时,我们可能会看到这样的东西:

int nrc;
// 一些代码确定行数和列数 = nrc
double[][] m = new double[nrc][nrc];
for (int d = 0; d < nrc; d++) {
    m[d][d] = 1.0;
}

值得一提的是,Java 中的二维数组实际上是数组的数组,没有什么能阻止无畏的程序员让这些第二层数组中的每个数组的长度都不同。也就是说,下面这样的事情是完全合法的:

int [][] differentLengthRows = {
     {1, 2, 3, 4, 5},
     {6, 7, 8, 9},
     {10, 11, 12},
     {13, 14},
     {15}};

在涉及不规则形状矩阵的各种线性代数应用中,可以应用这种类型的结构(有关更多信息,请参见此 Wikipedia 文章)。除此之外,既然我们了解到二维数组实际上是数组的数组,那么以下内容也就不足为奇了:

differentLengthRows.length

可以告诉我们二维数组 differentLengthRows 的行数,并且:

differentLengthRows[i].length

告诉我们 differentLengthRowsi 行的列数。

深入理解数组

考虑到在运行时确定数组大小的想法,我们看到数组在实例化之前仍需要我们知道该大小。但是,如果在处理完所有数据之前我们不知道大小怎么办?这是否意味着我们必须先处理一次以找出数组的大小,然后再次处理?这可能很难做到,尤其是如果我们只有一次机会使用数据时。

Java 集合框架很好地解决了这个问题。提供的其中一项是 ArrayList 类,它类似于数组,但可以动态扩展。为了演示 ArrayList 的工作原理,让我们创建一个 ArrayList 对象并将其初始化为前 20 个斐波那契数字

import java.lang.*;
import java.util.*;

public class Test3 {

    public static void main(String[] args) {

        ArrayList<Integer> fibos = new ArrayList<Integer>();

        fibos.add(0);
        fibos.add(1);
        for (int i = 2; i < 20; i++) {
            fibos.add(fibos.get(i - 1) + fibos.get(i - 2));
        }

        for (int i = 0; i < fibos.size(); i++) {
            System.out.println("fibonacci " + i + " = " + fibos.get(i));
        }

    }
}

上面的代码中,我们看到:

  • 用于存储多个 IntegerArrayList 的声明和实例化。
  • 使用 add() 附加到 ArrayList 实例。
  • 使用 get() 通过索引号检索元素。
  • 使用 size() 来确定 ArrayList 实例中已经有多少个元素。

这里没有展示 put() 方法,它的作用是将一个值放在给定的索引号上。

该程序的输出为:

fibonacci 0 = 0
fibonacci 1 = 1
fibonacci 2 = 1
fibonacci 3 = 2
fibonacci 4 = 3
fibonacci 5 = 5
fibonacci 6 = 8
fibonacci 7 = 13
fibonacci 8 = 21
fibonacci 9 = 34
fibonacci 10 = 55
fibonacci 11 = 89
fibonacci 12 = 144
fibonacci 13 = 233
fibonacci 14 = 377
fibonacci 15 = 610
fibonacci 16 = 987
fibonacci 17 = 1597
fibonacci 18 = 2584
fibonacci 19 = 4181

ArrayList 实例也可以通过其它方式初始化。例如,可以给 ArrayList 构造器提供一个数组,或者在编译过程中知道初始元素时也可以使用 List.of()array.aslist() 方法。我发现自己并不经常使用这些方式,因为我对 ArrayList 的主要用途是当我只想读取一次数据时。

此外,对于那些喜欢在加载数据后使用数组的人,可以使用 ArrayListtoArray() 方法将其实例转换为数组;或者,在初始化 ArrayList 实例之后,返回到当前数组本身。

Java 集合框架提供了另一种类似数组的数据结构,称为 Map(映射)。我所说的“类似数组”是指 Map 定义了一个对象集合,它的值可以通过一个键来设置或检索,但与数组(或 ArrayList)不同,这个键不需要是整型数;它可以是 String 或任何其它复杂对象。

例如,我们可以创建一个 Map,其键为 String,其值为 Integer 类型,如下:

Map<String, Integer> stoi = new Map<String, Integer>();

然后我们可以对这个 Map 进行如下初始化:

stoi.set("one",1);
stoi.set("two",2);
stoi.set("three",3);

等类似操作。稍后,当我们想要知道 "three" 的数值时,我们可以通过下面的方式将其检索出来:

stoi.get("three");

在我的认知中,Map 对于将第三方数据集中出现的字符串转换为我的数据集中的一致代码值非常有用。作为数据转换管道的一部分,我经常会构建一个小型的独立程序,用作在处理数据之前清理数据;为此,我几乎总是会使用一个或多个 Map

值得一提的是,ArrayListArrayListMapMap 是很可能的,有时也是合理的。例如,假设我们在看树,我们对按树种和年龄范围累计树的数目感兴趣。假设年龄范围定义是一组字符串值(“young”、“mid”、“mature” 和 “old”),物种是 “Douglas fir”、“western red cedar” 等字符串值,那么我们可以将这个 Map 中的 Map 定义为:

Map<String, Map<String, Integer>> counter = new Map<String, Map<String, Integer>>();

这里需要注意的一件事是,以上内容仅为 Map创建存储。因此,我们的累加代码可能类似于:

// 假设我们已经知道了物种和年龄范围
if (!counter.containsKey(species)) {
    counter.put(species,new Map<String, Integer>());
}
if (!counter.get(species).containsKey(ageRange)) {
    counter.get(species).put(ageRange,0);
}

此时,我们可以这样开始累加:

counter.get(species).put(ageRange, counter.get(species).get(ageRange) + 1);

最后,值得一提的是(Java 8 中的新特性)Streams 还可以用来初始化数组、ArrayList 实例和 Map 实例。关于此特性的详细讨论可以在此处此处中找到。


via: https://opensource.com/article/19/10/initializing-arrays-java

作者:Chris Hermansen 选题:lujun9972 译者:laingke 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

进入这个古怪而神奇的 Bash 数组的世界。

尽管软件工程师常常使用命令行来进行各种开发,但命令行中的数组似乎总是一个模糊的东西(虽然不像正则操作符 =~ 那么复杂隐晦)。除开隐晦和有疑问的语法,Bash 数组其实是非常有用的。

稍等,这是为什么?

写 Bash 相关的东西很难,但如果是写一篇像手册那样注重怪异语法的文章,就会非常简单。不过请放心,这篇文章的目的就是让你不用去读该死的使用手册。

真实(通常是有用的)示例

为了这个目的,想象一下真实世界的场景以及 Bash 是怎么帮忙的:你正在公司里面主导一个新工作,评估并优化内部数据管线的运行时间。首先,你要做个参数扫描分析来评估管线使用线程的状况。简单起见,我们把这个管道当作一个编译好的 C++ 黑盒子,这里面我们能够调整的唯一的参数是用于处理数据的线程数量:./pipeline --threads 4

基础

我们首先要做的事是定义一个数组,用来容纳我们想要测试的 --threads 参数:

allThreads=(1 2 4 8 16 32 64 128)

本例中,所有元素都是数字,但参数并不一定是数字,Bash 中的数组可以容纳数字和字符串,比如 myArray=(1 2 "three" 4 "five") 就是个有效的表达式。就像 Bash 中其它的变量一样,确保赋值符号两边没有空格。否则 Bash 将会把变量名当作程序来执行,把 = 当作程序的第一个参数。

现在我们初始化了数组,让我们解析它其中的一些元素。仅仅输入 echo $allThreads ,你能发现,它只会输出第一个元素。

要理解这个产生的原因,需要回到上一步,回顾我们一般是怎么在 Bash 中输出变量。考虑以下场景:

type="article"
echo "Found 42 $type"

假如我们得到的变量 $type 是一个单词,我们想要添加在句子结尾一个 s。我们无法直接把 s 加到 $type 里面,因为这会把它变成另一个变量,$types。尽管我们可以利用像 echo "Found 42 "$type"s" 这样的代码形变,但解决这个问题的最好方法是用一个花括号:echo "Found 42 ${type}s",这让我们能够告诉 Bash 变量名的起止位置(有趣的是,JavaScript/ES6 在 template literals 中注入变量和表达式的语法和这里是一样的)

事实上,尽管 Bash 变量一般不用花括号,但在数组中需要用到花括号。这反而允许我们指定要访问的索引,例如 echo ${allThreads[1]} 返回的是数组中的第二个元素。如果不写花括号,比如 echo $allThreads[1],会导致 Bash 把 [1] 当作字符串然后输出。

是的,Bash 数组的语法很怪,但是至少他们是从 0 开始索引的,不像有些语言(说的就是你,R 语言)。

遍历数组

上面的例子中我们直接用整数作为数组的索引,我们现在考虑两种其他情况:第一,如果想要数组中的第 $i 个元素,这里 $i 是一个代表索引的变量,我们可以这样 echo ${allThreads[$i]} 解析这个元素。第二,要输出一个数组的所有元素,我们把数字索引换成 @ 符号(你可以把 @ 当作表示 all 的符号):echo ${allThreads[@]}

遍历数组元素

记住上面讲过的,我们遍历 $allThreads 数组,把每个值当作 --threads 参数启动管线:

for t in ${allThreads[@]}; do
  ./pipeline --threads $t
done

遍历数组索引

接下来,考虑一个稍稍不同的方法。不遍历所有的数组元素,我们可以遍历所有的索引:

for i in ${!allThreads[@]}; do
  ./pipeline --threads ${allThreads[$i]}
done

一步一步看:如之前所见,${allThreads[@]} 表示数组中的所有元素。前面加了个感叹号,变成 ${!allThreads[@]},这会返回数组索引列表(这里是 0 到 7)。换句话说。for 循环就遍历所有的索引 $i 并从 $allThreads 中读取第 $i 个元素,当作 --threads 选项的参数。

这看上去很辣眼睛,你可能奇怪为什么我要一开始就讲这个。这是因为有时候在循环中需要同时获得索引和对应的值,例如,如果你想要忽视数组中的第一个元素,使用索引可以避免额外创建在循环中累加的变量。

填充数组

到目前为止,我们已经能够用给定的 --threads 选项启动管线了。现在假设按秒计时的运行时间输出到管线。我们想要捕捉每个迭代的输出,然后把它保存在另一个数组中,因此我们最终可以随心所欲的操作它。

一些有用的语法

在深入代码前,我们要多介绍一些语法。首先,我们要能解析 Bash 命令的输出。用这个语法可以做到:output=$( ./my_script.sh ),这会把命令的输出存储到变量 $output 中。

我们需要的第二个语法是如何把我们刚刚解析的值添加到数组中。完成这个任务的语法看起来很熟悉:

myArray+=( "newElement1" "newElement2" )

参数扫描

万事具备,执行参数扫描的脚步如下:

allThreads=(1 2 4 8 16 32 64 128)
allRuntimes=()
for t in ${allThreads[@]}; do
  runtime=$(./pipeline --threads $t)
  allRuntimes+=( $runtime )
done

就是这个了!

还有什么能做的?

这篇文章中,我们讲过使用数组进行参数扫描的场景。我敢保证有很多理由要使用 Bash 数组,这里就有两个例子:

日志警告

本场景中,把应用分成几个模块,每一个都有它自己的日志文件。我们可以编写一个 cron 任务脚本,当某个模块中出现问题标志时向特定的人发送邮件:

# 日志列表,发生问题时应该通知的人
logPaths=("api.log" "auth.log" "jenkins.log" "data.log")
logEmails=("jay@email" "emma@email" "jon@email" "sophia@email")

# 在每个日志中查找问题标志
for i in ${!logPaths[@]};
do
  log=${logPaths[$i]}
  stakeholder=${logEmails[$i]}
  numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l )

  # 如果近期发现超过 5 个错误,就警告负责人
  if [[ "$numErrors" -gt 5 ]];
  then
    emailRecipient="$stakeholder"
    emailSubject="WARNING: ${log} showing unusual levels of errors"
    emailBody="${numErrors} errors found in log ${log}"
    echo "$emailBody" | mailx -s "$emailSubject" "$emailRecipient"
  fi
done

API 查询

如果你想要生成一些分析数据,分析你的 Medium 帖子中用户评论最多的。由于我们无法直接访问数据库,SQL 不在我们考虑范围,但我们可以用 API!

为了避免陷入关于 API 授权和令牌的冗长讨论,我们将会使用 JSONPlaceholder,这是一个面向公众的测试服务 API。一旦我们查询每个帖子,解析出每个评论者的邮箱,我们就可以把这些邮箱添加到我们的结果数组里:

endpoint="https://jsonplaceholder.typicode.com/comments"
allEmails=()

# 查询前 10 个帖子
for postId in {1..10};
do
  # 执行 API 调用,获取该帖子评论者的邮箱
  response=$(curl "${endpoint}?postId=${postId}")
  
  # 使用 jq 把 JSON 响应解析成数组
  allEmails+=( $( jq '.[].email' <<< "$response" ) )
done

注意这里我是用 jq 工具 从命令行里解析 JSON 数据。关于 jq 的语法超出了本文的范围,但我强烈建议你了解它。

你可能已经想到,使用 Bash 数组在数不胜数的场景中很有帮助,我希望这篇文章中的示例可以给你思维的启发。如果你从自己的工作中找到其它的例子想要分享出来,请在帖子下方评论。

请等等,还有很多东西!

由于我们在本文讲了很多数组语法,这里是关于我们讲到内容的总结,包含一些还没讲到的高级技巧:

语法效果
arr=()创建一个空数组
arr=(1 2 3)初始化数组
${arr[2]}取得第三个元素
${arr[@]}取得所有元素
${!arr[@]}取得数组索引
${#arr[@]}计算数组长度
arr[0]=3覆盖第 1 个元素
arr+=(4)添加值
str=$(ls)ls 输出保存到字符串
arr=( $(ls) )ls 输出的文件保存到数组里
${arr[@]:s:n}取得从索引 s 开始的 n 个元素

最后一点思考

正如我们所见,Bash 数组的语法很奇怪,但我希望这篇文章让你相信它们很有用。只要你理解了这些语法,你会发现以后会经常使用 Bash 数组。

Bash 还是 Python?

问题来了:什么时候该用 Bash 数组而不是其他的脚本语法,比如 Python?

对我而言,完全取决于需求——如果你可以只需要调用命令行工具就能立马解决问题,你也可以用 Bash。但有些时候,当你的脚本属于一个更大的 Python 项目时,你也可以用 Python。

比如,我们可以用 Python 来实现参数扫描,但我们只用编写一个 Bash 的包装:

import subprocess

all_threads = [1, 2, 4, 8, 16, 32, 64, 128]
all_runtimes = []

# 用不同的线程数字启动管线
for t in all_threads:
  cmd = './pipeline --threads {}'.format(t)

  # 使用子线程模块获得返回的输出
  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
  output = p.communicate()[0]
  all_runtimes.append(output)

由于本例中没法避免使用命令行,所以可以优先使用 Bash。

羞耻的宣传时间

如果你喜欢这篇文章,这里还有很多类似的文章! 在此注册,加入 OSCON,2018 年 7 月 17 号我会在这做一个主题为 你所不了解的 Bash 的在线编码研讨会。没有幻灯片,不需要门票,只有你和我在命令行里面敲代码,探索 Bash 中的奇妙世界。

本文章由 [Medium] 首发,再发布时已获得授权。


via: https://opensource.com/article/18/5/you-dont-know-bash-intro-bash-arrays

作者:Robert Aboukhalil 选题:lujun9972 译者:BriFuture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

快速测试 - 下面的代码输出什么?

vals := make([]int, 5)  
for i := 0; i < 5; i++ {  
  vals = append(vals, i)
}
fmt.Println(vals)  

在 Go Playground 运行一下

如果你猜测的是 [0 0 0 0 0 0 1 2 3 4],那你是对的。

等等,什么? 为什么不是 [0 1 2 3 4]?

如果你在测试中做错了,你也不用担心。这是在过渡到 Go 语言的过程中相当常见的错误,在这篇文章中,我们将说明为什么输出不是你预期的,以及如何利用 Go 的细微差别来使你的代码更有效率。

切片 vs 数组

在 Go 中同时有数组(array)和切片(slice)。这可能令人困惑,但一旦你习惯了,你会喜欢上它。请相信我。

切片和数组之间存在许多差异,但我们要在本文中重点介绍的内容是数组的大小是其类型的一部分,而切片可以具有动态大小,因为它们是围绕数组的封装。

这在实践中意味着什么?那么假设我们有数组 val a [10]int。该数组具有固定大小,且无法更改。如果我们调用 len(a),它总是返回 10,因为这个大小是类型的一部分。因此,如果你突然需要在数组中超过 10 个项,则必须创建一个完全不同类型的新对象,例如 val b [11]int,然后将所有值从 a 复制到 b

在特定情况下,含有集合大小的数组是有价值的,但一般而言,这不是开发人员想要的。相反,他们希望在 Go 中使用类似于数组的东西,但是随着时间的推移,它们能够随时增长。一个粗略的方式是创建一个比它需要大得多的数组,然后将数组的一个子集视为数组。下面的代码是个例子。

var vals [20]int  
for i := 0; i < 5; i++ {  
  vals[i] = i * i
}
subsetLen := 5

fmt.Println("The subset of our array has a length of:", subsetLen)

// Add a new item to our array
vals[subsetLen] = 123  
subsetLen++  
fmt.Println("The subset of our array has a length of:", subsetLen)  

在 Go Playground 中运行

在代码中,我们有一个长度为 20 的数组,但是由于我们只使用一个子集,代码中我们可以假定数组的长度是 5,然后在我们向数组中添加一个新的项之后是 6

这是(非常粗略地说)切片是如何工作的。它们包含一个具有设置大小的数组,就像我们前面的例子中的数组一样,它的大小为 20

它们还跟踪程序中使用的数组的子集 - 这就是 append 属性,它类似于上一个例子中的 subsetLen 变量。

最后,一个切片还有一个 capacity,类似于前面例子中我们的数组的总长度(20)。这是很有用的,因为它会告诉你的子集在无法容纳切片数组之前可以增长的大小。当发生这种情况时,需要分配一个新的数组,但所有这些逻辑都隐藏在 append 函数的后面。

简而言之,使用 append 函数组合切片给我们一个非常类似于数组的类型,但随着时间的推移,它可以处理更多的元素。

我们再来看一下前面的例子,但是这次我们将使用切片而不是数组。

var vals []int  
for i := 0; i < 5; i++ {  
  vals = append(vals, i)
  fmt.Println("The length of our slice is:", len(vals))
  fmt.Println("The capacity of our slice is:", cap(vals))
}

// Add a new item to our array
vals = append(vals, 123)  
fmt.Println("The length of our slice is:", len(vals))  
fmt.Println("The capacity of our slice is:", cap(vals))

// Accessing items is the same as an array
fmt.Println(vals[5])  
fmt.Println(vals[2])  

在 Go Playground 中运行

我们仍然可以像数组一样访问我们的切片中的元素,但是通过使用切片和 append 函数,我们不再需要考虑背后数组的大小。我们仍然可以通过使用 lencap 函数来计算出这些东西,但是我们不用担心太多。简洁吧?

回到测试

记住这点,让我们回顾前面的测试,看下什么出错了。

vals := make([]int, 5)  
for i := 0; i < 5; i++ {  
  vals = append(vals, i)
}
fmt.Println(vals)  

当调用 make 时,我们允许最多传入 3 个参数。第一个是我们分配的类型,第二个是类型的“长度”,第三个是类型的“容量”(这个参数是可选的)。

通过传递参数 make([]int, 5),我们告诉程序我们要创建一个长度为 5 的切片,在这种情况下,默认的容量与长度相同 - 本例中是 5。

虽然这可能看起来像我们想要的那样,这里的重要区别是我们告诉我们的切片,我们要将“长度”和“容量”设置为 5,假设你想要在初始的 5 个元素之后添加新的元素,我们接着调用 append 函数,那么它会增加容量的大小,并且会在切片的最后添加新的元素。

如果在代码中添加一条 Println() 语句,你可以看到容量的变化。

vals := make([]int, 5)  
fmt.Println("Capacity was:", cap(vals))  
for i := 0; i < 5; i++ {  
  vals = append(vals, i)
  fmt.Println("Capacity is now:", cap(vals))
}

fmt.Println(vals)  

在 Go Playground 中运行

最后,我们最终得到 [0 0 0 0 0 0 1 2 3 4] 的输出而不是希望的 [0 1 2 3 4]

如何修复它呢?好的,这有几种方法,我们将讲解两种,你可以选取任何一种在你的场景中最有用的方法。

直接使用索引写入而不是 append

第一种修复是保留 make 调用不变,并且显式地使用索引来设置每个元素。这样,我们就得到如下的代码:

vals := make([]int, 5)  
for i := 0; i < 5; i++ {  
  vals[i] = i
}
fmt.Println(vals)  

在 Go Playground 中运行

在这种情况下,我们设置的值恰好与我们要使用的索引相同,但是你也可以独立跟踪索引。

比如,如果你想要获取 map 的键,你可以使用下面的代码。

package main

import "fmt"

func main() {  
  fmt.Println(keys(map[string]struct{}{
    "dog": struct{}{},
    "cat": struct{}{},
  }))
}

func keys(m map[string]struct{}) []string {  
  ret := make([]string, len(m))
  i := 0
  for key := range m {
    ret[i] = key
    i++
  }
  return ret
}

在 Go Playground 中运行

这样做很好,因为我们知道我们返回的切片的长度将与 map 的长度相同,因此我们可以用该长度初始化我们的切片,然后将每个元素分配到适当的索引中。这种方法的缺点是我们必须跟踪 i,以便了解每个索引要设置的值。

这就让我们引出了第二种方法……

使用 0 作为你的长度并指定容量

与其跟踪我们要添加的值的索引,我们可以更新我们的 make 调用,并在切片类型之后提供两个参数。第一个,我们的新切片的长度将被设置为 0,因为我们还没有添加任何新的元素到切片中。第二个,我们新切片的容量将被设置为 map 参数的长度,因为我们知道我们的切片最终会添加许多字符串。

这会如前面的例子那样仍旧会在背后构建相同的数组,但是现在当我们调用 append 时,它会将它们放在切片开始处,因为切片的长度是 0。

package main

import "fmt"

func main() {  
  fmt.Println(keys(map[string]struct{}{
    "dog": struct{}{},
    "cat": struct{}{},
  }))
}

func keys(m map[string]struct{}) []string {  
  ret := make([]string, 0, len(m))
  for key := range m {
    ret = append(ret, key)
  }
  return ret
}

在 Go Playground 中运行

如果 append 处理它,为什么我们还要担心容量呢?

接下来你可能会问:“如果 append 函数可以为我增加切片的容量,那我们为什么要告诉程序容量呢?”

事实是,在大多数情况下,你不必担心这太多。如果它使你的代码变得更复杂,只需用 var vals []int 初始化你的切片,然后让 append 函数处理接下来的事。

但这种情况是不同的。它并不是声明容量困难的例子,实际上这很容易确定我们的切片的最后容量,因为我们知道它将直接映射到提供的 map 中。因此,当我们初始化它时,我们可以声明切片的容量,并免于让我们的程序执行不必要的内存分配。

如果要查看额外的内存分配情况,请在 Go Playground 上运行以下代码。每次增加容量,程序都需要做一次内存分配。

package main

import "fmt"

func main() {  
  fmt.Println(keys(map[string]struct{}{
    "dog":       struct{}{},
    "cat":       struct{}{},
    "mouse":     struct{}{},
    "wolf":      struct{}{},
    "alligator": struct{}{},
  }))
}

func keys(m map[string]struct{}) []string {  
  var ret []string
  fmt.Println(cap(ret))
  for key := range m {
    ret = append(ret, key)
    fmt.Println(cap(ret))
  }
  return ret
}

在 Go Playground 中运行

现在将此与相同的代码进行比较,但具有预定义的容量。

package main

import "fmt"

func main() {  
  fmt.Println(keys(map[string]struct{}{
    "dog":       struct{}{},
    "cat":       struct{}{},
    "mouse":     struct{}{},
    "wolf":      struct{}{},
    "alligator": struct{}{},
  }))
}

func keys(m map[string]struct{}) []string {  
  ret := make([]string, 0, len(m))
  fmt.Println(cap(ret))
  for key := range m {
    ret = append(ret, key)
    fmt.Println(cap(ret))
  }
  return ret
}

在 Go Playground 中运行

在第一个代码示例中,我们的容量从 0 开始,然后增加到 124, 最后是 8,这意味着我们不得不分配 5 次数组,最后一个容纳我们切片的数组的容量是 8,这比我们最终需要的要大。

另一方面,我们的第二个例子开始和结束都是相同的容量(5),它只需要在 keys() 函数的开头分配一次。我们还避免了浪费任何额外的内存,并返回一个能放下这个数组的完美大小的切片。

不要过分优化

如前所述,我通常不鼓励任何人做这样的小优化,但如果最后大小的效果真的很明显,那么我强烈建议你尝试为切片设置适当的容量或长度。

这不仅有助于提高程序的性能,还可以通过明确说明输入的大小和输出的大小之间的关系来帮助澄清你的代码。

总结

你好!我写了很多关于Go、Web 开发和其他我觉得有趣的话题。

如果你想跟上最新的文章,请注册我的邮件列表。我会给你发送我新书的样例、Go 的 Web 开发、以及每当有新文章(通常每周 1-2 次)会给你发送邮件。

哦,我保证不会发垃圾邮件。我像你一样讨厌它 :)

本文并不是对切片或数组之间差异的详细讨论,而是简要介绍了容量和长度如何影响切片,以及它们在方案中的用途。

为了进一步阅读,我强烈推荐 Go 博客中的以下文章:


作者简介:

Jon 是一名软件顾问,也是 《Web Development with Go》 一书的作者。在此之前,他创立了 EasyPost,一家 Y Combinator 支持的创业公司,并在 Google 工作。

https://www.usegolang.com


via: https://www.calhoun.io/how-to-use-slice-capacity-and-length-in-go

作者:Jon Calhoun 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出