分类 软件开发 下的文章

了解使用数组和切片在 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中国 荣誉推出

本文是 Go 系列的第三篇文章,我将介绍三种最流行的复制文件的方法。

本文将介绍展示如何使用 Go 编程语言 来复制文件。在 Go 中复制文件的方法有很多,我只介绍三种最常见的:使用 Go 库中的 io.Copy() 函数调用、一次读取输入文件并将其写入另一个文件,以及使用缓冲区一块块地复制文件。

方法一:使用 io.Copy()

第一种方法就是使用 Go 标准库的 io.Copy() 函数。你可以在 copy() 函数的代码中找到它的实现逻辑,如下所示:

func copy(src, dst string) (int64, error) {
  sourceFileStat, err := os.Stat(src)
  if err != nil {
    return 0, err
  }

  if !sourceFileStat.Mode().IsRegular() {
    return 0, fmt.Errorf("%s is not a regular file", src)
  }

  source, err := os.Open(src)
  if err != nil {
    return 0, err
  }
  defer source.Close()

  destination, err := os.Create(dst)
  if err != nil {
    return 0, err
  }
  defer destination.Close()
  nBytes, err := io.Copy(destination, source)
    return nBytes, err
  }

首先,上述代码做了两个判断,以便确定它可以被打开读取:一是判断将要复制的文件是否存在(os.Stat(src)),二是判断它是否为常规文件(sourceFileStat.Mode().IsRegular())。剩下的所有工作都由 io.Copy(destination, source) 这行代码来完成。io.Copy() 函数执行结束后,会返回复制的字节数和复制过程中发生的第一条错误消息。在 Go 中,如果没有错误消息,错误变量的值就为 nil

你可以在 io 包 的文档页面了解有关 io.Copy() 函数的更多信息。

运行 cp1.go 将产生以下输出:

$ go run cp1.go
Please provide two command line arguments!
$ go run cp1.go fileCP.txt /tmp/fileCPCOPY
Copied 3826 bytes!
$ diff fileCP.txt /tmp/fileCPCOPY

这个方法已经非常简单了,不过它没有为开发者提供灵活性。这并不总是一件坏事,但是,有些时候,开发者可能会需要/想要告诉程序该如何读取文件。

方法二:使用 ioutil.WriteFile() 和 ioutil.ReadFile()

复制文件的第二种方法是使用 ioutil.ReadFile()ioutil.WriteFile() 函数。第一个函数用于将整个文件的内容,一次性地读入到某个内存中的字节切片里;第二个函数则用于将字节切片的内容写入到一个磁盘文件中。

实现代码如下:

input, err := ioutil.ReadFile(sourceFile)
if err != nil {
  fmt.Println(err)
  return
}

err = ioutil.WriteFile(destinationFile, input, 0644)
if err != nil {
  fmt.Println("Error creating", destinationFile)
  fmt.Println(err)
  return
}

上述代码包括了两个 if 代码块(嗯,用 Go 写程序就是这样的),程序的实际功能其实体现在 ioutil.ReadFile()ioutil.WriteFile() 这两行代码中。

运行 cp2.go,你会得到下面的输出:

$ go run cp2.go
Please provide two command line arguments!
$ go run cp2.go fileCP.txt /tmp/copyFileCP
$ diff fileCP.txt /tmp/copyFileCP

请注意,虽然这种方法能够实现文件复制,但它在复制大文件时的效率可能不高。这是因为当文件很大时,ioutil.ReadFile() 返回的字节切片会很大。

方法三:使用 os.Read() 和 os.Write()

在 Go 中复制文件的第三种方法就是下面要介绍的 cp3.go。它接受三个参数:输入文件名、输出文件名和缓冲区大小。

cp3.go 最重要的部分位于以下 for 循环中,你可以在 copy() 函数中找到它,如下所示:

buf := make([]byte, BUFFERSIZE)
for {
  n, err := source.Read(buf)
  if err != nil && err != io.EOF {
    return err
  }
  if n == 0 {
    break
  }

  if _, err := destination.Write(buf[:n]); err != nil {
    return err
  }
}

该方法使用 os.Read() 将输入文件的一小部分读入名为 buf 的缓冲区,然后使用 os.Write() 将该缓冲区的内容写入文件。当读取出错或到达文件末尾(io.EOF)时,复制过程将停止。

运行 cp3.go,你会得到下面的输出:

$ go run cp3.go
usage: cp3 source destination BUFFERSIZE
$ go run cp3.go fileCP.txt /tmp/buf10 10
Copying fileCP.txt to /tmp/buf10
$ go run cp3.go fileCP.txt /tmp/buf20 20
Copying fileCP.txt to /tmp/buf20

在接下来的基准测试中,你会发现,缓冲区的大小极大地影响了 cp3.go 的性能。

运行基准测试

在本文的最后一部分,我将尝试比较这三个程序以及 cp3.go 在不同缓冲区大小下的性能(使用 time(1) 命令行工具)。

以下输出显示了复制 500MB 大小的文件时,cp1.gocp2.gocp3.go 的性能对比:

$ ls -l INPUT
-rw-r--r--  1 mtsouk  staff  512000000 Jun  5 09:39 INPUT
$ time go run cp1.go INPUT /tmp/cp1
Copied 512000000 bytes!

real    0m0.980s
user    0m0.219s
sys     0m0.719s
$ time go run cp2.go INPUT /tmp/cp2

real    0m1.139s
user    0m0.196s
sys     0m0.654s
$ time go run cp3.go INPUT /tmp/cp3 1000000
Copying INPUT to /tmp/cp3

real    0m1.025s
user    0m0.195s
sys     0m0.486s

我们可以看出,这三个程序的性能非常接近,这意味着 Go 标准库函数的实现非常聪明、经过了充分优化。

现在,让我们测试一下缓冲区大小对 cp3.go 的性能有什么影响吧!执行 cp3.go,并分别指定缓冲区大小为 10、20 和 1000 字节,在一台运行很快的机器上复制 500MB 文件,得到的结果如下:

$ ls -l INPUT
-rw-r--r--  1 mtsouk  staff  512000000 Jun  5 09:39 INPUT
$ time go run cp3.go INPUT /tmp/buf10 10
Copying INPUT to /tmp/buf10

real    6m39.721s
user    1m18.457s
sys 5m19.186s
$ time go run cp3.go INPUT /tmp/buf20 20
Copying INPUT to /tmp/buf20

real    3m20.819s
user    0m39.444s
sys 2m40.380s
$ time go run cp3.go INPUT /tmp/buf1000 1000
Copying INPUT to /tmp/buf1000

real    0m4.916s
user    0m1.001s
sys     0m3.986s

我们可以发现,缓冲区越大,cp3.go 运行得就越快,这或多或少是符合预期的。此外,使用小于 20 字节的缓冲区来复制大文件会非常缓慢,应该避免。

你可以在 GitHub 找到 cp1.gocp2.gocp3.go 的 Go 代码。

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


via: https://opensource.com/article/18/6/copying-files-go

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

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

带你一窥生成二进制文件步骤的幕后,以便在出现一些错误时,你知道如何逐步解决问题。

C 语言广为人知,深受新老程序员的好评。使用 C 语言编写的源文件代码,使用了标准的英语术语,因而人们可以方便阅读。然而,计算机只能理解二进制代码。为将代码转换为机器语言,你需要使用一种被称为 编译器 compiler 的工具。

最常见的编译器是 GCC( GNU 编译器集 GNU Compiler Collection )。编译过程涉及到一系列的中间步骤及相关工具。

安装 GCC

为验证在你的系统上是否已经安装了 GCC,使用 gcc 命令:

$ gcc --version

如有必要,使用你的软件包管理器来安装 GCC。在基于 Fedora 的系统上,使用 dnf

$ sudo dnf install gcc libgcc

在基于 Debian 的系统上,使用 apt

$ sudo apt install build-essential

在安装后,如果你想查看 GCC 的安装位置,那么使用:

$ whereis gcc

演示使用 GCC 来编译一个简单的 C 程序

这里有一个简单的 C 程序,用于演示如何使用 GCC 来编译。打开你最喜欢的文本编辑器,并在其中粘贴这段代码:

// hellogcc.c
#include <stdio.h>

int main() {
    printf("Hello, GCC!\n");
    return 0;
}

保存文件为 hellogcc.c ,接下来编译它:

$ ls
hellogcc.c

$ gcc hellogcc.c

$ ls -1
a.out
hellogcc.c

如你所见,a.out 是编译后默认生成的二进制文件。为查看你所新编译的应用程序的输出,只需要运行它,就像你运行任意本地二进制文件一样:

$ ./a.out
Hello, GCC!

命名输出的文件

文件名称 a.out 是非常莫名其妙的,所以,如果你想具体指定可执行文件的名称,你可以使用 -o 选项:

(LCTT 译注:注意这和最近 Linux 内核废弃的 a.out 格式无关,只是名字相同,这里生成的 a.out 是 ELF 格式的 —— 也不知道谁给起了个 a.out 这破名字,在我看来,默认输出文件名就应该是去掉了 .c 扩展名后的名字。by wxy)

$ gcc -o hellogcc hellogcc.c

$ ls
a.out hellogcc hellogcc.c

$ ./hellogcc
Hello, GCC!

当开发一个需要编译多个 C 源文件文件的大型应用程序时,这种选项是很有用的。

在 GCC 编译中的中间步骤

编译实际上有四个步骤,即使在简单的用例中 GCC 自动执行了这些步骤。

  1. 预处理 Pre-Processing :GNU 的 C 预处理器(cpp)解析头文件(#include 语句),展开 macros 定义(#define 语句),并使用展开的源文件代码来生成一个中间文件,如 hellogcc.i
  2. 编译 Compilation :在这个期间中,编译器将预处理的源文件代码转换为指定 CPU 架构的汇编代码。由此生成是汇编文件使用一个 .s 扩展名来命名,如在这个示例中的 hellogcc.s
  3. 汇编 Assembly :汇编程序(as)将汇编代码转换为目标机器代码,放在目标文件中,例如 hellogcc.o
  4. 链接 Linking :链接器(ld)将目标代码和库代码链接起来生成一个可执行文件,例如 hellogcc

在运行 GCC 时,可以使用 -v 选项来查看每一步的细节:

$ gcc -v -o hellogcc hellogcc.c

Compiler flowchart

手动编译代码

体验编译的每个步骤可能是很有用的,因此在一些情况下,你不需要 GCC 完成所有的步骤。

首先,除源文件文件以外,删除在当前文件夹下生成的文件。

$ rm a.out hellogcc.o

$ ls
hellogcc.c

预处理器

首先,启动预处理器,将其输出重定向为 hellogcc.i

$ cpp hellogcc.c > hellogcc.i

$ ls
hellogcc.c hellogcc.i

查看输出文件,并注意一下预处理器是如何包含头文件和扩展宏中的源文件代码的。

编译器

现在,你可以编译代码为汇编代码。使用 -S 选项来设置 GCC 只生成汇编代码:

$ gcc -S hellogcc.i

$ ls
hellogcc.c hellogcc.i hellogcc.s

$ cat hellogcc.s

查看汇编代码,来看看生成了什么。

汇编

使用你刚刚所生成的汇编代码来创建一个目标文件:

$ as -o hellogcc.o hellogcc.s

$ ls
hellogcc.c hellogcc.i hellogcc.o hellogcc.s

链接

要生成一个可执行文件,你必须将对象文件链接到它所依赖的库。这并不像前面的步骤那么简单,但它却是有教育意义的:

$ ld -o hellogcc hellogcc.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
ld: hellogcc.o: in function `main`:
hellogcc.c:(.text+0xa): undefined reference to `puts'

在链接器查找完 libc.so 库后,出现一个引用 undefined puts 错误。你必须找出适合的链接器选项来链接必要的库以解决这个问题。这不是一个小技巧,它取决于你的系统的布局。

在链接时,你必须链接代码到 核心运行时 core runtime (CRT)目标,这是一组帮助二进制可执行文件启动的子例程。链接器也需要知道在哪里可以找到重要的系统库,包括 libclibgcc,尤其是其中的特殊的开始和结束指令。这些指令可以通过 --start-group--end-group 选项来分隔,或者使用指向 crtbegin.ocrtend.o 的路径。

这个示例使用了 RHEL 8 上的路径,因此你可能需要依据你的系统调整路径。

$ ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 \
    -o hello \
    /usr/lib64/crt1.o /usr/lib64/crti.o \
    --start-group \
        -L/usr/lib/gcc/x86_64-redhat-linux/8 \
        -L/usr/lib64 -L/lib64 hello.o \
        -lgcc \
        --as-needed -lgcc_s \
        --no-as-needed -lc -lgcc \
    --end-group \
    /usr/lib64/crtn.o

在 Slackware 上,同样的链接过程会使用一组不同的路径,但是,你可以看到这其中的相似之处:

$ ld -static -o hello \
    -L/usr/lib64/gcc/x86_64-slackware-linux/11.2.0/ \
    /usr/lib64/crt1.o /usr/lib64/crti.o hello.o /usr/lib64/crtn.o \
    --start-group \
        -lc -lgcc -lgcc_eh \
    --end-group

现在,运行由此生成的可执行文件:

$ ./hello
Hello, GCC!

一些有用的实用程序

下面是一些帮助检查文件类型、 符号表 symbol tables 和链接到可执行文件的库的实用程序。

使用 file 实用程序可以确定文件的类型:

$ file hellogcc.c
hellogcc.c: C source, ASCII text

$ file hellogcc.o
hellogcc.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

$ file hellogcc
hellogcc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bb76b241d7d00871806e9fa5e814fee276d5bd1a, for GNU/Linux 3.2.0, not stripped

对目标文件使用 nm 实用程序可以列出 符号表 symbol tables

$ nm hellogcc.o
0000000000000000 T main
             U puts

使用 ldd 实用程序来列出动态链接库:

$ ldd hellogcc
linux-vdso.so.1 (0x00007ffe3bdd7000)
libc.so.6 => /lib64/libc.so.6 (0x00007f223395e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2233b7e000)

总结

在这篇文章中,你了解到了 GCC 编译中的各种中间步骤,和检查文件类型、 符号表 symbol tables 和链接到可执行文件的库的实用程序。在你下次使用 GCC 时,你将会明白它为你生成一个二进制文件所要做的步骤,并且当出现一些错误时,你会知道如何逐步处理解决问题。


via: https://opensource.com/article/22/5/gnu-c-compiler

作者:Jayashree Huttanagoudar 选题:lkxed 译者:robsean 校对:wxy

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

掌握管理本地/远程分支等最常见的 Git 任务。

Git 的主要优势之一就是它能够将工作“分叉”到不同的分支中。

如果只有你一个人在使用某个存储库,分支的好处是有限的。但是,一旦你开始与许多其他贡献者一起工作,分支就变得必不可少。Git 的分支机制允许多人同时处理一个项目,甚至是同一个文件。用户可以引入不同的功能,彼此独立,然后稍后将更改合并回主分支。那些专门为一个目的创建的分支,有时也被称为 主题分支 topic branch ,例如添加新功能或修复已知错误。

当你开始使用分支,了解如何管理它们会很有帮助。以下是开发者在现实世界中使用 Git 分支执行的最常见任务。

重命名分支

有时候,你或许会错误地命名了一个分支,或者你会想要在内容合并到主分支后,使用同一个分支在不同的错误或任务之间切换。在这种情况下,重命名主题分支就会很有帮助。

重命名本地分支

1、重命名本地分支:

$ git branch -m <old_branch_name> <new_branch_name>

当然,这只会重命名你的分支副本。如果远程 Git 服务器上存在该分支,请继续执行后续步骤。

2、推送这个新分支,从而创建一个新的远程分支:

$ git push origin <new_branch_name>

3、删除旧的远程分支:

$ git push origin -d -f <old_branch_name>

重命名当前分支

当你要重命名的分支恰好是当前分支时,你不需要指定旧的分支名称。

1、重命名当前分支:

$ git branch -m <new_branch_name>

2、推送新分支,从而创建一个新的远程分支:

$ git push origin <new_branch_name>

3、删除旧的远程分支:

$ git push origin -d -f <old_branch_name>

使用 Git 删除本地和远程分支

为了保持存储库的整洁,通常建议你在确保已将内容合并到主分支后,删除临时分支。

删除本地分支

删除本地分支只会删除系统上存在的该分支的副本。如果分支已经被推送到远程存储库,它仍然可供使用该存储库的每个人使用。

1、签出存储库的主分支(例如 mainmaster):

$ git checkout <central_branch_name>

2、列出所有分支(本地和远程):

$ git branch -a

3、删除本地分支:

$ git branch -d <name_of_the_branch>

要删除所有本地主题分支并仅保留 main 分支:

$ git branch | grep -v main | xargs git branch -d

删除远程分支

删除远程分支只会删除远程服务器上存在的该分支的副本。如果你想撤销删除,也可以将其重新推送到远程(例如 GitHub),只要你还有本地副本即可。

1、签出存储库的主分支(通常是 mainmaster):

$ git checkout <central_branch_name>

2、列出所有分支(本地和远程):

$ git branch -a

3、删除远程分支:

$ git push origin -d <name_of_the_branch>

查看远程主题分支的作者

如果你是存储库管理员,你可能会有这个需求,以便通知未使用分支的作者它将被删除。

1、签出存储库的主分支(例如 mainmaster):

$ git checkout <central_branch_name>

2、删除不存在的远程分支的分支引用:

$ git remote prune origin

3、列出存储库中所有远程主题分支的作者,使用 --format 选项,并配合特殊的选择器来只打印你想要的信息(在本例中,%(authorname)%(refname) 分别代表作者名字和分支名称):

$ git for-each-ref --sort=authordate --format='%(authorname) %(refname)' refs/remotes

示例输出:

tux  refs/remotes/origin/dev
agil refs/remotes/origin/main

你可以添加更多格式,包括颜色编码和字符串操作,以便于阅读:

$ git for-each-ref --sort=authordate \
    --format='%(color:cyan)%(authordate:format:%m/%d/%Y %I:%M %p)%(align:25,left)%(color:yellow) %(authorname)%(end)%(color:reset)%(refname:strip=3)' \
    refs/remotes

示例输出:

01/16/2019 03:18 PM tux      dev
05/15/2022 10:35 PM agil     main

你可以使用 grep 获取特定远程主题分支的作者:

$ git for-each-ref --sort=authordate \
    --format='%(authorname) %(refname)' \
    refs/remotes | grep <topic_branch_name>

熟练运用分支

Git 分支的工作方式存在细微差别,具体取决于你想要分叉代码库的位置、存储库维护者如何管理分支、 压扁 squashing 变基 rebasing 等。若想进一步了解该主题,你可以阅读下面这三篇文章:


via: https://opensource.com/article/22/5/git-branch-rename-delete-find-author

作者:Agil Antony 选题:lkxed 译者:lkxed 校对:wxy

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

Spark SQL 是 Spark 生态系统中处理结构化格式数据的模块。它在内部使用 Spark Core API 进行处理,但对用户的使用进行了抽象。这篇文章深入浅出地告诉你 Spark SQL 3.x 的新内容。

有了 Spark SQL,用户可以编写 SQL 风格的查询。这对于精通结构化查询语言或 SQL 的广大用户群体来说,基本上是很有帮助的。用户也将能够在结构化数据上编写交互式和临时性的查询。Spark SQL 弥补了 弹性分布式数据集 resilient distributed data sets (RDD)和关系表之间的差距。RDD 是 Spark 的基本数据结构。它将数据作为分布式对象存储在适合并行处理的节点集群中。RDD 很适合底层处理,但在运行时很难调试,程序员不能自动推断 模式 schema 。另外,RDD 没有内置的优化功能。Spark SQL 提供了 数据帧 DataFrame 和数据集来解决这些问题。

Spark SQL 可以使用现有的 Hive 元存储、SerDes 和 UDF。它可以使用 JDBC/ODBC 连接到现有的 BI 工具。

数据源

大数据处理通常需要处理不同的文件类型和数据源(关系型和非关系型)的能力。Spark SQL 支持一个统一的数据帧接口来处理不同类型的源,如下所示。

  • 文件:

    • CSV
    • Text
    • JSON
    • XML
  • JDBC/ODBC:

    • MySQL
    • Oracle
    • Postgres
  • 带模式的文件:

    • AVRO
    • Parquet
  • Hive 表:

    • Spark SQL 也支持读写存储在 Apache Hive 中的数据。

通过数据帧,用户可以无缝地读取这些多样化的数据源,并对其进行转换/连接。

Spark SQL 3.x 的新内容

在以前的版本中(Spark 2.x),查询计划是基于启发式规则和成本估算的。从解析到逻辑和物理查询计划,最后到优化的过程是连续的。这些版本对转换和行动的运行时特性几乎没有可见性。因此,由于以下原因,查询计划是次优的:

  • 缺失和过时的统计数据
  • 次优的启发式方法
  • 错误的成本估计

Spark 3.x 通过使用运行时数据来迭代改进查询计划和优化,增强了这个过程。前一阶段的运行时统计数据被用来优化后续阶段的查询计划。这里有一个反馈回路,有助于重新规划和重新优化执行计划。

Figure 1: Query planning

自适应查询执行(AQE)

查询被改变为逻辑计划,最后变成物理计划。这里的概念是“重新优化”。它利用前一阶段的可用数据,为后续阶段重新优化。正因为如此,整个查询的执行要快得多。

AQE 可以通过设置 SQL 配置来启用,如下所示(Spark 3.0 中默认为 false):

spark.conf.set(“spark.sql.adaptive.enabled”,true)

动态合并“洗牌”分区

Spark 在“ 洗牌 shuffle ”操作后确定最佳的分区数量。在 AQE 中,Spark 使用默认的分区数,即 200 个。这可以通过配置来启用。

spark.conf.set(“spark.sql.adaptive.coalescePartitions.enabled”,true)

动态切换连接策略

广播哈希是最好的连接操作。如果其中一个数据集很小,Spark 可以动态地切换到广播连接,而不是在网络上“洗牌”大量的数据。

动态优化倾斜连接

如果数据分布不均匀,数据会出现倾斜,会有一些大的分区。这些分区占用了大量的时间。Spark 3.x 通过将大分区分割成多个小分区来进行优化。这可以通过设置来启用:

spark.conf.set(“spark.sql.adaptive.skewJoin.enabled”,true)

Figure 2: Performance improvement in Spark 3.x (Source: Databricks)

其他改进措施

此外,Spark SQL 3.x还支持以下内容。

动态分区修剪

3.x 将只读取基于其中一个表的值的相关分区。这消除了解析大表的需要。

连接提示

如果用户对数据有了解,这允许用户指定要使用的连接策略。这增强了查询的执行过程。

兼容 ANSI SQL

在兼容 Hive 的早期版本的 Spark 中,我们可以在查询中使用某些关键词,这样做是完全可行的。然而,这在 Spark SQL 3 中是不允许的,因为它有完整的 ANSI SQL 支持。例如,“将字符串转换为整数”会在运行时产生异常。它还支持保留关键字。

较新的 Hadoop、Java 和 Scala 版本

从 Spark 3.0 开始,支持 Java 11 和 Scala 2.12。 Java 11 具有更好的原生协调和垃圾校正,从而带来更好的性能。 Scala 2.12 利用了 Java 8 的新特性,优于 2.11。

Spark 3.x 提供了这些现成的有用功能,而无需开发人员操心。这将显着提高 Spark 的整体性能。


via: https://www.opensourceforu.com/2022/05/structured-data-processing-with-spark-sql/

作者:Phani Kiran 选题:lkxed 译者:geekpi 校对:wxy

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

仅用大约 65 行代码,开发一个用于生成随机数、支持并发的 TCP 服务端。

TCP 和 UDP 服务端随处可见,它们基于 TCP/IP 协议栈,通过网络为客户端提供服务。在这篇文章中,我将介绍如何使用 Go 语言 开发一个用于返回随机数、支持并发的 TCP 服务端。对于每一个来自 TCP 客户端的连接,它都会启动一个新的 goroutine(轻量级线程)来处理相应的请求。

你可以在 GitHub 上找到本项目的源码:concTcp.go

处理 TCP 连接

这个程序的主要逻辑在 handleConnection() 函数中,具体实现如下:

func handleConnection(c net.Conn) {
        fmt.Printf("Serving %s\n", c.RemoteAddr().String())
        for {
                netData, err := bufio.NewReader(c).ReadString('\n')
                if err != nil {
                        fmt.Println(err)
                        return
                }

                temp := strings.TrimSpace(string(netData))
                if temp == "STOP" {
                        break
                }

                result := strconv.Itoa(random()) + "\n"
                c.Write([]byte(string(result)))
        }
        c.Close()
}

如果 TCP 客户端发送了一个 “STOP” 字符串,为它提供服务的 goroutine 就会终止;否则,TCP 服务端就会返回一个随机数给它。只要客户端不主动终止,服务端就会一直提供服务,这是由 for 循环保证的。具体来说,for 循环中的代码使用了 bufio.NewReader(c).ReadString('\n') 来逐行读取客户端发来的数据,并使用 c.Write([]byte(string(result))) 来返回数据(生成的随机数)。你可以在 Go 的 net 标准包 文档 中了解更多。

支持并发

main() 函数的实现部分,每当 TCP 服务端收到 TCP 客户端的连接请求,它都会启动一个新的 goroutine 来为这个请求提供服务。

func main() {
        arguments := os.Args
        if len(arguments) == 1 {
                fmt.Println("Please provide a port number!")
                return
        }

        PORT := ":" + arguments[1]
        l, err := net.Listen("tcp4", PORT)
        if err != nil {
                fmt.Println(err)
                return
        }
        defer l.Close()
        rand.Seed(time.Now().Unix())

        for {
                c, err := l.Accept()
                if err != nil {
                        fmt.Println(err)
                        return
                }
                go handleConnection(c)
        }
}

首先,main() 确保程序至少有一个命令行参数。注意,现有代码并没有检查这个参数是否为有效的 TCP 端口号。不过,如果它是一个无效的 TCP 端口号,net.Listen() 就会调用失败,并返回一个错误信息,类似下面这样:

$ go run concTCP.go 12a
listen tcp4: lookup tcp4/12a: nodename nor servname provided, or not known
$ go run concTCP.go -10
listen tcp4: address -10: invalid port

net.Listen() 函数用于告诉 Go 接受网络连接,因而承担了服务端的角色。它的返回值类型是 net.Conn,后者实现了 io.Readerio.Writer 接口。此外,main() 函数中还调用了 rand.Seed() 函数,用于初始化随机数生成器。最后,for 循环允许程序一直使用 Accept() 函数来接受 TCP 客户端的连接请求,并以 goroutine 的方式来运行 handleConnection(c) 函数,处理客户端的后续请求。

net.Listen() 的第一个参数

net.Listen() 函数的第一个参数定义了使用的网络类型,而第二个参数定义了服务端监听的地址和端口号。第一个参数的有效值为 tcptcp4tcp6udpudp4udp6ipip4ip6Unix(Unix 套接字)、UnixgramUnixpacket,其中:tcp4udp4ip4 只接受 IPv4 地址,而 tcp6udp6ip6 只接受 IPv6 地址。

服务端并发测试

concTCP.go 需要一个命令行参数,来指定监听的端口号。当它开始服务 TCP 客户端时,你会得到类似下面的输出:

$ go run concTCP.go 8001
Serving 127.0.0.1:62554
Serving 127.0.0.1:62556

netstat 的输出可以确认 congTCP.go 正在为多个 TCP 客户端提供服务,并且仍在继续监听建立连接的请求:

$ netstat -anp TCP | grep 8001
tcp4       0      0  127.0.0.1.8001         127.0.0.1.62556        ESTABLISHED
tcp4       0      0  127.0.0.1.62556        127.0.0.1.8001         ESTABLISHED
tcp4       0      0  127.0.0.1.8001         127.0.0.1.62554        ESTABLISHED
tcp4       0      0  127.0.0.1.62554        127.0.0.1.8001         ESTABLISHED
tcp4       0      0  *.8001                 *.*                    LISTEN

在上面输出中,最后一行显示了有一个进程正在监听 8001 端口,这意味着你可以继续连接 TCP 的 8001 端口。第一行和第二行显示了有一个已建立的 TCP 网络连接,它占用了 8001 和 62556 端口。相似地,第三行和第四行显示了有另一个已建立的 TCP 连接,它占用了 8001 和 62554 端口。

下面这张图片显示了 concTCP.go 在服务多个 TCP 客户端时的输出:

concTCP.go TCP 服务端测试

类似地,下面这张图片显示了两个 TCP 客户端的输出(使用了 nc 工具):

是用 nc 工具作为 concTCP.go 的 TCP 客户端

你可以在 维基百科 上找到更多关于 nc(即 netcat)的信息。

总结

现在,你学会了如何用大约 65 行 Go 代码来开发一个生成随机数、支持并发的 TCP 服务端,这真是太棒了!如果你想要让你的 TCP 服务端执行别的任务,只需要修改 handleConnection() 函数即可。


via: https://opensource.com/article/18/5/building-concurrent-tcp-server-go

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

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