详解 Ubuntu snap 包的制作过程

Summary: 在这篇文章中，我们将看到如何为名为 timg 的实用程序制作对应的 snap 包。

>
> 如果你看过译者以前翻译的 snappy 文章，不知有没有感觉相关主题都是浅尝辄止，讲得不够透彻，看得也不太过瘾？如果有的话，相信这篇详细讲解如何从零开始制作一个 snap 包的文章应该不会让你失望。
>
>
>

![](/data/attachment/album/201710/04/005358bmfkk33gzwmfwtkf.jpg)

在这篇文章中，我们将看到如何为名为 [timg](https://github.com/hzeller/timg) 的实用程序制作对应的 snap 包。如果这是你第一次听说 snap 安装包，你可以先看看 [如何创建你的第一个 snap 包](https://tutorials.ubuntu.com/tutorial/create-your-first-snap)。

今天我们将学习以下有关使用 snapcraft 制作 snap 包的内容：

* [timg](https://github.com/hzeller/timg) 源码中的 Makefile 文件是手工编写，我们需要修改一些 [make 插件参数](https://snapcraft.io/docs/reference/plugins/make)。
* 这个程序是用 C++ 语言写的，依赖几个额外的库文件。我们需要把相关的代码添加到 snap 包中。
* [严格限制还是传统限制](https://snapcraft.io/docs/reference/confinement)？我们将会讨论如何在它们之间进行选择。

首先，我们了解下 [timg](https://github.com/hzeller/timg) 有什么用？

### 背景

Linux 终端模拟器已经变得非常炫酷，并且还能显示颜色！

![1.png-19.9kB](/data/attachment/album/201710/04/005427sz50aoivjjctwxuh.png)

除了标准的颜色，大多数终端模拟器（如上图显示的 GNOME 终端）都支持真彩色（1600 万种颜色）。

![图片.png-61.9kB](/data/attachment/album/201710/04/005428jpmp2g9xdgwdgf12.png)

是的！终端模拟器已经支持真彩色了！从这个页面“ [多个终端和终端应用程序已经支持真彩色（1600 万种颜色）](https://gist.github.com/XVilka/8346728)” 可以获取 AWK 代码自己进行测试。你可以看到在代码中使用了一些 [转义序列](https://en.wikipedia.org/wiki/Escape_sequence) 来指定 RGB 的值（256 \* 256 \* 256 ~= 1600 万种颜色）。

### timg 是什么？

好了，言归正传，[timg](https://github.com/hzeller/timg) 有什么用？它能将输入的图片重新调整为终端窗口字符所能显示范围的大小（比如：80 x 25），然后在任何分辨率的终端窗口用彩色字符显示图像。

![图片.png-37.3kB](/data/attachment/album/201710/04/005428q9ixpu5if25pnqxp.png)

这幅图用彩色块字符显示了 [Ubuntu 的 logo](http://design.ubuntu.com/wp-content/uploads/ubuntu-logo112.png)，原图是一个 PNG 格式的文件。

![图片.png-165kB](/data/attachment/album/201710/04/005428h6yta595ykwsokz7.png)

这是 [@Doug8888 拍摄的花](https://www.flickr.com/photos/doug88888/5776072628/in/photolist-9WCiNQ-7U3Trc-7YUZBL-5DwkEQ-6e1iT8-a372aS-5F75aL-a1gbow-6eNayj-8gWK2H-5CtH7P-6jVqZv-86RpwN-a2nEnB-aiRmsc-6aKvwK-8hmXrN-5CWDNP-62hWM8-a9smn1-ahQqHw-a22p3w-a36csK-ahN4Pv-7VEmnt-ahMSiT-9NpTa7-5A3Pon-ai7DL7-9TKCqV-ahr7gN-a1boqP-83ZzpH-9Sqjmq-5xujdi-7UmDVb-6J2zQR-5wAGNR-5eERar-5KVDym-5dL8SZ-5S2Uut-7RVyHg-9Z6MAt-aiRiT4-5tLesw-aGLSv6-5ftp6j-5wAVBq-5T2KAP)。

如果你通过远程连接服务器来管理自己的业务，并想要查看图像文件，那么 [timg](https://github.com/hzeller/timg) 将会特别有用。

除了静态图片，[timg](https://github.com/hzeller/timg) 同样也可以显示 gif 动图。

那么让我们开始 snap 之旅吧！

### 熟悉 timg 的源码

[timg](https://github.com/hzeller/timg) 的源码可以在 <https://github.com/hzeller/timg> 找到。让我们试着手动编译它，以了解它有什么需求。

![图片.png-128.4kB](/data/attachment/album/201710/04/005429o3521zzmp9mcc950.png)

`Makefile` 在 `src/` 子文件夹中而不是项目的根文件夹中。在 github 页面上，他们说需要安装两个开发包（GraphicsMagic++ 和 WebP），然后使用 `make` 就能生成可执行文件。在截图中可以看到我已经将它们安装好了（在我读完相关的 Readme.md 文件后）。

因此，在编写 `snapcraft.yaml` 文件时已经有了四条腹稿：

1. `Makefile` 在 `src/` 子文件夹中而不是项目的根文件夹中。
2. 这个程序编译时需要两个开发库。
3. 为了让 timg 以 snap 包形式运行，我们需要将这两个库捆绑在 snap 包中（或者静态链接它们）。
4. [timg](https://github.com/hzeller/timg) 是用 C++ 编写的，所以需要安装 g++。在编译之前，让我们通过 `snapcraft.yaml` 文件来检查 `build-essential` 元包是否已经安装。

### 从 snapcraft 开始

让我们新建一个名为 `timg-snap/` 的文件夹，并在其中运行 `snapcraft init` 这条命令来创建 `snapcraft.yaml` 工作的框架。

```
ubuntu@snaps:~$ mkdir timg-snap
ubuntu@snaps:~$ cd timg-snap/
ubuntu@snaps:~/timg-snap$ snapcraft init
Created snap/snapcraft.yaml.
Edit the file to your liking or run `snapcraft` to get started
ubuntu@snaps:~/timg-snap$ cat snap/snapcraft.yaml
name: my-snap-name # you probably want to 'snapcraft register <name>'
version: '0.1' # just for humans, typically '1.2+git' or '1.3.2'
summary: Single-line elevator pitch for your amazing snap # 79 char long summary
description: |
  This is my-snap's description. You have a paragraph or two to tell the most important story about your snap. Keep it under 100 words though, we live in tweetspace and your description wants to look good in the snap store.

grade: devel # must be 'stable' to release into candidate/stable channels
confinement: devmode # use 'strict' once you have the right plugs and slots

parts:
  my-part:
    # See 'snapcraft plugins'
    plugin: nil

```

### 填充元数据

`snapcraft.yaml` 配置文件的上半部分是元数据。我们需要一个一个把它们填满，这算是比较容易的部分。元数据由以下字段组成：

1. `name` （名字）—— snap 包的名字，它将公开在 Ubuntu 商店中。
2. `version` （版本）—— snap 包的版本号。可以是源代码存储库中一个适当的分支或者标记，如果没有分支或标记的话，也可以是当前日期。
3. `summary` （摘要）—— 不超过 80 个字符的简短描述。
4. `description` （描述）—— 长一点的描述, 100 个字以下。
5. `grade` （等级）—— `stable` （稳定）或者 `devel` （开发）。因为我们想要在 Ubuntu 商店的稳定通道中发布这个 snap 包，所以在 snap 包能正常工作后，就把它设置成 `stable`。
6. `confinement` （限制）—— 我们首先设置为 `devmode` （开发模式），这样系统将不会以任何方式限制 snap 包。一旦它在 `devmode`下能正常工作，我们再考虑选择 `strict` （严格）还是 `classic` （传统）限制。

我们将使用 `timg` 这个名字：

```
ubuntu@snaps:~/timg-snap$ snapcraft register timg
Registering timg.
You already own the name 'timg'.

```

是的，这个名字我已经注册了 :-)。

接下来，我们应该选择哪个版本的 timg？

![图片.png-72.7kB](/data/attachment/album/201710/04/005429bf4787aa48787899.png)

当在仓库中寻找分支或标记时，我们会发现有一个 v0.9.5 标签，其中有 2016 年 6 月 27 日最新提交的代码。

![图片.png-71.4kB](/data/attachment/album/201710/04/005429isi8z381pkdsf1gs.png)

然而主分支（`master`）中有两个看起来很重要的提交。因此我们使用主分支而不用 `v0.9.5` 标签的那个。我们使用今天的日期—— `20170226` 做为版本号。

我们从仓库中搜集了摘要和描述。其中摘要的内容为 `A terminal image viewer`，描述的内容为 `A viewer that uses 24-Bit color capabilities and unicode character blocks to display images in the terminal`。

最后，将 `grade` （等级）设置为 `stable` （稳定），将 `confinement` 限制设置为 `devmode` （开发模式）（一直到 snap 包真正起作用）。

这是更新后的 `snapcraft.yaml`，带有所有的元数据：

```
ubuntu@snaps:~/timg-snap$ cat snap/snapcraft.yaml
name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks to display images in the terminal.

grade: stable
confinement: devmode

parts:
  my-part:
    # See 'snapcraft plugins'
    plugin: nil

```

### 弄清楚 `parts:` 是什么

现在我们需要将上面已经存在的 `parts:` 部分替换成真实的 `parts:`。

![timg-git-url.png-8kB](/data/attachment/album/201710/04/005430rbpub2wbkpkqwksp.png)

*Git 仓库的 URL。*

![图片.png-28.7kB](/data/attachment/album/201710/04/005430j25sdioid56xz9kp.png)

*存在 Makefile，因此我们需要 make 插件。*

我们已经知道 git 仓库的 URL 链接，并且 timg 源码中已有了 `Makefile` 文件。至于 [snapcraft make 插件](https://snapcraft.io/docs/reference/plugins/make) 的 Makefile 命令，正如文档所言，这个插件总是会运行 `make` 后再运行 `make install`。为了确认 `make` 插件的用法，我查看了 [snapcraft 可用插件列表](https://snapcraft.io/docs/reference/plugins/)。

因此，我们将最初的配置：

```
parts:
  my-part:
    # See 'snapcraft plugins'
    plugin: nil

```

修改为：

```
parts:
  timg:
    source: https://github.com/hzeller/timg.git
    plugin: make

```

这是当前 `snapcraft.yaml` 文件的内容：

```
name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks
  to display images in the terminal.

grade: stable
confinement: devmode

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    plugin: make

```

让我们运行下 `snapcraft prime` 命令看看会发生什么：

```
ubuntu@snaps:~/timg-snap$ snapcraft prime
Preparing to pull timg
Pulling timg
Cloning into '/home/ubuntu/timg-snap/parts/timg/src'...
remote: Counting objects: 144, done.
remote: Total 144 (delta 0), reused 0 (delta 0), pack-reused 144
Receiving objects: 100% (144/144), 116.00 KiB | 0 bytes/s, done.
Resolving deltas: 100% (89/89), done.
Checking connectivity... done.
Preparing to build timg
Building timg
make -j4
make: *** No targets specified and no makefile found.  Stop.
Command '['/bin/sh', '/tmp/tmpem97fh9d', 'make', '-j4']' returned non-zero exit status 2
ubuntu@snaps:~/timg-snap$

```

我们可以看到 `snapcraft` 无法在源代码中找到 `Makefile` 文件，正如我们之前所暗示的，`Makefile` 位于 `src/` 子文件夹中。那么，我们可以让 `snapcraft` 使用 `src/` 文件夹中的 `Makefile` 文件吗？

每个 snapcraft 插件都有自己的选项，并且有一些通用选项是所有插件共享的。在本例中，我们希望研究那些[与源代码相关的 snapcraft 选项](https://snapcraft.io/docs/reference/plugins/source)。我们开始吧：

**source-subdir：path**

snapcraft 会<ruby> 检出 <rt>  checkout </rt></ruby> `source` 关键字所引用的仓库或者解压归档文件到 `parts/<part-name>/src/` 中，但是它只会将特定的子目录复制到 `parts/<part-name>/build/` 中。

我们已经有了适当的选项，下面更新下 `parts`：

```
parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make

```

然后再次运行 `snapcraft prime`：

```
ubuntu@snaps:~/timg-snap$ snapcraft prime
The 'pull' step of 'timg' is out of date:

The 'source-subdir' part property appears to have changed.

Please clean that part's 'pull' step in order to continue
ubuntu@snaps:~/timg-snap$ snapcraft clean
Cleaning up priming area
Cleaning up staging area
Cleaning up parts directory
ubuntu@snaps:~/timg-snap$ snapcraft prime
Skipping pull timg (already ran)
Preparing to build timg
Building timg
make -j4
g++ `GraphicsMagick++-config --cppflags --cxxflags` -Wall -O3 -fPIC -c -o timg.o timg.cc
g++ -Wall -O3 -fPIC   -c -o terminal-canvas.o terminal-canvas.cc
/bin/sh: 1: GraphicsMagick++-config: not found
timg.cc:33:22: fatal error: Magick++.h: No such file or directory
compilation terminated.
Makefile:10: recipe for target 'timg.o' failed
make: *** [timg.o] Error 1
make: *** Waiting for unfinished jobs....
Command '['/bin/sh', '/tmp/tmpeeyxj5kw', 'make', '-j4']' returned non-zero exit status 2
ubuntu@snaps:~/timg-snap$

```

从错误信息我们可以得知 snapcraft 找不到 GraphicsMagick++ 这个开发库文件。根据 [snapcraft 常见关键字](https://snapcraft.io/docs/reference/plugins/common) 可知，我们需要在 `snapcraft.yaml` 中指定这个库文件，这样 snapcraft 才能安装它。

**build-packages：[deb, deb, deb…]**

列出构建 part 前需要在主机中安装的 Ubuntu 包。这些包通常不会进入最终的 snap 包中，除非它们含有 snap 包中二进制文件直接依赖的库文件（在这种情况下，可以通过 `ldd` 发现它们），或者在 `stage-package` 中显式地指定了它们。

让我们寻找下这个开发包的名字：

```
ubuntu@snaps:~/timg-snap$ apt-cache search graphicsmagick++ | grep dev
graphicsmagick-libmagick-dev-compat/xenial 1.3.23-1build1 all
libgraphicsmagick++1-dev/xenial 1.3.23-1build1 amd64
  format-independent image processing - C++ development files
libgraphicsmagick1-dev/xenial 1.3.23-1build1 amd64
  format-independent image processing - C development files
ubuntu@snaps:~/timg-snap$

```

可以看到包名为 `libgraphicsmagick++1-dev`，下面是更新后的 `parts`：

```
parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages:
      - libgraphicsmagick++1-dev

```

再次运行 `snapcraft`：

```
ubuntu@snaps:~/timg-snap$ snapcraft
Installing build dependencies: libgraphicsmagick++1-dev
[...]
The following NEW packages will be installed:
  libgraphicsmagick++-q16-12 libgraphicsmagick++1-dev libgraphicsmagick-q16-3
  libgraphicsmagick1-dev libwebp5
[...]
Building timg
make -j4
g++ `GraphicsMagick++-config --cppflags --cxxflags` -Wall -O3 -fPIC -c -o timg.o timg.cc
g++ -Wall -O3 -fPIC   -c -o terminal-canvas.o terminal-canvas.cc
g++ -o timg timg.o terminal-canvas.o `GraphicsMagick++-config --ldflags --libs`
/usr/bin/ld: cannot find -lwebp
collect2: error: ld returned 1 exit status
Makefile:7: recipe for target 'timg' failed
make: *** [timg] Error 1
Command '['/bin/sh', '/tmp/tmptma45jzl', 'make', '-j4']' returned non-zero exit status 2
ubuntu@snaps:~/timg-snap$

```

虽然只指定了开发库 `libgraphicsmagick+1-dev`，但 Ubuntu 还安装了一些代码库，包括 `libgraphicsmagick ++-q16-12`，以及动态代码库 `libwebp`。

这里仍然有一个错误，这个是因为缺少开发版本的 `webp` 库（一个静态库）。我们可以通过下面的命令找到它：

```
ubuntu@snaps:~/timg-snap$ apt-cache search libwebp | grep dev
libwebp-dev - Lossy compression of digital photographic images.
ubuntu@snaps:~/timg-snap$

```

上面安装的 `libwebp5` 包只提供了一个动态库（.so）。通过 `libwebp-dev` 包，我们可以得到相应的静态库（.a）。好了，让我们更新下 `parts:` 部分：

```
parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages:
      - libgraphicsmagick++1-dev
      - libwebp-dev

```

下面是更新后的 `snapcraft.yaml` 文件的内容：

```
name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks
  to display images in the terminal.

grade: stable
confinement: devmode

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages:
      - libgraphicsmagick++1-dev
      - libwebp-dev

```

让我们运行下 `snapcraft prime`：

```
ubuntu@snaps:~/timg-snap$ snapcraft prime
Skipping pull timg (already ran)
Preparing to build timg
Building timg
make -j4
g++ `GraphicsMagick++-config --cppflags --cxxflags` -Wall -O3 -fPIC -c -o timg.o timg.cc
g++ -Wall -O3 -fPIC   -c -o terminal-canvas.o terminal-canvas.cc
g++ -o timg timg.o terminal-canvas.o `GraphicsMagick++-config --ldflags --libs`
make install DESTDIR=/home/ubuntu/timg-snap/parts/timg/install
install timg /usr/local/bin
install: cannot create regular file '/usr/local/bin/timg': Permission denied
Makefile:13: recipe for target 'install' failed
make: *** [install] Error 1
Command '['/bin/sh', '/tmp/tmptq_s1itc', 'make', 'install', 'DESTDIR=/home/ubuntu/timg-snap/parts/timg/install']' returned non-zero exit status 2
ubuntu@snaps:~/timg-snap$

```

我们遇到了一个新问题。由于 `Makefile` 文件是手工编写的，不符合 [snapcraft make 插件](https://snapcraft.io/docs/reference/plugins/make) 的参数设置，所以不能正确安装到 `prime/` 文件夹中。`Makefile` 会尝试安装到 `usr/local/bin` 中。

我们需要告诉 [snapcraft make 插件](https://snapcraft.io/docs/reference/plugins/make) 不要运行 `make install`，而是找到 `timg` 可执行文件然后把它放到 `prime/` 文件夹中。根据文档的描述：

```
- artifacts:
（列表）
将 make 生成的指定文件复制或者链接到 snap 包安装目录。如果使用，则 `make install` 这步操作将被忽略。

```

所以，我们需要将一些东西放到 `artifacts:` 中。但是具体是哪些东西？

```
ubuntu@snaps:~/timg-snap/parts/timg$ ls build/src/
Makefile            terminal-canvas.h  timg*     timg.o
terminal-canvas.cc  terminal-canvas.o  timg.cc
ubuntu@snaps:~/timg-snap/parts/timg$

```

在 `build/` 子目录中，我们可以找到 `make` 的输出结果。由于我们设置了 `source-subdir:` 为 `src`，所以 `artifacts:` 的基目录为 `build/src`。在这里我们可以找到可执行文件 `timg`，我们需要将它设置为 `artifacts:` 的一个参数。通过 `artifacts:`，我们可以把 `make` 输出的某些文件复制到 snap 包的安装目录（在 `prime/` 中）。

下面是更新后 `snapcraft.yaml` 文件 `parts:` 部分的内容：

```
parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages:
      - libgraphicsmagick++1-dev
      - libwebp-dev
    artifacts: [timg]

```

让我们运行 `snapcraft prime`：

```
ubuntu@snaps:~/timg-snap$ snapcraft prime
Preparing to pull timg
Pulling timg
Cloning into '/home/ubuntu/timg-snap/parts/timg/src'...
remote: Counting objects: 144, done.
remote: Total 144 (delta 0), reused 0 (delta 0), pack-reused 144
Receiving objects: 100% (144/144), 116.00 KiB | 207.00 KiB/s, done.
Resolving deltas: 100% (89/89), done.
Checking connectivity... done.
Preparing to build timg
Building timg
make -j4
g++ `GraphicsMagick++-config --cppflags --cxxflags` -Wall -O3 -fPIC -c -o timg.o timg.cc
g++ -Wall -O3 -fPIC   -c -o terminal-canvas.o terminal-canvas.cc
g++ -o timg timg.o terminal-canvas.o `GraphicsMagick++-config --ldflags --libs`
Staging timg
Priming timg
ubuntu@snaps:~/timg-snap$

```

我们还将继续迭代。

### 导出命令

到目前为止，snapcraft 生成了可执行文件，但没有导出给用户使用的命令。接下来我们需要通过 `apps:` 导出一个命令。

首先我们需要知道命令在 `prime/` 的哪个子文件夹中：

```
ubuntu@snaps:~/timg-snap$ ls prime/
meta/  snap/  timg*  usr/
ubuntu@snaps:~/timg-snap$

```

它在 `prime/` 子文件夹的根目录中。现在，我们已经准备好要在 `snapcaft.yaml` 中增加 `apps:` 的内容：

```
ubuntu@snaps:~/timg-snap$ cat snap/snapcraft.yaml
name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks
  to display images in the terminal.

grade: stable
confinement: devmode

apps:
  timg:
    command: timg

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages:
      - libgraphicsmagick++1-dev
      - libwebp-dev
    artifacts: [timg]

```

让我们再次运行 `snapcraft prime`，然后测试下生成的 snap 包：

```
ubuntu@snaps:~/timg-snap$ snapcraft prime
Skipping pull timg (already ran)
Skipping build timg (already ran)
Skipping stage timg (already ran)
Skipping prime timg (already ran)
ubuntu@snaps:~/timg-snap$ snap try --devmode prime/
timg 20170226 mounted from /home/ubuntu/timg-snap/prime
ubuntu@snaps:~/timg-snap$

```

![图片.png-42.3kB](/data/attachment/album/201710/04/005430z79wgjktgadlv7lk.png)

*图片来源： <https://www.flickr.com/photos/mustangjoe/6091603784/>*

我们可以通过 `snap try --devmode prime/` 启用该 snap 包然后测试 `timg` 命令。这是一种高效的测试方法，可以避免生成 .snap 文件，并且无需安装和卸载它们，因为 `snap try prime/` 直接使用了 `prime/` 文件夹中的内容。

### 限制 snap

到目前为止，snap 包一直是在不受限制的开发模式下运行的。让我们看看如何限制它的运行：

```
ubuntu@snaps:~/timg-snap$ snap list
Name           Version   Rev   Developer  Notes
core           16-2      1337  canonical  -
timg           20170226  x1               devmode,try
ubuntu@snaps:~/timg-snap$ snap try --jailmode prime
timg 20170226 mounted from /home/ubuntu/timg-snap/prime
ubuntu@snaps:~/timg-snap$ snap list
Name           Version   Rev   Developer  Notes
core           16-2      1337  canonical  -
timg           20170226  x2               jailmode,try
ubuntu@snaps:~/timg-snap$ timg pexels-photo-149813.jpeg
Trouble loading pexels-photo-149813.jpeg (Magick: Unable to open file (pexels-photo-149813.jpeg) reported by magick/blob.c:2828 (OpenBlob))
ubuntu@snaps:~/timg-snap$

```

通过这种方式，我们可以无需修改 `snapcraft.yaml` 文件就从开发模式（`devmode`）切换到限制模式（`jailmode`）（`confinement: strict`）。正如预期的那样，`timg` 无法读取图像，因为我们没有开放访问文件系统的权限。

现在，我们需要作出决定。使用限制模式，我们可以很容易授予某个命令访问用户 `$HOME` 目录中文件的权限，但是只能访问那里。如果图像文件位于其它地方，我们总是需要复制到 `$HOME` 目录并在 `$HOME` 的副本上运行 timg。如果我们觉得可行，那我们可以设置 `snapcraf.yaml` 为:

```
name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks
  to display images in the terminal.

grade: stable
confinement: strict

apps:
  timg:
    command: timg
    plugs: [home]

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages:
      - libgraphicsmagick++1-dev
      - libwebp-dev
    artifacts: [timg]

```

另一方面，如果希望 timg snap 包能访问整个文件系统，我们可以设置传统限制来实现。对应的 `snapcraft.yaml` 内容如下：

```
name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks
  to display images in the terminal.

grade: stable
confinement: classic

apps:
  timg:
    command: timg

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages:
      - libgraphicsmagick++1-dev
      - libwebp-dev
    artifacts: [timg]

```

接下来我们将选择严格（`strict`）约束选项。因此，图像应该只能放在 $HOME 中。

### 打包和测试

让我们打包这个 snap，也就是制作 .snap 文件，然后在新安装的 Ubuntu 系统上对它进行测试。

```
ubuntu@snaps:~/timg-snap$ snapcraft
Skipping pull timg (already ran)
Skipping build timg (already ran)
Skipping stage timg (already ran)
Skipping prime timg (already ran)
Snapping 'timg' \
Snapped timg_20170226_amd64.snap
ubuntu@snaps:~/timg-snap$

```

我们如何在几秒钟内得到一个全新安装的 Ubuntu 系统来对 snap 包进行测试？

请查看 [尝试在 Ubuntu 上使用 LXD 容器](https://blog.simos.info/trying-out-lxd-containers-on-our-ubuntu/)，并在你的系统上设置 LXD。然后回到这里，尝试运行下面的命令：

```
$ lxc launch ubuntu:x snaptesting
Creating snaptesting
Starting snaptesting
$ lxc file push timg_20170226_amd64.snap snaptesting/home/ubuntu/
$ lxc exec snaptesting -- sudo su - ubuntu
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

ubuntu@snaptesting:~$ ls
timg_20170226_amd64.snap
ubuntu@snaptesting:~$ snap install timg_20170226_amd64.snap
error: access denied (try with sudo)
ubuntu@snaptesting:~$ sudo snap install timg_20170226_amd64.snap
error: cannot find signatures with metadata for snap "timg_20170226_amd64.snap"
ubuntu@snaptesting:~$ sudo snap install timg_20170226_amd64.snap --dangerous
error: cannot perform the following tasks:
- Mount snap "core" (1337) ([start snap-core-1337.mount] failed with exit status 1: Job for snap-core-1337.mount failed. See "systemctl status snap-core-1337.mount" and "journalctl -xe" for details.
)
ubuntu@snaptesting:~$ sudo apt install squashfuse
[...]
Setting up squashfuse (0.1.100-0ubuntu1~ubuntu16.04.1) ...
ubuntu@snaptesting:~$ sudo snap install timg_20170226_amd64.snap --dangerous
timg 20170226 installed
ubuntu@snaptesting:~$ wget https://farm7.staticflickr.com/6187/6091603784_d6960c8be2_z_d.jpg
[...]
2017-02-26 22:12:18 (636 KB/s) - ‘6091603784_d6960c8be2_z_d.jpg’ saved [240886/240886]
ubuntu@snaptesting:~$ timg 6091603784_d6960c8be2_z_d.jpg
[it worked!]
ubuntu@snaptesting:~$

```

我们启动了一个名为 `snaptesting` 的 LXD 容器，并将 .snap 文件复制进去。然后，通过普通用户连接到容器，并尝试安装 snap 包。最初，我们安装失败了，因为在无特权的 LXD 容器中安装 snap 包需要使用 `sudo` 。接着又失败了，因为 .snap 没有经过签名（我们需要使用 `--dangerous` 参数）。然而还是失败了，这次是因为我们需要安装 `squashfuse` 包（Ubuntu 16.04 镜像中没有预装）。最后，我们成功安装了snap，并设法查看了图像。

在一个全新安装的 Linux 系统中测试 snap 包是很重要的，因为这样才能确保 snap 包中包含所有必须的代码库。在这个例子中，我们使用了静态库并运行良好。

### 发布到 Ubuntu 商店

这是 [发布 snap 包到 Ubuntu 商店的说明](https://snapcraft.io/docs/build-snaps/publish)。 在之前的教程中，我们已经发布了一些 snap 包。对于 `timg` 来说，我们设置了严格限制和稳定等级。因此，我们会将它发布到稳定通道。

```
$ snapcraft push timg_20170226_amd64.snap
Pushing 'timg_20170226_amd64.snap' to the store.
Uploading timg_20170226_amd64.snap [                                       ]   0%
Uploading timg_20170226_amd64.snap [=======================================] 100%
Ready to release!|
Revision 6 of 'timg' created.
$ snapcraft release timg 6 stable
Track    Arch    Series    Channel    Version    Revision
latest   amd64   16        stable     20170226   6
                           candidate  ^          ^
                           beta       0.9.5      5
                           edge       0.9.5      5
The 'stable' channel is now open.

```

我们把 .snap 包推送到 Ubuntu 商店后，得到了一个 `Revision 6`。然后，我们将 timg `Revision 6` 发布到了 Ubuntu 商店的稳定通道。

在候选通道中没有已发布的 snap 包，它继承的是稳定通道的包，所以显示 `^` 字符。

在之前的测试中，我将一些较老版本的 snap 包上传到了测试和边缘通道。这些旧版本使用了 timg 标签为 `0.9.5` 的源代码。

我们可以通过将稳定版本发布到测试和边缘通道来移除旧的 0.9.5 版本的包。

```
$ snapcraft release timg 6 beta
Track    Arch    Series    Channel    Version    Revision
latest   amd64   16        stable     20170226   6
                           candidate  ^          ^
                           beta       20170226   6
                           edge       0.9.5      5
$ snapcraft release timg 6 edge
Track    Arch    Series    Channel    Version    Revision
latest   amd64   16        stable     20170226   6
                           candidate  ^          ^
                           beta       20170226   6
                           edge       20170226   6

```

### 使用 timg

让我们不带参数运行 `timg`：

```
ubuntu@snaptesting:~$ timg
Expected image filename.
usage: /snap/timg/x1/timg [options] <image> [<image>...]
Options:
    -g<w>x<h>  : Output pixel geometry. Default from terminal 80x48
    -s[<ms>]   : Scroll horizontally (optionally: delay ms (60)).
    -d<dx:dy>  : delta x and delta y when scrolling (default: 1:0).
    -w<seconds>: If multiple images given: Wait time between (default: 0.0).
    -t<seconds>: Only animation or scrolling: stop after this time.
    -c<num>    : Only Animation or scrolling: number of runs through a full cycle.
    -C         : Clear screen before showing image.
    -F         : Print filename before showing picture.
    -v         : Print version and exit.
If both -c and -t are given, whatever comes first stops.
If both -w and -t are given for some animation/scroll, -t takes precedence
ubuntu@snaptesting:~$

```

这里提到当前我们终端模拟器的缩放级别，即分辨率为：80 × 48。

让我们缩小一点，并最大化 GNOME 终端窗口。

```
-g<w>x<h>  : Output pixel geometry. Default from terminal 635x428

```

这是一个更好的解决方案，但我几乎看不到字符，因为他们太小了。让我们调用前面的命令再次显示这辆车。

![图片.png-904.9kB](/data/attachment/album/201710/04/005431hpzmiudusl3iffse.png)

你所看到的是调整后的图像（1080p）。虽然它是用彩色文本字符显示的，但看起来依旧很棒。

接下来呢？`timg` 其实也可以播放 gif 动画哦！

```
$ wget https://m.popkey.co/9b7141/QbAV_f-maxage-0.gif -O JonahHillAmazed.gif$ timg JonahHillAmazed.gif

```

你可以试着安装 `timg` 来体验 gif 动画。要是不想自己动手，可以在 [asciinema](https://asciinema.org/a/dezbe2gpye84e0pjndp8t0pvh) 上查看相关记录 （如果视频看上去起伏不定的，请重新运行它）。

谢谢阅读！

---

译者简介：

经常混迹于 snapcraft.io，对 Ubuntu Core、Snaps 和 Snapcraft 有着浓厚的兴趣，并致力于将这些还在快速发展的新技术通过翻译或原创的方式介绍到中文世界。有兴趣的小伙伴也可以关注译者个人公众号： `Snapcraft`

---

via：<https://blog.simos.info/how-to-create-a-snap-for-timg-with-snapcraft-on-ubuntu/>

作者：[Mi blog lah!](https://blog.simos.info/) 译者：[Snapcrafter](https://github.com/Snapcrafter) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


开发一个 Linux 调试器（八）：堆栈展开

Summary: 有一个 backtrace 命令，它给你提供了程序当前的函数调用链。这篇文章将向你展示如何在 x86_64 上实现堆栈展开以生成这样的回溯。

![](/data/attachment/album/201710/04/165839gmb9iqdiee2bh3m5.jpg)

有时你需要知道的最重要的信息是什么，你当前的程序状态是如何到达那里的。有一个 `backtrace` 命令，它给你提供了程序当前的函数调用链。这篇文章将向你展示如何在 x86\_64 上实现堆栈展开以生成这样的回溯。

### 系列索引

这些链接将会随着其他帖子的发布而上线。

1. [准备环境](/article-8626-1.html)
2. [断点](/article-8645-1.html)
3. [寄存器和内存](/article-8663-1.html)
4. [ELF 和 DWARF](/article-8719-1.html)
5. [源码和信号](/article-8812-1.html)
6. [源码级逐步执行](/article-8813-1.html)
7. [源码级断点](/article-8890-1.html)
8. [堆栈展开](https://blog.tartanllama.xyz/c++/2017/06/24/writing-a-linux-debugger-unwinding/)
9. 读取变量
10. 之后步骤

用下面的程序作为例子：

```
void a() {
    //stopped here
}

void b() {
     a();
}

void c() {
     a();
}

int main() {
    b();
    c();
}

```

如果调试器停在 `//stopped here' 这行，那么有两种方法可以达到：`main->b->a`或`main->c->a`。如果我们用 LLDB 设置一个断点，继续执行并请求一个回溯，那么我们将得到以下内容：

```
* frame #0: 0x00000000004004da a.out`a() + 4 at bt.cpp:3
  frame #1: 0x00000000004004e6 a.out`b() + 9 at bt.cpp:6
  frame #2: 0x00000000004004fe a.out`main + 9 at bt.cpp:14
  frame #3: 0x00007ffff7a2e830 libc.so.6`__libc_start_main + 240 at libc-start.c:291
  frame #4: 0x0000000000400409 a.out`_start + 41

```

这说明我们目前在函数 `a` 中，`a` 从函数 `b` 中跳转，`b` 从 `main` 中跳转等等。最后两个帧是编译器如何引导 `main` 函数的。

现在的问题是我们如何在 x86\_64 上实现。最稳健的方法是解析 ELF 文件的 `.eh_frame` 部分，并解决如何从那里展开堆栈，但这会很痛苦。你可以使用 `libunwind` 或类似的来做，但这很无聊。相反，我们假设编译器以某种方式设置了堆栈，我们将手动遍历它。为了做到这一点，我们首先需要了解堆栈的布局。

```
            High
        |   ...   |
        +---------+
     +24|  Arg 1  |
        +---------+
     +16|  Arg 2  |
        +---------+
     + 8| Return  |
        +---------+
EBP+--> |Saved EBP|
        +---------+
     - 8|  Var 1  |
        +---------+
ESP+--> |  Var 2  |
        +---------+
        |   ...   |
            Low

```

如你所见，最后一个堆栈帧的帧指针存储在当前堆栈帧的开始处，创建一个链接的指针列表。堆栈依据这个链表解开。我们可以通过查找 DWARF 信息中的返回地址来找出列表中下一帧的函数。一些编译器将忽略跟踪 `EBP` 的帧基址，因为这可以表示为 `ESP` 的偏移量，并可以释放一个额外的寄存器。即使启用了优化，传递 `-fno-omit-frame-pointer` 到 GCC 或 Clang 会强制它遵循我们依赖的约定。

我们将在 `print_backtrace` 函数中完成所有的工作：

```
void debugger::print_backtrace() {

```

首先要决定的是使用什么格式打印出帧信息。我用了一个 lambda 来推出这个方法：

```
    auto output_frame = [frame_number = 0] (auto&& func) mutable {
        std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func)
                  << ' ' << dwarf::at_name(func) << std::endl;
    };

```

打印输出的第一帧是当前正在执行的帧。我们可以通过查找 DWARF 中的当前程序计数器来获取此帧的信息：

```
    auto current_func = get_function_from_pc(get_pc());
    output_frame(current_func);

```

接下来我们需要获取当前函数的帧指针和返回地址。帧指针存储在 `rbp` 寄存器中，返回地址是从帧指针堆栈起的 8 字节。

```
    auto frame_pointer = get_register_value(m_pid, reg::rbp);
    auto return_address = read_memory(frame_pointer+8);

```

现在我们拥有了展开堆栈所需的所有信息。我只需要继续展开，直到调试器命中 `main`，但是当帧指针为 `0x0` 时，你也可以选择停止，这些是你在调用 `main` 函数之前调用的函数。我们将从每帧抓取帧指针和返回地址，并打印出信息。

```
    while (dwarf::at_name(current_func) != "main") {
        current_func = get_function_from_pc(return_address);
        output_frame(current_func);
        frame_pointer = read_memory(frame_pointer);
        return_address = read_memory(frame_pointer+8);
    }
}

```

就是这样！以下是整个函数：

```
void debugger::print_backtrace() {
    auto output_frame = [frame_number = 0] (auto&& func) mutable {
        std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func)
                  << ' ' << dwarf::at_name(func) << std::endl;
    };

    auto current_func = get_function_from_pc(get_pc());
    output_frame(current_func);

    auto frame_pointer = get_register_value(m_pid, reg::rbp);
    auto return_address = read_memory(frame_pointer+8);

    while (dwarf::at_name(current_func) != "main") {
        current_func = get_function_from_pc(return_address);
        output_frame(current_func);
        frame_pointer = read_memory(frame_pointer);
        return_address = read_memory(frame_pointer+8);
    }
}

```

### 添加命令

当然，我们必须向用户公开这个命令。

```
    else if(is_prefix(command, "backtrace")) {
        print_backtrace();
    }

```

### 测试

测试此功能的一个方法是通过编写一个测试程序与一堆互相调用的小函数。设置几个断点，跳到代码附近，并确保你的回溯是准确的。

我们已经从一个只能产生并附加到其他程序的程序走了很长的路。本系列的倒数第二篇文章将通过支持读写变量来完成调试器的实现。在此之前，你可以在[这里](https://github.com/TartanLlama/minidbg/tree/tut_unwind)找到这个帖子的代码。

---

via: <https://blog.tartanllama.xyz/c++/2017/06/24/writing-a-linux-debugger-unwinding/>

作者：[Simon Brand](https://twitter.com/TartanLlama) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


DevOps 的意义

Summary: 真正的组织文化变革有助于弥合你原以为无法跨过的鸿沟

真正的组织文化变革有助于弥合你原以为无法跨过的鸿沟

>
> 真正的组织文化变革有助于弥合你原以为无法跨过的鸿沟
>
>
>

![What's the point of DevOps?](/data/attachment/album/201710/05/165152tpv8dpzjlb8121jl.png "What's the point of DevOps?")

回想一下你最近一次尝试改掉一个个人习惯的事情，你可能遇到过这样的情形，你需要改变你思考的方式并且改掉之前的习惯。这很艰难，你只能试着改变*你自己的*思维方式。

所以你可能会试着让自己置身于新的环境。新的环境实际上可帮助我们养成*新的*习惯，它反过来又会促成新的思维方式。

那就是能否成功改变的所在：思考的越多，得到的越多。你需要知道你在改变的原因以及你的目的所在（而不仅仅你要怎么做），因为改变本身往往是短暂和短视的。

现在想想你的 IT 组织需要做出的改变。也许你正在考虑采用像 DevOps 这样的东西。这个我们称之为 “DevOps” 的东西有三个组件：人、流程和工具。人和流程是*任何*团体组织的基础。因此，采用 DevOps 需要对大多数组织的核心进行根本性的改变，而不仅仅是学习新的工具。

如同其它的改变一样，它也是短视的。如果您将注意力集中在将改变作为单点解决方案 —— 例如，“获得更好的报警工具” —— 你可能只是管中窥豹。这种思维方式或许可以提供一套拥有更多铃声、口哨以及可以更好地处理呼叫轮询的工具，但是它不能解决这样的实际问题：警报不能送达到正确的团队，或者故障得不到解决，因为实际上没有人知道如何修复服务。

新的工具（或者至少一个新工具的想法）创造了一个讨论潜在问题的机会，可以让你的团队讨论对监控的看法。新工具让你能够做出更大的改变 —— 信仰和做法的改变 —— 它们作为你组织的基础而显得更加重要。

创造更深层次的变革需要一种可以全新地改变观念的方法。要找到这种方法，我们首先需要更好的理解变革的驱动力。

### 清除栅栏

>
> 就改革而言，它不同于推翻。这是一条直白且简单的原则，这个原则或许被视作悖论。在这种情况下，存在某种制度或法律；这么说吧，为了简单起见，在一条路上架设了一个栅栏或门。当今的改革者们来到这儿，并说：“我看不到它的用处，让我们把它清除掉。”更聪明的改革者会很好地回答：“如果你看不到它的用处，我肯定不会让你清除它，回去想想，然后你可以回来告诉我你知道了它的用处，我也许会允许你摧毁它。” — G.K Chesterton, 1929
>
>
>

为了了解对 DevOps 的需求 —— 它试图将传统意义上分开的开发部门和运维部门进行重新组合 —— 我们首先必须明白这个分开是如何产生的。一旦我们"知道了它的用处"，然后我们就会知道将它们分开是为了什么，并且在必要的时候可以取消分开。

今天我们没有一个单一的管理理论，但是大多数现代管理理论的起源可以追溯到<ruby> 弗雷德里克·温斯洛·泰勒 <rt>  Frederick Winslow Taylor </rt></ruby>。泰勒是一名机械工程师，他创建了一个衡量钢厂工人效率的系统。泰勒认为，他可以对工厂的劳动者运用科学分析的方法，不仅可以改进个人任务，也证明发现了有一个可以用来执行*任何*任务最佳方法。

我们可以很容易地画一个以泰勒为起源的历史树。从泰勒早在 18 世纪 80 年代后期的研究出现的时间运动研究和其他质量改进计划，跨越 20 世纪 20 年代一直到今天，我们可以从中看到六西格玛、精益，等等类似方法。自上而下、指导式管理，再加上研究过程的系统方法，主宰了今天主流商业文化。它主要侧重于把效率作为工人成功的测量标准。

如果泰勒是我们这颗历史树的根，那么我们主干上的下一个主叉将是 20 世纪 20 年代通用汽车公司的<ruby> 阿尔弗雷德·斯隆 <rt>  Alfred P. Sloan </rt></ruby>。通用汽车公司创造的斯隆结构不仅持续强劲到 21 世纪初，而且在未来五十年的大部分时间里，都将成为该公司的主要模式。

1920 年，通用公司正经历一场管理危机，或者说是缺乏管理的危机。斯隆向董事会写了一份为通用汽车的多个部门提出了一个新的结构《组织研究》。这一新结构的核心概念是“集中管理下放业务”。与雪佛兰，凯迪拉克和别克等品牌相关的各个部门将独立运作，同时为中央管理层提供推动战略和控制财务的手段。

在斯隆的建议下（以及后来就任 CEO 的指导下），通用汽车在美国汽车工业中占据了主导地位。斯隆的计划把一个处于灾难边缘公司创造成了一个非常成功的公司。从中间来看，自治单位是黑盒子，激励和目标被设置在顶层，而团队在底层推动。

泰勒思想的“最佳实践” —— 标准、可互换和可重复的行为 —— 仍然在今天的管理理念中占有一席之地，与斯隆公司结构的层次模式相结合，主导了僵化部门的分裂和孤岛化以实现最大的控制。

我们可以指出几份管理研究来证明这一点，但商业文化不是通过阅读书籍而创造和传播的。组织文化是 *真实的* 人在 *实际的* 情形下执行推动文化规范的 *具体的* 行为的产物。这就是为何类似泰勒和斯隆这样的理论变得固化而不可动摇的原因。

技术部门投资就是一个例子。以下是这个周期是如何循环的：投资者只投资于他们认为可以实现 *他们的* 特定成功观点的公司。这个成功的模式并不一定源于公司本身（和它的特定的目标）；它来自董事会对一家成功的公司 *应该* 如何看待的想法。许多投资者来自从经营企业的尝试和苦难中幸存下来的公司，因此他们对什么会使一个公司成功有 *不同的* 理念。他们为那些能够被教导模仿他们的成功模式的公司提供资金，希望获得资金的公司学会模仿。这样，初创公司孵化器就是一种重现理想的结构和文化的*直接的*方式。

“开发”和“运维”的分开不是因为人的原因，也不是因为不同的技能，或者放在新员工头上的一顶魔法分院帽；它是泰勒和斯隆的理论的副产品。责任与人员之间的清晰而不可渗透的界线是一个管理功能，同时也注重员工的工作效率。管理上的分开可以很容易的落在产品或者项目界线上，而不是技能上，但是通过今天的业务管理理论的历史告诉我们，基于技能的分组是“最好”的高效方式。

不幸的是，那些界线造成了紧张局势，这些紧张局势是由不同的管理链出于不同的目标设定的相反目标的直接结果。例如：

* 敏捷 ⟷ 稳定
* 吸引新用户 ⟷ 现有用户的体验
* 让应用程序增加新功能 ⟷ 让应用程序保持可用
* 打败竞争对手 ⟷ 维持收入
* 修复出现的问题 ⟷ 在问题出现之前就进行预防

今天，我们可以看到组织的高层领导人越来越认识到，现有的商业文化（并扩大了它所产生的紧张局势）是一个严重的问题。在 2016 年的 Gartner 报告中，57％ 的受访者表示，文化变革是 2020 年之前企业面临的主要挑战之一。像作为一种影响组织变革的手段的敏捷和 DevOps 这样的新方法的兴起反映了这一认识。“[影子 IT](https://thenewstack.io/parity-check-dont-afraid-shadow-yet/)” 的出现更是事物的另一个方面；最近的估计有将近 30％ 的 IT 支出在 IT 组织的控制之外。

这些只是企业正在面临的一些“文化担忧”。改变的必要性是明确的，但前进的道路仍然受到昨天的决定的约束。

### 抵抗并不是没用的

>
> Bert Lance 认为如果他能让政府采纳一条简单的格言“如果东西还没损坏，那就别去修理它”，他就可以为国家节省三十亿。他解释说：“这是政府的问题：‘修复没有损坏的东西，而不是修复已经损坏了的东西。’” — Nation's Business, 1977.5
>
>
>

通常，改革是组织针对所出现的错误所做的应对。在这个意义上说，如果紧张局势（即使逆境）是变革的正常催化剂，那么对变化的 *抵抗* 就是成功的指标。但是过分强调成功的道路会使组织变得僵硬、衰竭和独断。重视有效结果的政策导向是这种不断增长的僵局的症状。

传统 IT 部门的成功加剧了 IT 孤岛的壁垒。其他部门现在变成了“顾客”，而不是伙伴。试图将 IT 从成本中心转移出来创建一个新的操作模式，它可以将 IT 与其他业务目标断开。这反过来又会对敏捷性造成限制，增加摩擦，降低反应能力。合作被搁置转而偏向“专家方向”。结果是一个孤立主义的观点，IT 只能带来更多的伤害而不是好处。

正如“软件吃掉世界”，IT 越来越成为组织整体成功的核心。具有前瞻性的 IT 组织认识到这一点，并且已经对其战略规划进行了有意义的改变，而不是将改变视为恐惧。

例如，Facebook 与人类学家<ruby> 罗宾·邓巴 <rt>  Robin Dunbar </rt></ruby>就社会团体的方法进行了磋商，而且意识到这一点对公司成长的内部团体（不仅仅是该网站的外部用户）的影响。<ruby> 扎波斯 <rt>  Zappos </rt></ruby>的文化得到了很多的赞誉，该组织创立了一个部门，专注于培养他人对于核心价值观和企业文化的看法。当然，这本书是 《开放式组织》的姊妹篇，那是一本描述被应用于管理的开放原则 —— 透明度、参与度和社区 —— 可以如何为我们快节奏的互联时代重塑组织。

### 决心改变

>
> “如果外界的变化率超过了内部的变化率，那末日就不远了。” — Jack Welch, 2004
>
>
>

一位同事曾经告诉我他可以只用 “[信息技术基础设施库（ITIL）](https://en.wikipedia.org/wiki/ITIL)” 框架里面的词汇向一位项目经理解释 DevOps。

虽然这些框架 *似乎* 是反面的，但实际上它们都集中在风险和变革管理上。它们简单地介绍了这种管理的不同流程和工具。在 IT 圈子外面谈论 DevOps 时，这一点需要注意。不要强调流程问题和故障，而是显示更小的变化引起的风险 *更小* 等等。这是强调改变团队文化的有益方式：专注于 *新的* 功能而不是老问题，是改变的有效中介，特别是当您采用别人的框架进行参考时。

改革不仅仅只是 *重构* 组织；它也是跨越历史上不可跨越的鸿沟的新途径 —— 通过拒绝把像“敏捷”和“稳定”这样的东西作为互相排斥的力量来解决我之前提到的那些紧张局势。建立注重 *结果* 胜过 *功能* 的跨部门团队是一个有效的策略。把不同的团队 —— 其中每个人的工作依赖于其他人 —— 聚集起来围绕一个项目或目标是最常见的方法之一。消除这些团队之间的摩擦和改善沟通能够产生巨大的改进 —— 即使在坚持铁仓管理结构时（如果可以掌握，则不需要拆除孤岛）。在这些情况下，对改革的 *抵抗* 不是成功的一个指标；而对改革的拥抱才是。

这些也不是“最佳实例”，它们只是一种检查你自己的栅栏的方式。每个组织都会有独特的、由他们内部人员创造的栅栏。一旦你“知道了它的用途”，你就可以决定它是需要拆解还是掌握。

*本文是 Opensource.com 即将推出的关于开放组织和 IT 文化指南的一部分。[你可以在这注册以便当它发布时收到通知](https://opensource.com/open-organization/resources/book-series)。*

（题图 : opensource.com）

---

作者简介：

Matt Micene 是 Red Hat 公司的 Linux 和容器传播者。从架构和系统设计到数据中心设计，他在信息技术方面拥有超过 15 年的经验。他对关键技术（如容器，云计算和虚拟化）有深入的了解。他目前的重点是宣传红帽企业版 Linux，以及操作系统如何与计算环境的新时代相关。

---

via: <https://opensource.com/open-organization/17/5/what-is-the-point-of-DevOps>

作者：[Matt Micene](https://opensource.com/users/matt-micene) 译者：[zhousiyu325](https://github.com/zhousiyu325) 校对：[apemost](https://github.com/apemost)，[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


OpenGL 与 Go 教程（一）Hello, OpenGL

Summary: 这个教程的目的是给你一个切入点，让你对 OpenGL 有个基本的了解，然后教你怎么用 Go 操作它。我们最终的目标是用 OpenGL 在桌面窗口绘制游戏面板，进而实现康威生命游戏。

在macOS High Sierra上需要移动一下窗口才能看见三角形

go . opengl   绘制点 线   是这样写的        gl.COLOR = 0xff0000

        gl.COLOR_ATTACHMENT3(1.0, 0.0, 0.0)

        gl.BeginConditionalRender(aa, gl.POINTS)

        gl.VertexAttrib2f(0, 0.0, 0.0)

        gl.VertexAttrib2f(0, 0.1, 0.1)

        gl.VertexAttrib2f(0, 0.1, 0.2)

        gl.VertexAttrib2f(0, 0.1, 0.3)

        gl.VertexAttrib2f(0, 0.1, 0.4)

        gl.EndConditionalRender()

        gl.Flush()

这是固定管线的写法。4.1的可编程管线，要用VBO传入顶点数据！

![](/data/attachment/album/201710/05/212715clrssdrurc8nnras.jpg)

* [第一节: Hello, OpenGL](/article-8933-1.html)
* [第二节: 绘制游戏面板](/article-8937-1.html)
* [第三节：实现游戏功能](/article-8969-1.html)

这篇教程的所有源代码都可以在 [GitHub](https://github.com/KyleBanks/conways-gol) 上找到。

### 介绍

[OpenGL](https://www.opengl.org/) 是一门相当好的技术，适用于从桌面的 GUI 到游戏，到移动应用甚至 web 应用的多种类型的绘图工作。我敢保证，你今天看到的图形有些就是用 OpenGL 渲染的。可是，不管 OpenGL 多受欢迎、有多好用，与学习其它高级绘图库相比，学习 OpenGL 是要相当足够的决心的。

这个教程的目的是给你一个切入点，让你对 OpenGL 有个基本的了解，然后教你怎么用 [Go](https://golang.org/) 操作它。几乎每种编程语言都有绑定 OpenGL 的库，Go 也不例外，它有 [go-gl](https://github.com/go-gl/gl) 这个包。这是一个完整的套件，可以绑定 OpenGL ，适用于多种版本的 OpenGL。

这篇教程会按照下面列出的几个阶段进行介绍，我们最终的目标是用 OpenGL 在桌面窗口绘制游戏面板，进而实现[康威生命游戏](https://en.wikipedia.org/wiki/Conway's_Game_of_Life)。完整的源代码可以在 GitHub [github.com/KyleBanks/conways-gol](https://github.com/KyleBanks/conways-gol) 上获得，当你有疑惑的时候可以随时查看源代码，或者你要按照自己的方式学习也可以参考这个代码。

在我们开始之前，我们要先弄明白<ruby> 康威生命游戏 <rt>  Conway's Game of Life </rt></ruby> 到底是什么。这里是 [Wikipedia](https://en.wikipedia.org/wiki/Conway's_Game_of_Life) 上面的总结：

>
> 《生命游戏》，也可以简称为 Life，是一个细胞自动变化的过程，由英国数学家 John Horton Conway 于 1970 年提出。
>
>
> 这个“游戏”没有玩家，也就是说它的发展依靠的是它的初始状态，不需要输入。用户通过创建初始配置文件、观察它如何演变，或者对于高级“玩家”可以创建特殊属性的模式，进而与《生命游戏》进行交互。
>
>
> `规则`
>
>
> 《生命游戏》的世界是一个无穷多的二维正交的正方形细胞的格子世界，每一个格子都有两种可能的状态，“存活”或者“死亡”，也可以说是“填充态”或“未填充态”（区别可能很小，可以把它看作一个模拟人类/哺乳动物行为的早期模型，这要看一个人是如何看待方格里的空白）。每一个细胞与它周围的八个细胞相关联，这八个细胞分别是水平、垂直、斜对角相接的。在游戏中的每一步，下列事情中的一件将会发生：
>
>
> 1. 当任何一个存活的细胞的附近少于 2 个存活的细胞时，该细胞将会消亡，就像人口过少所导致的结果一样
> 2. 当任何一个存活的细胞的附近有 2 至 3 个存活的细胞时，该细胞在下一代中仍然存活。
> 3. 当任何一个存活的细胞的附近多于 3 个存活的细胞时，该细胞将会消亡，就像人口过多所导致的结果一样
> 4. 任何一个消亡的细胞附近刚好有 3 个存活的细胞，该细胞会变为存活的状态，就像重生一样。
>
>
>

不需要其他工具，这里有一个我们将会制作的演示程序：

![Conway's Game of Life - 示例游戏](/data/attachment/album/201710/05/212729obh007mah3d02noh.gif)

在我们的运行过程中，白色的细胞表示它是存活着的，黑色的细胞表示它已经死亡。

### 概述

本教程将会涉及到很多基础内容，从最基本的开始，但是你还是要对 Go 由一些最基本的了解 —— 至少你应该知道变量、切片、函数和结构体，并且装了一个 Go 的运行环境。我写这篇教程用的 Go 版本是 1.8，但它应该与之前的版本兼容。这里用 Go 语言实现没有什么特别新奇的东西，因此只要你有过类似的编程经历就行。

这里是我们在这个教程里将会讲到的东西：

* [第一节: Hello, OpenGL](https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-1-hello-opengl)： 安装 OpenGL 和 [GLFW](http://www.glfw.org/)，在窗口上绘制一个三角形。
* [第二节: 绘制游戏面板](https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-2-drawing-the-game-board)： 用三角形拼成方形，在窗口上用方形绘成格子。
* [第三节: 实现游戏功能](https://kylewbanks.com/blog/blog/tutorial-opengl-with-golang-part-3-implementing-the-game)： 实现 Conway 游戏

最后的源代码可以在 [GitHub](https://github.com/KyleBanks/conways-gol) 上获得，每一节的末尾有个*回顾*，包含该节相关的代码。如果有什么不清楚的地方或者是你感到疑惑的，看看每一节末尾的完整代码。

现在就开始吧！

### 安装 OpenGL 和 GLFW

我们介绍过 OpenGL，但是为了使用它，我们要有个窗口可以绘制东西。 [GLFW](http://www.glfw.org/) 是一款用于 OpenGL 的跨平台 API，允许我们创建并使用窗口，而且它也是 [go-gl](https://github.com/go-gl/glfw) 套件中提供的。

我们要做的第一件事就是确定 OpenGL 的版本。为了方便本教程，我们将会使用 `OpenGL v4.1`，但要是你的操作系统不支持最新的 OpenGL，你也可以用 `v2.1`。要安装 OpenGL，我们需要做这些事：

```
# 对于 OpenGL 4.1
$ go get github.com/go-gl/gl/v4.1-core/gl

# 或者 2.1
$ go get github.com/go-gl/gl/v2.1/gl

```

然后是安装 GLFW：

```
$ go get github.com/go-gl/glfw/v3.2/glfw

```

安装好这两个包之后，我们就可以开始了！先创建 `main.go` 文件，导入相应的包（我们待会儿会用到的其它东西）。

```
package main

import (
    "log"
    "runtime"

    "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
    "github.com/go-gl/glfw/v3.2/glfw"
)

```

接下来定义一个叫做 `main` 的函数，这是用来初始化 OpenGL 以及 GLFW，并显示窗口的：

```
const (
    width  = 500
    height = 500
)

func main() {
    runtime.LockOSThread()

    window := initGlfw()
    defer glfw.Terminate()

    for !window.ShouldClose() {
        // TODO
    }
}

// initGlfw 初始化 glfw 并且返回一个可用的窗口。
func initGlfw() *glfw.Window {
    if err := glfw.Init(); err != nil {
            panic(err)
    }

    glfw.WindowHint(glfw.Resizable, glfw.False)
    glfw.WindowHint(glfw.ContextVersionMajor, 4) // OR 2
    glfw.WindowHint(glfw.ContextVersionMinor, 1)
    glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
    glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)

    window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
    if err != nil {
            panic(err)
    }
    window.MakeContextCurrent()

    return window
}

```

好了，让我们花一分钟来运行一下这个程序，看看会发生什么。首先定义了一些常量， `width` 和 `height` —— 它们决定窗口的像素大小。

然后就是 `main` 函数。这里我们使用了 `runtime` 包的 `LockOSThread()`，这能确保我们总是在操作系统的同一个线程中运行代码，这对 GLFW 来说很重要，GLFW 需要在其被初始化之后的线程里被调用。讲完这个，接下来我们调用 `initGlfw` 来获得一个窗口的引用，并且推迟（`defer`）其终止。窗口的引用会被用在一个 `for` 循环中，只要窗口处于打开的状态，就执行某些事情。我们待会儿会讲要做的事情是什么。

`initGlfw` 是另一个函数，这里我们调用 `glfw.Init()` 来初始化 GLFW 包。然后我们定义了 GLFW 的一些全局属性，包括禁用调整窗口大小和改变 OpenGL 的属性。然后创建了 `glfw.Window`，这会在稍后的绘图中用到。我们仅仅告诉它我们想要的宽度和高度，以及标题，然后调用 `window.MakeContextCurrent`，将窗口绑定到当前的线程中。最后就是返回窗口的引用了。

如果你现在就构建、运行这个程序，你看不到任何东西。很合理，因为我们还没有用这个窗口做什么实质性的事。

定义一个新函数，初始化 OpenGL，就可以解决这个问题：

```
// initOpenGL 初始化 OpenGL 并且返回一个初始化了的程序。
func initOpenGL() uint32 {
    if err := gl.Init(); err != nil {
            panic(err)
    }
    version := gl.GoStr(gl.GetString(gl.VERSION))
    log.Println("OpenGL version", version)

    prog := gl.CreateProgram()
    gl.LinkProgram(prog)
    return prog
}

```

`initOpenGL` 就像之前的 `initGlfw` 函数一样，初始化 OpenGL 库，创建一个<ruby> 程序 <rt>  program </rt></ruby>。“程序”是一个包含了<ruby> 着色器 <rt>  shader </rt></ruby>的引用，稍后会用<ruby> 着色器 <rt>  shader </rt></ruby>绘图。待会儿会讲这一点，现在只用知道 OpenGL 已经初始化完成了，我们有一个程序的引用。我们还打印了 OpenGL 的版本，可以用于之后的调试。

回到 `main` 函数里，调用这个新函数：

```
func main() {
    runtime.LockOSThread()

    window := initGlfw()
    defer glfw.Terminate()

    program := initOpenGL()

    for !window.ShouldClose() {
        draw(window, program)
    }
}

```

你应该注意到了现在我们有 `program` 的引用，在我们的窗口循环中，调用新的 `draw` 函数。最终这个函数会绘制出所有细胞，让游戏状态变得可视化，但是现在它做的仅仅是清除窗口，所以我们只能看到一个全黑的屏幕：

```
func draw(window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(prog)

    glfw.PollEvents()
    window.SwapBuffers()
}

```

我们首先做的是调用 `gl.clear` 函数来清除上一帧在窗口中绘制的东西，给我们一个干净的面板。然后我们告诉 OpenGL 去使用我们的程序引用，这个引用还没有做什么事。最终我们告诉 GLFW 用 `PollEvents` 去检查是否有鼠标或者键盘事件（这一节里还不会对这些事件进行处理），告诉窗口去交换缓冲区 `SwapBuffers`。 [交换缓冲区](http://www.glfw.org/docs/latest/window_guide.html#buffer_swap) 很重要，因为 GLFW（像其他图形库一样）使用双缓冲，也就是说你绘制的所有东西实际上是绘制到一个不可见的画布上，当你准备好进行展示的时候就把绘制的这些东西放到可见的画布中 —— 这种情况下，就需要调用 `SwapBuffers` 函数。

好了，到这里我们已经讲了很多东西，花一点时间看看我们的实验成果。运行这个程序，你应该可以看到你所绘制的第一个东西：

![Conway's Game of Life - 第一个窗口](/data/attachment/album/201710/05/212730edjzf25dbsz599c9.png)

完美！

### 在窗口里绘制三角形

我们已经完成了一些复杂的步骤，即使看起来不多，但我们仍然需要绘制一些东西。我们会以三角形绘制开始，可能这第一眼看上去要比我们最终要绘制的方形更难，但你会知道这样的想法是错的。你可能不知道的是三角形或许是绘制的图形中最简单的，实际上我们最终会用某种方式把三角形拼成方形。

好吧，那么我们想要绘制一个三角形，怎么做呢？我们通过定义图形的顶点来绘制图形，把它们交给 OpenGL 来进行绘制。先在 `main.go` 的顶部里定义我们的三角形：

```
var (
    triangle = []float32{
        0, 0.5, 0, // top
        -0.5, -0.5, 0, // left
        0.5, -0.5, 0, // right
    }
)

```

这看上去很奇怪，让我们分开来看。首先我们用了一个 `float32` <ruby> 切片 <rt>  slice </rt></ruby>，这是一种我们总会在向 OpenGL 传递顶点时用到的数据类型。这个切片包含 9 个值，每三个值构成三角形的一个点。第一行， `0, 0.5, 0` 表示的是 X、Y、Z 坐标，是最上方的顶点，第二行是左边的顶点，第三行是右边的顶点。每一组的三个点都表示相对于窗口中心点的 X、Y、Z 坐标，大小在 `-1` 和 `1` 之间。因此最上面的顶点 X 坐标是 `0`，因为它在 X 方向上位于窗口中央，Y 坐标是 `0.5` 意味着它会相对窗口中央上移 1/4 个单位（因为窗口的范围是 `-1` 到 `1`），Z 坐标是 0。因为我们只需要在二维空间中绘图，所以 Z 值永远是 `0`。现在看一看左右两边的顶点，看看你能不能理解为什么它们是这样定义的 —— 如果不能立刻就弄清楚也没关系，我们将会在屏幕上去观察它，因此我们需要一个完美的图形来进行观察。

好了，我们定义了一个三角形，但是现在我们得把它画出来。要画出这个三角形，我们需要一个叫做<ruby> 顶点数组对象 <rt>  Vertex Array Object </rt></ruby>或者叫 vao 的东西，这是由一系列的点（也就是我们定义的三角形）创造的，这个东西可以提供给 OpenGL 来进行绘制。创建一个叫做 `makeVao` 的函数，然后我们可以提供一个点的切片，让它返回一个指向 OpenGL 顶点数组对象的指针：

```
// makeVao 执行初始化并从提供的点里面返回一个顶点数组
func makeVao(points []float32) uint32 {
    var vbo uint32
    gl.GenBuffers(1, &vbo)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)

    var vao uint32
    gl.GenVertexArrays(1, &vao)
    gl.BindVertexArray(vao)
    gl.EnableVertexAttribArray(0)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)

    return vao
}

```

首先我们创造了<ruby> 顶点缓冲区对象 <rt>  Vertex Buffer Object </rt></ruby> 或者说 vbo 绑定到我们的 `vao` 上，`vbo` 是通过所占空间（也就是 4 倍 `len(points)` 大小的空间）和一个指向顶点的指针（`gl.Ptr(points)`）来创建的。你也许会好奇为什么它是 4 倍 —— 而不是 6 或者 3 或者 1078 呢？原因在于我们用的是 `float32` 切片，32 个位的浮点型变量是 4 个字节，因此我们说这个缓冲区以字节为单位的大小是点个数的 4 倍。

现在我们有缓冲区了，可以创建 `vao` 并用 `gl.BindBuffer` 把它绑定到缓冲区上，最后返回 `vao`。这个 `vao` 将会被用于绘制三角形！

回到 `main` 函数：

```
func main() {
    ...

    vao := makeVao(triangle)
    for !window.ShouldClose() {
        draw(vao, window, program)
    }
}

这里我们调用了 `makeVao` ，从我们之前定义的 `triangle` 顶点中获得 `vao` 引用，将它作为一个新的参数传递给 `draw` 函数：

func draw(vao uint32, window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(program)

    gl.BindVertexArray(vao)
    gl.DrawArrays(gl.TRIANGLES, 0, int32(len(triangle) / 3))

    glfw.PollEvents()
    window.SwapBuffers()
}

```

然后我们把 OpenGL 绑定到 `vao` 上，这样当我们告诉 OpenGL 三角形切片的顶点数（除以 3，是因为每一个点有 X、Y、Z 坐标），让它去 `DrawArrays` ，它就知道要画多少个顶点了。

如果你这时候运行程序，你可能希望在窗口中央看到一个美丽的三角形，但是不幸的是你还看不到。还有一件事情没做，我们告诉 OpenGL 我们要画一个三角形，但是我们还要告诉它*怎么*画出来。

要让它画出来，我们需要叫做<ruby> 片元着色器 <rt>  fragment shader </rt></ruby>和<ruby> 顶点着色器 <rt>  vertex shader </rt></ruby>的东西，这些已经超出本教程的范围了（老实说，也超出了我对 OpenGL 的了解），但 [Harold Serrano 在 Quora](https://www.quora.com/What-is-a-vertex-shader-and-what-is-a-fragment-shader/answer/Harold-Serrano?srid=aVb) 上对对它们是什么给出了完美的介绍。我们只需要理解，对于这个应用来说，着色器是它内部的小程序（用 [OpenGL Shader Language 或 GLSL](https://www.opengl.org/sdk/docs/tutorials/ClockworkCoders/glsl_overview.php) 编写的），它操作顶点进行绘制，也可用于确定图形的颜色。

添加两个 `import` 和一个叫做 `compileShader` 的函数：

```
import (
    "strings"
    "fmt"
)

func compileShader(source string, shaderType uint32) (uint32, error) {
    shader := gl.CreateShader(shaderType)

    csources, free := gl.Strs(source)
    gl.ShaderSource(shader, 1, csources, nil)
    free()
    gl.CompileShader(shader)

    var status int32
    gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
    if status == gl.FALSE {
        var logLength int32
        gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)

        log := strings.Repeat("\x00", int(logLength+1))
        gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))

        return 0, fmt.Errorf("failed to compile %v: %v", source, log)
    }

    return shader, nil
}

```

这个函数的目的是以字符串的形式接受着色器源代码和它的类型，然后返回一个指向这个编译好的着色器的指针。如果编译失败，我们就会获得出错的详细信息。

现在定义着色器，在 `makeProgram` 里编译。回到我们的 `const` 块中，我们在这里定义了 `width` 和 `hegiht`。

```
vertexShaderSource = `
    #version 410
    in vec3 vp;
    void main() {
        gl_Position = vec4(vp, 1.0);
    }
` + "\x00"

fragmentShaderSource = `
    #version 410
    out vec4 frag_colour;
    void main() {
        frag_colour = vec4(1, 1, 1, 1);
    }
` + "\x00"

```

如你所见，这是两个包含了 GLSL 源代码字符串的着色器，一个是<ruby> 顶点着色器 <rt>  vertex shader </rt></ruby>，另一个是<ruby> 片元着色器 <rt>  fragment shader </rt></ruby>。唯一比较特殊的地方是它们都要在末尾加上一个空终止字符，`\x00` —— OpenGL 需要它才能编译着色器。注意 `fragmentShaderSource`，这是我们用 RGBA 形式的值通过 `vec4` 来定义我们图形的颜色。你可以修改这里的值来改变这个三角形的颜色，现在的值是 `RGBA(1, 1, 1, 1)` 或者说是白色。

同样需要注意的是这两个程序都是运行在 `#version 410` 版本下，如果你用的是 OpenGL 2.1，那你也可以改成 `#version 120`。这里 `120` 不是打错的，如果你用的是 OpenGL 2.1，要用 `120` 而不是 `210`！

接下来在 `initOpenGL` 中我们会编译着色器，把它们附加到我们的 `program` 中。

```
func initOpenGL() uint32 {
    if err := gl.Init(); err != nil {
        panic(err)
    }
    version := gl.GoStr(gl.GetString(gl.VERSION))
    log.Println("OpenGL version", version)

    vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
    if err != nil {
        panic(err)
    }
    fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
    if err != nil {
        panic(err)
    }

    prog := gl.CreateProgram()
    gl.AttachShader(prog, vertexShader)
    gl.AttachShader(prog, fragmentShader)
    gl.LinkProgram(prog)
    return prog
}

```

这里我们用顶点着色器（`vertexShader`）调用了 `compileShader` 函数，指定它的类型是 `gl.VERTEX_SHADER`，对片元着色器（`fragmentShader`）做了同样的事情，但是指定的类型是 `gl.FRAGMENT_SHADER`。编译完成后，我们把它们附加到程序中，调用 `gl.AttachShader`，传递程序（`prog`）以及编译好的着色器作为参数。

现在我们终于可以看到我们漂亮的三角形了！运行程序，如果一切顺利的话你会看到这些：

![Conway's Game of Life - Hello, Triangle!](/data/attachment/album/201710/05/212730fr944kc3kr3akka3.png)

### 总结

是不是很惊喜！这些代码画出了一个三角形，但我保证我们已经完成了大部分的 OpenGL 代码，在接下来的章节中我们还会用到这些代码。我十分推荐你花几分钟修改一下代码，看看你能不能移动三角形，改变三角形的大小和颜色。OpenGL 可以令人心生畏惧，有时想要理解发生了什么很困难，但是要记住，这不是魔法 - 它只不过看上去像魔法。

下一节里我们讲会用两个锐角三角形拼出一个方形 - 看看你能不能在进入下一节前试着修改这一节的代码。不能也没有关系，因为我们在 [第二节](https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-2-drawing-the-game-board) 还会编写代码， 接着创建一个有许多方形的格子，我们把它当做游戏面板。

最后，在[第三节](https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-3-implementing-the-game) 里我们会用格子来实现 *Conway’s Game of Life*！

### 回顾

本教程 `main.go` 文件的内容如下：

```
package main

import (
    "fmt"
    "log"
    "runtime"
    "strings"

    "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
    "github.com/go-gl/glfw/v3.2/glfw"
)

const (
    width  = 500
    height = 500

    vertexShaderSource = `
        #version 410
        in vec3 vp;
        void main() {
            gl_Position = vec4(vp, 1.0);
        }
    ` + "\x00"

    fragmentShaderSource = `
        #version 410
        out vec4 frag_colour;
        void main() {
            frag_colour = vec4(1, 1, 1, 1.0);
        }
    ` + "\x00"
)

var (
    triangle = []float32{
        0, 0.5, 0,
        -0.5, -0.5, 0,
        0.5, -0.5, 0,
    }
)

func main() {
    runtime.LockOSThread()

    window := initGlfw()
    defer glfw.Terminate()
    program := initOpenGL()

    vao := makeVao(triangle)
    for !window.ShouldClose() {
        draw(vao, window, program)
    }
}

func draw(vao uint32, window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(program)

    gl.BindVertexArray(vao)
    gl.DrawArrays(gl.TRIANGLES, 0, int32(len(triangle)/3))

    glfw.PollEvents()
    window.SwapBuffers()
}

// initGlfw 初始化 glfw 并返回一个窗口供使用。
func initGlfw() *glfw.Window {
    if err := glfw.Init(); err != nil {
        panic(err)
    }
    glfw.WindowHint(glfw.Resizable, glfw.False)
    glfw.WindowHint(glfw.ContextVersionMajor, 4)
    glfw.WindowHint(glfw.ContextVersionMinor, 1)
    glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
    glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)

    window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
    if err != nil {
        panic(err)
    }
    window.MakeContextCurrent()

    return window
}

// initOpenGL 初始化 OpenGL 并返回一个已经编译好的着色器程序
func initOpenGL() uint32 {
    if err := gl.Init(); err != nil {
        panic(err)
    }
    version := gl.GoStr(gl.GetString(gl.VERSION))
    log.Println("OpenGL version", version)

    vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
    if err != nil {
        panic(err)
    }

    fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
    if err != nil {
        panic(err)
    }

    prog := gl.CreateProgram()
    gl.AttachShader(prog, vertexShader)
    gl.AttachShader(prog, fragmentShader)
    gl.LinkProgram(prog)
    return prog
}

// makeVao 执行初始化并从提供的点里面返回一个顶点数组
func makeVao(points []float32) uint32 {
    var vbo uint32
    gl.GenBuffers(1, &vbo)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)

    var vao uint32
    gl.GenVertexArrays(1, &vao)
    gl.BindVertexArray(vao)
    gl.EnableVertexAttribArray(0)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)

    return vao
}

func compileShader(source string, shaderType uint32) (uint32, error) {
    shader := gl.CreateShader(shaderType)

    csources, free := gl.Strs(source)
    gl.ShaderSource(shader, 1, csources, nil)
    free()
    gl.CompileShader(shader)

    var status int32
    gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
    if status == gl.FALSE {
        var logLength int32
        gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)

        log := strings.Repeat("\x00", int(logLength+1))
        gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))

        return 0, fmt.Errorf("failed to compile %v: %v", source, log)
    }

    return shader, nil
}

```

请在 Twitter [@kylewbanks](https://twitter.com/kylewbanks) 上告诉我这篇文章对你是否有帮助，或者点击下方的关注，以便及时获取最新文章！

---

via: <https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-1-hello-opengl>

作者：[kylewbanks](https://twitter.com/kylewbanks) 译者：[GitFuture](https://github.com/GitFuture) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


关于 Linux 你可能不是非常了解的七件事

学习的快乐！

![Hidden features of Linux](/data/attachment/album/201710/05/220827fgbirkfifb5jrkab.jpg)

使用 Linux 最酷的事情之一就是随着时间的推移，你可以不断获得新的知识。每天，你都可能会遇到一个新的实用工具，或者只是一个不太熟悉的奇技淫巧，但是却非常有用。这些零碎的东西并不总是能够改变生活，但是却是专业知识的基础。

即使是专家，也不可能事事皆知。无论你有多少经验，可能总会有更多的东西需要你去学习。所以，在这儿我列出了七件关于 Linux 你可能不知道的事情。

### 一个查找命令历史的交互模式

你可能对 `history` 命令非常熟悉，它会读取 bash 历史，然后以编号列表的方式输出到标准输出（`stdout`）。然而，如果你在 `curl` 命令的海洋里寻找一个特定的链接（URL），那么这个列表并不总是那么容易阅读的。

你还可以有另一个选择，Linux 有一个交互式的反向搜索可以帮助你解决这个问题。你可以通过快捷键 `ctrl+r`启动交互模式，然后进入一个交互提示中，它将会根据你提供的字符串来向后搜索 bash 历史，你可以通过再次按下 `ctrl+r` 向后搜索更老的命令，或者按下 `ctrl+s` 向前搜索。

注意，`ctrl+s` 有时会与 XON/XOFF 流控制冲突，即 XON/XOFF 流控制也会使用该快捷键。你可以通过运行 `stty -ixon` 命令来禁用该快捷键。在你的个人电脑上，这通常是有用的，但是在禁用前，确保你不需要 XON/XOFF 。

### Cron 不是安排任务的唯一方式

Cron 任务对于任何水平的系统管理员，无论是毫无经验的初学者，还是经验丰富的专家来说，都是非常有用的。但是，如果你需要安排一个一次性的任务，那么 `at` 命令为你提供了一个快捷的方式来创建任务，从而你不需要接触 crontab 。

`at` 命令的运行方式是在后面紧跟着你想要运行任务的运行时间。时间是灵活的，因为它支持许多时间格式。包括下面这些例子：

```
at 12:00 PM September 30 2017
at now + 1 hour
at 9:00 AM tomorrow

```

当你以带参数的方式输入 `at` 命令以后，将会提示你该命令将在你的 Linux 系统上运行。这可能是一个备份脚本，一套维护任务，或者甚至是一个普通的 bash 命令。如果要结束任务，可以按 `ctrl+d` 。

另外，你可以使用 `atq` 命令查看当前用户的所有任务，或者使用 `sudo atq` 查看所有用户的任务。它将会展示出所有排定好的任务，并且每个任务都伴有一个 ID 。如果你想取消一个排定好的任务，可以使用 `atrm` 命令，并且以任务 ID 作为参数。

### 你可以按照功能搜索命令，而不仅仅是通过名字

记住命令的名字非常困难，特别是对于初学者来说。幸运的是，Linux 附带了一个通过名字和描述来搜索 man 页面的工具。

下次，如果你没有记住你想要使用的工具的名称，你可以尝试使用 `apropos` 命令加上你想要干的事情的描述。比如，`apropos build filesystem` 将会返回一系列名字和描述包括了 “build” 和 “filesystem” 单词的工具。

`apropos` 命令接受一个或多个字符串作为参数，但同时它也有其他参数，比如你可以使用 `-r` 参数，从而通过正则表达式来搜索。

### 一个允许你来管理系统版本的替代系统

如果你曾进行过软件开发，你就会明白跨项目管理不同版本的语言的支持的重要性。许多 Linux 发行版都有工具可以来处理不同的内建版本。

可执行文件比如 `java` 往往符号链接到目录 `/etc/alternatives` 下。反过来，该目录会将符号链接存储为二进制文件并提供一个管理这些链接的接口。Java 可能是替代系统最常管理的语言，但是，经过一些配置，它也可以作为其他应用程序替代品，比如 NVM 和 RVM （NVM 和 RVM 分别是 NodeJS 和 Ruby 的版本管理器）。

在基于 Debian 的系统中，你可以使用 `update-alternatives` 命令创建和管理这些链接。在 CentOS 中，这个工具就叫做 `alternatives` 。通过更改你的 alternatives 文件中的链接，你便可以安装一个语言的多个版本，并且在不同的情况下使用不同的二进制。这个替代系统也提供了对任何你可能在命令行运行的程序的支持。

### `shred` 命令是更加安全的删除文件方式

我们大多数时候总是使用 `rm` 命令来删除文件。但是文件去哪儿了呢？真相是 `rm` 命令所做的事情并不是像你所想像的那样，它仅仅删除了文件系统和硬盘上的数据的硬链接。硬盘上的数据依旧存在，直到被另一个应用重写覆盖。对于非常敏感的数据来说，这会带来一个很大的安全隐患。

`shred` 命令是 `rm` 命令的升级版。当你使用 `shred` 命令删除一个文件之后，文件中的数据会被多次随机覆写。甚至有一个选项可以在随机覆写之后对所有的数据进行清零。

如果你想安全的删除一个文件并且以零覆盖，那么可以使用下面的命令：

`shred -u -z [file name]`

同时，你也可以使用 `-n` 选项和一个数字作为参数，从而指定在随机覆盖数据的时候迭代多少次。

### 通过自动更正来避免输入很长的无效文件路径

有多少次，你输入一个文件的绝对路径，然而却看到“没有该文件或目录”的消息。任何人都会明白输入一个很长的字符串的痛苦。幸运的是，有一个很简单的解决办法。

内建的 `shopt` 命令允许你设置不同的选项来改变 shell 的行为。设置 `cdspell` 选项是避免输入文件路径时一个字母出错的头痛的一个简单方式。你可以通过运行 `shopt -s cdspell` 命令来启用该选项。启用该选项后，当你想要切换目录时，会自动更正为最匹配的目录。

Shell 选项是节省时间的一个好方法（更不用说减少麻烦），此外还有许许多多的其他选项。如果想查看你的系统中所有选项的完整列表，可以运行不带参数的 `shopt` 命令。需要注意的是，这是 bash 的特性，如果你运行 zsh 或者其他可供选择的 shell，可能无法使用。

### 通过子 shell 返回到当前目录

如果你曾经配置过一个比较复杂的系统，那么你可能会发现你需要频繁的更换目录，从而很难跟踪你所在的位置。如果在运行完一个命令后自动返回到当前位置，不是很好吗？

Linux 系统实际上提供了一个解决该问题的方法，并且非常简单。如果你想通过 `cd` 命令进入另一个目录完成一些任务，然后再返回当前工作目录，那么你可以将命令置于括号中。你可以在你的 Linux 系统上尝试下面这个命令。记住你当前的工作目录，然后运行：

```
(cd /etc && ls -a)

```

该命令会输出 `/etc` 目录的内容。现在，检查你的当前工作目录。它和执行该命令前的目录一样，而不是 `/etc` 目录。

它是如何工作的呢？运行一个括号中的命令会创建一个子 shell 或一个当前 shell 进程的复刻副本。该子 shell 可以访问所有的父变量，反之则不行。所以请记住，你是在运行一个非常复杂的单行命令。

在并行处理中经常使用子 shell ，但是在命令行中，它也能为你带来同样的力量，从而使你在浏览文件系统时更加容易。

---

作者简介：

Phil Zona 是 Linux Academy 的技术作家。他编写了 AWS、Microsoft Azure 和 Linux 系统管理的指南和教程。他同时也管理着 Cloud Assessments 博客，该博客旨在帮助个人通过技术实现他们的事业目标。

---

via: <http://opensourceforu.com/2017/09/top-7-things-linux-may-not-known-far/>

作者：[PHIL ZONA](http://opensourceforu.com/author/phil-zona/) 译者：[ucasFL](https://github.com/ucasFL) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


论 HTTP 性能，Go 与 .NET Core 一争雌雄

Summary: 如前面所提的，我们会比较它们相同的东西，比如应用程序、预期响应及运行时的稳定性，所以我们不会把像对 JSON 或者 XML 的编码、解码这些烦多的事情加入比较游戏中来，仅仅只会使用简单的文本消息。为了公平起见，我们会分别使用 Go 和 .NET Core 的 MVC 架构模式。

![](/data/attachment/album/201710/06/111305ltswozo3cmicl3z6.png)

朋友们，你们好！

近来，我听到了大量的关于新出的 .NET Core 和其性能的讨论，尤其在 Web 服务方面的讨论更甚。

因为是新出的，我不想立马就比较两个不同的东西，所以我耐心等待，想等发布更稳定的版本后再进行。

本周一（8 月 14 日），微软[发布 .NET Core 2.0 版本](https://blogs.msdn.microsoft.com/dotnet/2017/08/14/announcing-net-core-2-0/)，因此，我准备开始。您们认为呢？

如前面所提的，我们会比较它们相同的东西，比如应用程序、预期响应及运行时的稳定性，所以我们不会把像对 JSON 或者 XML 的编码、解码这些烦多的事情加入比较游戏中来，仅仅只会使用简单的文本消息。为了公平起见，我们会分别使用 Go 和 .NET Core 的 [MVC 架构模式](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)。

### 参赛选手

[Go](https://golang.org/) （或称 Golang）： 是一种[快速增长](http://www.tiobe.com/tiobe-index/)的开源编程语言，旨在构建出简单、快捷和稳定可靠的应用软件。

用于支持 Go 语言的 MVC web 框架并不多，还好我们找到了 Iris ，可胜任此工作。

[Iris](http://iris-go.com/)： 支持 Go 语言的快速、简单和高效的微型 Web 框架。它为您的下一代网站、API 或分布式应用程序奠定了精美的表现方式和易于使用的基础。

[C#](https://en.wikipedia.org/wiki/C_Sharp_%28programming_language%29)： 是一种通用的、面向对象的编程语言。其开发团队由 [Anders Hejlsberg](https://twitter.com/ahejlsberg) 领导。

[.NET Core](https://www.microsoft.com/net/)： 跨平台，可以在极少时间内开发出高性能的应用程序。

可从 <https://golang.org/dl> 下载 Go ，从 <https://www.microsoft.com/net/core> 下载 .NET Core。

在下载和安装好这些软件后，还需要为 Go 安装 Iris。安装很简单，仅仅只需要打开终端，然后执行如下语句：

```
go get -u github.com/kataras/iris

```

### 基准

#### 硬件

* 处理器： Intel(R) Core(TM) i7–4710HQ CPU @ 2.50GHz 2.50GHz
* 内存： 8.00 GB

#### 软件

* 操作系统： 微软 Windows [10.0.15063 版本]， 电源计划设置为“高性能”
* HTTP 基准工具： <https://github.com/codesenberg/bombardier>， 使用最新的 1.1 版本。
* .NET Core： <https://www.microsoft.com/net/core>， 使用最新的 2.0 版本。
* Iris： <https://github.com/kataras/iris>， 使用基于 [Go 1.8.3](https://golang.org/) 构建的最新 8.3 版本。

两个应用程序都通过请求路径 “api/values/{id}” 返回文本“值”。

##### .NET Core MVC

![](/data/attachment/album/201710/06/111305kfvhsvvawf2o1sc5.png)

Logo 由 [Pablo Iglesias](https://github.com/campusMVP/dotnetCoreLogoPack) 设计。

可以使用 `dotnet new webapi` 命令创建项目，其 `webapi` 模板会为您生成代码，代码包含 `GET` 请求方法的 `返回“值”`。

源代码：

```
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace netcore_mvc
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }
}

```

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace netcore_mvc
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvcCore();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMvc();
        }
    }
}

```

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace netcore_mvc.Controllers
{
    // ValuesController is the equivalent
    // `ValuesController` of the Iris 8.3 mvc application.
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // Get handles "GET" requests to "api/values/{id}".
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }

        // Put handles "PUT" requests to "api/values/{id}".
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }

        // Delete handles "DELETE" requests to "api/values/{id}".
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

```

运行 .NET Core web 服务项目：

```
$ cd netcore-mvc
$ dotnet run -c Release
Hosting environment: Production
Content root path: C:\mygopath\src\github.com\kataras\iris\_benchmarks\netcore-mvc
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

```

运行和定位 HTTP 基准工具：

```
$ bombardier -c 125 -n 5000000 http://localhost:5000/api/values/5
Bombarding http://localhost:5000/api/values/5 with 5000000 requests using 125 connections
 5000000 / 5000000 [=====================================================] 100.00% 2m3s
Done!
Statistics        Avg      Stdev        Max
  Reqs/sec     40226.03    8724.30     161919
  Latency        3.09ms     1.40ms   169.12ms
  HTTP codes:
    1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:     8.91MB/s

```

##### Iris MVC

![](/data/attachment/album/201710/06/111305gx85k8pcgchtz8gc.png)

Logo 由 [Santosh Anand](https://github.com/santoshanand) 设计。

源代码：

```
package main

import (
    "github.com/kataras/iris"
    "github.com/kataras/iris/_benchmarks/iris-mvc/controllers"
)

func main() {
    app := iris.New()
    app.Controller("/api/values/{id}", new(controllers.ValuesController))
    app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker)
}

```

```
package controllers

import "github.com/kataras/iris/mvc"

// ValuesController is the equivalent
// `ValuesController` of the .net core 2.0 mvc application.
type ValuesController struct {
    mvc.Controller
}

// Get handles "GET" requests to "api/values/{id}".
func (vc *ValuesController) Get() {
    // id,_ := vc.Params.GetInt("id")
    vc.Ctx.WriteString("value")
}

// Put handles "PUT" requests to "api/values/{id}".
func (vc *ValuesController) Put() {}

// Delete handles "DELETE" requests to "api/values/{id}".
func (vc *ValuesController) Delete() {}

```

运行 Go web 服务项目：

```
$ cd iris-mvc
$ go run main.go
Now listening on: http://localhost:5000
Application started. Press CTRL+C to shut down.

```

运行和定位 HTTP 基准工具：

```
$ bombardier -c 125 -n 5000000 http://localhost:5000/api/values/5
Bombarding http://localhost:5000/api/values/5 with 5000000 requests using 125 connections
 5000000 / 5000000 [======================================================] 100.00% 47s
Done!
Statistics        Avg      Stdev        Max
  Reqs/sec    105643.81    7687.79     122564
  Latency        1.18ms   366.55us    22.01ms
  HTTP codes:
    1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:    19.65MB/s

```

想通过图片来理解的人，我也把我的屏幕截屏出来了！

请点击[这儿](https://github.com/kataras/iris/tree/master/_benchmarks/screens)可以看到这些屏幕快照。

#### 总结

* 完成 `5000000 个请求`的时间 - 越短越好。
* 请求次数/每秒 - 越大越好。
* 等待时间 — 越短越好。
* 吞吐量 — 越大越好。
* 内存使用 — 越小越好。
* LOC (代码行数) — 越少越好。

.NET Core MVC 应用程序，使用 86 行代码，运行 2 分钟 8 秒，每秒接纳 39311.56 个请求，平均 3.19ms 等待，最大时到 229.73ms，内存使用大约为 126MB（不包括 dotnet 框架）。

Iris MVC 应用程序，使用 27 行代码，运行 47 秒，每秒接纳 105643.71 个请求，平均 1.18ms 等待，最大时到 22.01ms，内存使用大约为 12MB。

>
> 还有另外一个模板的基准，滚动到底部。
>
>
>

**2017 年 8 月 20 号更新**

[Josh Clark](https://twitter.com/clarkis117) 和 [Scott Hanselman](https://twitter.com/shanselman)在此 [tweet 评论](https://twitter.com/shanselman/status/899005786826788865)上指出，.NET Core `Startup.cs` 文件中 `services.AddMvc();` 这行可以替换为 `services.AddMvcCore();`。我听从他们的意见，修改代码，重新运行基准，该文章的 .NET Core 应用程序的基准输出已经修改。

@topdawgevh @shanselman 他们也在使用 `AddMvc()` 而不是 `AddMvcCore()` ...，难道都不包含中间件？

 —  @clarkis117

@clarkis117 @topdawgevh Cool @MakisMaropoulos @ben*a*adams @davidfowl 我们来看看。认真学习下怎么使用更简单的性能默认值。

 —  @shanselman

@shanselman @clarkis117 @topdawgevh @ben*a*adams @davidfowl @shanselman @ben*a*adams @davidfowl 谢谢您们的反馈意见。我已经修改，更新了结果，没什么不同。对其它的建议，我非常欢迎。

 —  @MakisMaropoulos

>
> 它有点稍微的不同但相差不大（从 8.61MB/s 到 8.91MB/s）
>
>
>

想要了解跟 `services.AddMvc()` 标准比较结果的，可以点击[这儿](https://github.com/kataras/iris/blob/master/_benchmarks/screens/5m_requests_netcore-mvc.png)。

### 想再多了解点儿吗?

我们再制定一个基准，产生 `1000000 次请求`，这次会通过视图引擎由模板生成 `HTML` 页面。

#### .NET Core MVC 使用的模板

```
using System;

namespace netcore_mvc_templates.Models
{
    public class ErrorViewModel
    {
        public string Title { get; set; }
        public int Code { get; set; }
    }
}

```

```
 using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using netcore_mvc_templates.Models;

namespace netcore_mvc_templates.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult About()
        {
            ViewData["Message"] = "Your application description page.";

            return View();
        }

        public IActionResult Contact()
        {
            ViewData["Message"] = "Your contact page.";

            return View();
        }

        public IActionResult Error()
        {
            return View(new ErrorViewModel { Title = "Error", Code = 500});
        }
    }
}

```

```
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace netcore_mvc_templates
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }
}

```

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace netcore_mvc_templates
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            /*  An unhandled exception was thrown by the application.
                System.InvalidOperationException: No service for type
                'Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory' has been registered.
                Solution: Use AddMvc() instead of AddMvcCore() in Startup.cs and it will work.
            */
            // services.AddMvcCore();
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

```

```
/*
wwwroot/css
wwwroot/images
wwwroot/js
wwwroot/lib
wwwroot/favicon.ico

Views/Shared/_Layout.cshtml
Views/Shared/Error.cshtml

Views/Home/About.cshtml
Views/Home/Contact.cshtml
Views/Home/Index.cshtml

These files are quite long to be shown in this article but you can view them at:
https://github.com/kataras/iris/tree/master/_benchmarks/netcore-mvc-templates

```

运行 .NET Core 服务项目：

```
$ cd netcore-mvc-templates
$ dotnet run -c Release
Hosting environment: Production
Content root path: C:\mygopath\src\github.com\kataras\iris\_benchmarks\netcore-mvc-templates
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

```

运行 HTTP 基准工具：

```
Bombarding http://localhost:5000 with 1000000 requests using 125 connections
 1000000 / 1000000 [====================================================] 100.00% 1m20s
Done!
Statistics Avg Stdev Max
 Reqs/sec 11738.60 7741.36 125887
 Latency 10.10ms 22.10ms 1.97s
 HTTP codes:
 1xx — 0, 2xx — 1000000, 3xx — 0, 4xx — 0, 5xx — 0
 others — 0
 Throughput: 89.03MB/s

```

#### Iris MVC 使用的模板

```
package controllers

import "github.com/kataras/iris/mvc"

type AboutController struct{ mvc.Controller }

func (c *AboutController) Get() {
    c.Data["Title"] = "About"
    c.Data["Message"] = "Your application description page."
    c.Tmpl = "about.html"
}

```

```
package controllers

import "github.com/kataras/iris/mvc"

type ContactController struct{ mvc.Controller }

func (c *ContactController) Get() {
    c.Data["Title"] = "Contact"
    c.Data["Message"] = "Your contact page."
    c.Tmpl = "contact.html"
}

```

```
package models

// HTTPError a silly structure to keep our error page data.
type HTTPError struct {
    Title string
    Code  int
}

```

```
package controllers

import "github.com/kataras/iris/mvc"

type IndexController struct{ mvc.Controller }

func (c *IndexController) Get() {
    c.Data["Title"] = "Home Page"
    c.Tmpl = "index.html"
}

```

```
package main

import (
    "github.com/kataras/iris/_benchmarks/iris-mvc-templates/controllers"

    "github.com/kataras/iris"
    "github.com/kataras/iris/context"
)

const (
    // templatesDir is the exactly the same path that .NET Core is using for its templates,
    // in order to reduce the size in the repository.
    // Change the "C\\mygopath" to your own GOPATH.
    templatesDir = "C:\\mygopath\\src\\github.com\\kataras\\iris\\_benchmarks\\netcore-mvc-templates\\wwwroot"
)

func main() {
    app := iris.New()
    app.Configure(configure)

    app.Controller("/", new(controllers.IndexController))
    app.Controller("/about", new(controllers.AboutController))
    app.Controller("/contact", new(controllers.ContactController))

    app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker)
}

func configure(app *iris.Application) {
    app.RegisterView(iris.HTML("./views", ".html").Layout("shared/layout.html"))
    app.StaticWeb("/public", templatesDir)
    app.OnAnyErrorCode(onError)
}

type err struct {
    Title string
    Code  int
}

func onError(ctx context.Context) {
    ctx.ViewData("", err{"Error", ctx.GetStatusCode()})
    ctx.View("shared/error.html")
}

```

```
/*
../netcore-mvc-templates/wwwroot/css
../netcore-mvc-templates/wwwroot/images
../netcore-mvc-templates/wwwroot/js
../netcore-mvc-templates/wwwroot/lib
../netcore-mvc-templates/wwwroot/favicon.ico
views/shared/layout.html
views/shared/error.html
views/about.html
views/contact.html
views/index.html
These files are quite long to be shown in this article but you can view them at:
https://github.com/kataras/iris/tree/master/_benchmarks/iris-mvc-templates
*/

```

运行 Go 服务项目：

```
$ cd iris-mvc-templates
$ go run main.go
Now listening on: http://localhost:5000
Application started. Press CTRL+C to shut down.

```

运行 HTTP 基准工具：

```
Bombarding http://localhost:5000 with 1000000 requests using 125 connections
 1000000 / 1000000 [======================================================] 100.00% 37s
Done!
Statistics Avg Stdev Max
 Reqs/sec 26656.76 1944.73 31188
 Latency 4.69ms 1.20ms 22.52ms
 HTTP codes:
 1xx — 0, 2xx — 1000000, 3xx — 0, 4xx — 0, 5xx — 0
 others — 0
 Throughput: 192.51MB/s

```

#### 总结

* 完成 `1000000 个请求`的时间 - 越短越好。
* 请求次数/每秒 - 越大越好。
* 等待时间 — 越短越好。
* 内存使用 — 越小越好。
* 吞吐量 — 越大越好。

.NET Core MVC 模板应用程序，运行 1 分钟 20 秒，每秒接纳 11738.60 个请求，同时每秒生成 89.03M 页面，平均 10.10ms 等待，最大时到 1.97s，内存使用大约为 193MB（不包括 dotnet 框架）。

Iris MVC 模板应用程序，运行 37 秒，每秒接纳 26656.76 个请求，同时每秒生成 192.51M 页面，平均 1.18ms 等待，最大时到 22.52ms，内存使用大约为 17MB。

### 接下来呢？

[这里](https://github.com/kataras/iris/tree/master/_benchmarks)有上面所示的源代码，请下载下来，在您本地以同样的基准运行，然后把运行结果在这儿给大家分享。

想添加 Go 或 C# .net core WEB 服务框架到列表的朋友请向[这个仓库](https://github.com/kataras/iris)的 `_benchmarks` 目录推送 PR。

我也需要亲自感谢下 [dev.to](https://dev.to/kataras/go-vsnet-core-in-terms-of-http-performance) 团队，感谢把我的这篇文章分享到他们的 Twitter 账户。

感谢大家真心反馈，玩得开心！

#### 更新 : 2017 年 8 月 21 ，周一

很多人联系我，希望看到一个基于 .NET Core 的较低级别 Kestrel 的基准测试文章。

因此我完成了，请点击下面的[链接](https://medium.com/@kataras/iris-go-vs-net-core-kestrel-in-terms-of-http-performance-806195dc93d5)来了解 Kestrel 和 Iris 之间的性能差异，它还包含一个会话存储管理基准！

---

via: <https://hackernoon.com/go-vs-net-core-in-terms-of-http-performance-7535a61b67b8>

作者：[Gerasimos Maropoulos](https://hackernoon.com/@kataras?source=post_header_lockup) 译者：[runningwater](https://github.com/runningwater) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


开发一个 Linux 调试器（九）：处理变量

Summary: 变量是偷偷摸摸的。有时，它们会很高兴地呆在寄存器中，但是一转头就会跑到堆栈中。

变量是偷偷摸摸的。有时，它们会很高兴地呆在寄存器中，但是一转头就会跑到堆栈中。

![](/data/attachment/album/201710/06/180943lioqi6q6ortjfur0.jpg)

变量是偷偷摸摸的。有时，它们会很高兴地呆在寄存器中，但是一转头就会跑到堆栈中。为了优化，编译器可能会完全将它们从窗口中抛出。无论变量在内存中的如何移动，我们都需要一些方法在调试器中跟踪和操作它们。这篇文章将会教你如何处理调试器中的变量，并使用 `libelfin` 演示一个简单的实现。

### 系列文章索引

1. [准备环境](/article-8626-1.html)
2. [断点](/article-8645-1.html)
3. [寄存器和内存](/article-8663-1.html)
4. [ELF 和 DWARF](/article-8719-1.html)
5. [源码和信号](/article-8812-1.html)
6. [源码级逐步执行](/article-8813-1.html)
7. [源码级断点](/article-8890-1.html)
8. [堆栈展开](/article-8930-1.html)
9. [处理变量](https://blog.tartanllama.xyz/writing-a-linux-debugger-variables/)
10. [高级话题](https://blog.tartanllama.xyz/writing-a-linux-debugger-advanced-topics/)

在开始之前，请确保你使用的 `libelfin` 版本是[我分支上的 `fbreg`](https://github.com/TartanLlama/libelfin/tree/fbreg)。这包含了一些 hack 来支持获取当前堆栈帧的基址并评估位置列表，这些都不是由原生的 `libelfin` 提供的。你可能需要给 GCC 传递 `-gdwarf-2` 参数使其生成兼容的 DWARF 信息。但是在实现之前，我将详细说明 DWARF 5 最新规范中的位置编码方式。如果你想要了解更多信息，那么你可以从[这里](http://dwarfstd.org/)获取该标准。

### DWARF 位置

某一给定时刻的内存中变量的位置使用 `DW_AT_location` 属性编码在 DWARF 信息中。位置描述可以是单个位置描述、复合位置描述或位置列表。

* 简单位置描述：描述了对象的一个连续的部分（通常是所有部分）的位置。简单位置描述可以描述可寻址存储器或寄存器中的位置，或缺少位置（具有或不具有已知值）。比如，`DW_OP_fbreg -32`： 一个整个存储的变量 - 从堆栈帧基址开始的32个字节。
* 复合位置描述：根据片段描述对象，每个对象可以包含在寄存器的一部分中或存储在与其他片段无关的存储器位置中。比如， `DW_OP_reg3 DW_OP_piece 4 DW_OP_reg10 DW_OP_piece 2`：前四个字节位于寄存器 3 中，后两个字节位于寄存器 10 中的一个变量。
* 位置列表：描述了具有有限生存期或在生存期内更改位置的对象。比如：
	+ `<loclist with 3 entries follows>`
		- `[ 0]<lowpc=0x2e00><highpc=0x2e19>DW_OP_reg0`
		- `[ 1]<lowpc=0x2e19><highpc=0x2e3f>DW_OP_reg3`
		- `[ 2]<lowpc=0x2ec4><highpc=0x2ec7>DW_OP_reg2`
	+ 根据程序计数器的当前值，位置在寄存器之间移动的变量。

根据位置描述的种类，`DW_AT_location` 以三种不同的方式进行编码。`exprloc` 编码简单和复合的位置描述。它们由一个字节长度组成，后跟一个 DWARF 表达式或位置描述。`loclist` 和 `loclistptr` 的编码位置列表，它们在 `.debug_loclists` 部分中提供索引或偏移量，该部分描述了实际的位置列表。

### DWARF 表达式

使用 DWARF 表达式计算变量的实际位置。这包括操作堆栈值的一系列操作。有很多 DWARF 操作可用，所以我不会详细解释它们。相反，我会从每一个表达式中给出一些例子，给你一个可用的东西。另外，不要害怕这些；`libelfin` 将为我们处理所有这些复杂性。

* 字面编码
	+ `DW_OP_lit0`、`DW_OP_lit1`……`DW_OP_lit31`
		- 将字面量压入堆栈
	+ `DW_OP_addr <addr>`
		- 将地址操作数压入堆栈
	+ `DW_OP_constu <unsigned>`
		- 将无符号值压入堆栈
* 寄存器值
	+ `DW_OP_fbreg <offset>`
		- 压入在堆栈帧基址找到的值，偏移给定值
	+ `DW_OP_breg0`、`DW_OP_breg1`…… `DW_OP_breg31 <offset>`
		- 将给定寄存器的内容加上给定的偏移量压入堆栈
* 堆栈操作
	+ `DW_OP_dup`
		- 复制堆栈顶部的值
	+ `DW_OP_deref`
		- 将堆栈顶部视为内存地址，并将其替换为该地址的内容
* 算术和逻辑运算
	+ `DW_OP_and`
		- 弹出堆栈顶部的两个值，并压回它们的逻辑 `AND`
	+ `DW_OP_plus`
		- 与 `DW_OP_and` 相同，但是会添加值
* 控制流操作
	+ `DW_OP_le`、`DW_OP_eq`、`DW_OP_gt` 等
		- 弹出前两个值，比较它们，并且如果条件为真，则压入 `1`，否则为 `0`
	+ `DW_OP_bra <offset>`
		- 条件分支：如果堆栈的顶部不是 `0`，则通过 `offset` 在表达式中向后或向后跳过
* 输入转化
	+ `DW_OP_convert <DIE offset>`
		- 将堆栈顶部的值转换为不同的类型，它由给定偏移量的 DWARF 信息条目描述
* 特殊操作
	+ `DW_OP_nop`
		- 什么都不做！

### DWARF 类型

DWARF 类型的表示需要足够强大来为调试器用户提供有用的变量表示。用户经常希望能够在应用程序级别进行调试，而不是在机器级别进行调试，并且他们需要了解他们的变量正在做什么。

DWARF 类型与大多数其他调试信息一起编码在 DIE 中。它们可以具有指示其名称、编码、大小、字节等的属性。无数的类型标签可用于表示指针、数组、结构体、typedef 以及 C 或 C++ 程序中可以看到的任何其他内容。

以这个简单的结构体为例：

```
struct test{
    int i;
    float j;
    int k[42];
    test* next;
};

```

这个结构体的父 DIE 是这样的：

```
< 1><0x0000002a>    DW_TAG_structure_type
                      DW_AT_name                  "test"
                      DW_AT_byte_size             0x000000b8
                      DW_AT_decl_file             0x00000001 test.cpp
                      DW_AT_decl_line             0x00000001

```

上面说的是我们有一个叫做 `test` 的结构体，大小为 `0xb8`，在 `test.cpp` 的第 `1` 行声明。接下来有许多描述成员的子 DIE。

```
< 2><0x00000032>      DW_TAG_member
                        DW_AT_name                  "i"
                        DW_AT_type                  <0x00000063>
                        DW_AT_decl_file             0x00000001 test.cpp
                        DW_AT_decl_line             0x00000002
                        DW_AT_data_member_location  0
< 2><0x0000003e>      DW_TAG_member
                        DW_AT_name                  "j"
                        DW_AT_type                  <0x0000006a>
                        DW_AT_decl_file             0x00000001 test.cpp
                        DW_AT_decl_line             0x00000003
                        DW_AT_data_member_location  4
< 2><0x0000004a>      DW_TAG_member
                        DW_AT_name                  "k"
                        DW_AT_type                  <0x00000071>
                        DW_AT_decl_file             0x00000001 test.cpp
                        DW_AT_decl_line             0x00000004
                        DW_AT_data_member_location  8
< 2><0x00000056>      DW_TAG_member
                        DW_AT_name                  "next"
                        DW_AT_type                  <0x00000084>
                        DW_AT_decl_file             0x00000001 test.cpp
                        DW_AT_decl_line             0x00000005
                        DW_AT_data_member_location  176(as signed = -80)

```

每个成员都有一个名称、一个类型（它是一个 DIE 偏移量）、一个声明文件和行，以及一个指向其成员所在的结构体的字节偏移。其类型指向如下。

```
< 1><0x00000063>    DW_TAG_base_type
                      DW_AT_name                  "int"
                      DW_AT_encoding              DW_ATE_signed
                      DW_AT_byte_size             0x00000004
< 1><0x0000006a>    DW_TAG_base_type
                      DW_AT_name                  "float"
                      DW_AT_encoding              DW_ATE_float
                      DW_AT_byte_size             0x00000004
< 1><0x00000071>    DW_TAG_array_type
                      DW_AT_type                  <0x00000063>
< 2><0x00000076>      DW_TAG_subrange_type
                        DW_AT_type                  <0x0000007d>
                        DW_AT_count                 0x0000002a
< 1><0x0000007d>    DW_TAG_base_type
                      DW_AT_name                  "sizetype"
                      DW_AT_byte_size             0x00000008
                      DW_AT_encoding              DW_ATE_unsigned
< 1><0x00000084>    DW_TAG_pointer_type
                      DW_AT_type                  <0x0000002a>

```

如你所见，我笔记本电脑上的 `int` 是一个 4 字节的有符号整数类型，`float`是一个 4 字节的浮点数。整数数组类型通过指向 `int` 类型作为其元素类型，`sizetype`（可以认为是 `size_t`）作为索引类型，它具有 `2a` 个元素。 `test *` 类型是 `DW_TAG_pointer_type`，它引用 `test` DIE。

### 实现简单的变量读取器

如上所述，`libelfin` 将为我们处理大部分复杂性。但是，它并没有实现用于表示可变位置的所有方法，并且在我们的代码中处理这些将变得非常复杂。因此，我现在选择只支持 `exprloc`。请根据需要添加对更多类型表达式的支持。如果你真的有勇气，请提交补丁到 `libelfin` 中来帮助完成必要的支持！

处理变量主要是将不同部分定位在存储器或寄存器中，读取或写入与之前一样。为了简单起见，我只会告诉你如何实现读取。

首先我们需要告诉 `libelfin` 如何从我们的进程中读取寄存器。我们创建一个继承自 `expr_context` 的类并使用 `ptrace` 来处理所有内容：

```
class ptrace_expr_context : public dwarf::expr_context {
public:
    ptrace_expr_context (pid_t pid) : m_pid{pid} {}

    dwarf::taddr reg (unsigned regnum) override {
        return get_register_value_from_dwarf_register(m_pid, regnum);
    }

    dwarf::taddr pc() override {
        struct user_regs_struct regs;
        ptrace(PTRACE_GETREGS, m_pid, nullptr, &regs);
        return regs.rip;
    }

    dwarf::taddr deref_size (dwarf::taddr address, unsigned size) override {
        //TODO take into account size
        return ptrace(PTRACE_PEEKDATA, m_pid, address, nullptr);
    }

private:
    pid_t m_pid;
};

```

读取将由我们 `debugger` 类中的 `read_variables` 函数处理：

```
void debugger::read_variables() {
    using namespace dwarf;

    auto func = get_function_from_pc(get_pc());

    //...
}

```

我们上面做的第一件事是找到我们目前进入的函数，然后我们需要循环访问该函数中的条目来寻找变量：

```
    for (const auto& die : func) {
        if (die.tag == DW_TAG::variable) {
            //...
        }
    }

```

我们通过查找 DIE 中的 `DW_AT_location` 条目获取位置信息：

```
            auto loc_val = die[DW_AT::location];

```

接着我们确保它是一个 `exprloc`，并请求 `libelfin` 来评估我们的表达式：

```
            if (loc_val.get_type() == value::type::exprloc) {
                ptrace_expr_context context {m_pid};
                auto result = loc_val.as_exprloc().evaluate(&context);

```

现在我们已经评估了表达式，我们需要读取变量的内容。它可以在内存或寄存器中，因此我们将处理这两种情况：

```
                switch (result.location_type) {
                case expr_result::type::address:
                {
                    auto value = read_memory(result.value);
                    std::cout << at_name(die) << " (0x" << std::hex << result.value << ") = "
                              << value << std::endl;
                    break;
                }

                case expr_result::type::reg:
                {
                    auto value = get_register_value_from_dwarf_register(m_pid, result.value);
                    std::cout << at_name(die) << " (reg " << result.value << ") = "
                              << value << std::endl;
                    break;
                }

                default:
                    throw std::runtime_error{"Unhandled variable location"};
                }

```

你可以看到，我根据变量的类型，打印输出了值而没有解释。希望通过这个代码，你可以看到如何支持编写变量，或者用给定的名字搜索变量。

最后我们可以将它添加到我们的命令解析器中：

```
    else if(is_prefix(command, "variables")) {
        read_variables();
    }

```

### 测试一下

编写一些具有一些变量的小功能，不用优化并带有调试信息编译它，然后查看是否可以读取变量的值。尝试写入存储变量的内存地址，并查看程序改变的行为。

已经有九篇文章了，还剩最后一篇！下一次我会讨论一些你可能会感兴趣的更高级的概念。现在你可以在[这里](https://github.com/TartanLlama/minidbg/tree/tut_variable)找到这个帖子的代码。

---

via: <https://blog.tartanllama.xyz/writing-a-linux-debugger-variables/>

作者：[Simon Brand](https://www.twitter.com/TartanLlama) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


OpenGL 与 Go 教程（二）绘制游戏面板

makeCells()

第一行应该是

cells := make([][]*cell, rows, columns)

建议修改下，虽然结果一样的，这样好理解些

![](/data/attachment/album/201710/07/223453r5tqot1qsp0s6ga6.jpg)

* [第一节: Hello, OpenGL](/article-8933-1.html)
* [第二节: 绘制游戏面板](/article-8937-1.html)
* [第三节：实现游戏功能](/article-8969-1.html)

这篇教程的所有源代码都可以在 [GitHub](https://github.com/KyleBanks/conways-gol) 上找到。

欢迎回到《OpenGL 与 Go 教程》。如果你还没有看过[第一节](/article-8933-1.html)，那就要回过头去看看那一节。

你现在应该能够创造一个漂亮的白色三角形，但我们不会把三角形当成我们游戏的基本单元，是时候把三角形变成正方形了，然后我们会做出一个完整的方格。

让我们现在开始做吧！

### 利用三角形绘制方形

在我们绘制方形之前，先把三角形变成直角三角形。打开 `main.go` 文件，把 `triangle` 的定义改成像这个样子：

```
triangle = []float32{
    -0.5, 0.5, 0,
    -0.5, -0.5, 0,
    0.5, -0.5, 0,
}

```

我们做的事情是，把最上面的顶点 X 坐标移动到左边（也就是变为 `-0.5`），这就变成了像这样的三角形：

![Conway's Game of Life  - 右弦三角形](/data/attachment/album/201710/07/223511c84rhv985frva4vv.png)

很简单，对吧？现在让我们用两个这样的三角形顶点做成正方形。把 `triangle` 重命名为 `square`，然后添加第二个倒置的三角形的顶点数据，把直角三角形变成这样的：

```
square = []float32{
    -0.5, 0.5, 0,
    -0.5, -0.5, 0,
    0.5, -0.5, 0,

    -0.5, 0.5, 0,
    0.5, 0.5, 0,
    0.5, -0.5, 0,
}

```

注意：你也要把在 `main` 和 `draw` 里面命名的 `triangle` 改为 `square`。

我们通过添加三个顶点，把顶点数增加了一倍，这三个顶点就是右上角的三角形，用来拼成方形。运行它看看效果：

![Conway's Game of Life - 两个三角形构成方形](/data/attachment/album/201710/07/223511wghwhbgk6j1csvvh.png)

很好，现在我们能够绘制正方形了！OpenGL 一点都不难，对吧？

### 在窗口中绘制方形格子

现在我们能画一个方形，怎么画 100 个吗？我们来创建一个 `cell` 结构体，用来表示格子的每一个单元，因此我们能够很灵活的选择绘制的数量：

```
type cell struct {
    drawable uint32

    x int
    y int
}

```

`cell` 结构体包含一个 `drawable` 属性，这是一个顶点数组对象，就像我们在之前创建的一样，这个结构体还包含 X 和 Y 坐标，用来表示这个格子的位置。

我们还需要两个常量，用来设定格子的大小和形状：

```
const (
    ...

    rows = 10
    columns = 10
)

```

现在我们添加一个创建格子的函数：

```
func makeCells() [][]*cell {
    cells := make([][]*cell, rows, rows)
    for x := 0; x < rows; x++ {
        for y := 0; y < columns; y++ {
            c := newCell(x, y)
            cells[x] = append(cells[x], c)
        }
    }

    return cells
}

```

这里我们创建多维的<ruby> 切片 <rt>  slice </rt></ruby>，代表我们的游戏面板，用名为 `newCell` 的新函数创建的 `cell` 来填充矩阵的每个元素，我们待会就来实现 `newCell` 这个函数。

在接着往下阅读前，我们先花一点时间来看看 `makeCells` 函数做了些什么。我们创造了一个切片，这个切片的长度和格子的行数相等，每一个切片里面都有一个<ruby> 细胞 <rt>  cell </rt></ruby>的切片，这些细胞的数量与列数相等。如果我们把 `rows` 和 `columns` 都设定成 2，那么就会创建如下的矩阵：

```
[
    [cell, cell],
    [cell, cell]
]

```

还可以创建一个更大的矩阵，包含 `10x10` 个细胞：

```
[
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell]
]

```

现在应该理解了我们创造的矩阵的形状和表示方法。让我们看看 `newCell` 函数到底是怎么填充矩阵的：

```
func newCell(x, y int) *cell {
    points := make([]float32, len(square), len(square))
    copy(points, square)

    for i := 0; i < len(points); i++ {
        var position float32
        var size float32
        switch i % 3 {
        case 0:
                size = 1.0 / float32(columns)
                position = float32(x) * size
        case 1:
                size = 1.0 / float32(rows)
                position = float32(y) * size
        default:
                continue
        }

        if points[i] < 0 {
                points[i] = (position * 2) - 1
        } else {
                points[i] = ((position + size) * 2) - 1
        }
    }

    return &cell{
        drawable: makeVao(points),

        x: x,
        y: y,
    }
}

```

这个函数里有很多内容，我们把它分成几个部分。我们做的第一件事是复制了 `square` 的定义。这让我们能够修改该定义，定制当前的细胞位置，而不会影响其它使用 `square` 切片定义的细胞。然后我们基于当前索引迭代 `points` 副本。我们用求余数的方法来判断我们是在操作 X 坐标（`i % 3 == 0`），还是在操作 Y 坐标（`i % 3 == 1`）（跳过 Z 坐标是因为我们仅在二维层面上进行操作），跟着确定细胞的大小（也就是占据整个游戏面板的比例），当然它的位置是基于细胞在 `相对游戏面板的` X 和 Y 坐标。

接着，我们改变那些包含在 `square` 切片中定义的 `0.5`，`0`， `-0.5` 这样的点。如果点小于 0，我们就把它设置成原来的 2 倍（因为 OpenGL 坐标的范围在 `-1` 到 `1` 之间，范围大小是 2），减 1 是为了归一化 OpenGL 坐标。如果点大于等于 0，我们的做法还是一样的，不过要加上我们计算出的尺寸。

这样做是为了设置每个细胞的大小，这样它就能只填充它在面板中的部分。因为我们有 10 行 10 列，每一个格子能分到游戏面板的 10% 宽度和高度。

最后，确定了所有点的位置和大小，我们用提供的 X 和 Y 坐标创建一个 `cell`，并设置 `drawable` 字段与我们刚刚操作 `points` 得到的顶点数组对象（vao）一致。

好了，现在我们在 `main` 函数里可以移去对 `makeVao` 的调用了，用 `makeCells` 代替。我们还修改了 `draw`，让它绘制一系列的细胞而不是一个 `vao`。

```
func main() {
    ...

    // vao := makeVao(square)
    cells := makeCells()

    for !window.ShouldClose() {
        draw(cells, window, program)
    }
}

func draw(cells [][]*cell, window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(program)

    // TODO

    glfw.PollEvents()
    window.SwapBuffers()
}

```

现在我们要让每个细胞知道怎么绘制出自己。在 `cell` 里面添加一个 `draw` 函数：

```
func (c *cell) draw() {
    gl.BindVertexArray(c.drawable)
    gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square) / 3))
}

```

这看上去很熟悉，它很像我们之前在 `vao` 里写的 `draw`，唯一的区别是我们的 `BindVertexArray` 函数用的是 `c.drawable`，这是我们在 `newCell` 中创造的细胞的 `vao`。

回到 main 中的 `draw` 函数上，我们可以循环每个细胞，让它们自己绘制自己：

```
func draw(cells [][]*cell, window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(program)

    for x := range cells {
        for _, c := range cells[x] {
            c.draw()
        }
    }

    glfw.PollEvents()
    window.SwapBuffers()
}

```

如你所见，我们循环每一个细胞，调用它的 `draw` 函数。如果运行这段代码，你能看到像下面这样的东西：

![Conway's Game of Life - 全部格子](/data/attachment/album/201710/07/223512fukkduvyxvxvj5d5.png)

这是你想看到的吗？我们做的是在格子里为每一行每一列创建了一个方块，然后给它上色，这就填满了整个面板！

注释掉 for 循环，我们就可以看到一个明显独立的细胞，像这样：

```
// for x := range cells {
//     for _, c := range cells[x] {
//         c.draw()
//     }
// }

cells[2][3].draw()

```

![Conway's Game of Life - 一个单独的细胞](/data/attachment/album/201710/07/223512yk5w3el05emzahzi.png)

这只绘制坐标在 `(X=2, Y=3)` 的格子。你可以看到，每一个独立的细胞占据着面板的一小块部分，并且负责绘制自己那部分空间。我们也能看到游戏面板有自己的原点，也就是坐标为 `(X=0, Y=0)` 的点，在窗口的左下方。这仅仅是我们的 `newCell` 函数计算位置的方式，也可以用右上角，右下角，左上角，中央，或者其它任何位置当作原点。

接着往下做，移除 `cells[2][3].draw()` 这一行，取消 for 循环的那部分注释，变成之前那样全部绘制的样子。

### 总结

好了，我们现在能用两个三角形画出一个正方形了，我们还有一个游戏的面板了！我们该为此自豪，目前为止我们已经接触到了很多零碎的内容，老实说，最难的部分还在前面等着我们！

在接下来的第三节，我们会实现游戏核心逻辑，看到很酷的东西！

### 回顾

这是这一部分教程中 `main.go` 文件的内容：

```
package main

import (
    "fmt"
    "log"
    "runtime"
    "strings"

    "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
    "github.com/go-gl/glfw/v3.2/glfw"
)

const (
    width  = 500
    height = 500

    vertexShaderSource = `
        #version 410
        in vec3 vp;
        void main() {
            gl_Position = vec4(vp, 1.0);
        }
    ` + "\x00"

    fragmentShaderSource = `
        #version 410
        out vec4 frag_colour;
        void main() {
            frag_colour = vec4(1, 1, 1, 1.0);
        }
    ` + "\x00"

    rows    = 10
    columns = 10
)

var (
    square = []float32{
        -0.5, 0.5, 0,
        -0.5, -0.5, 0,
        0.5, -0.5, 0,

        -0.5, 0.5, 0,
        0.5, 0.5, 0,
        0.5, -0.5, 0,
    }
)

type cell struct {
    drawable uint32

    x int
    y int
}

func main() {
    runtime.LockOSThread()

    window := initGlfw()
    defer glfw.Terminate()
    program := initOpenGL()

    cells := makeCells()
    for !window.ShouldClose() {
        draw(cells, window, program)
    }
}

func draw(cells [][]*cell, window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(program)

    for x := range cells {
        for _, c := range cells[x] {
            c.draw()
        }
    }

    glfw.PollEvents()
    window.SwapBuffers()
}

func makeCells() [][]*cell {
    cells := make([][]*cell, rows, rows)
    for x := 0; x < rows; x++ {
        for y := 0; y < columns; y++ {
            c := newCell(x, y)
            cells[x] = append(cells[x], c)
        }
    }

    return cells
}

func newCell(x, y int) *cell {
    points := make([]float32, len(square), len(square))
    copy(points, square)

    for i := 0; i < len(points); i++ {
        var position float32
        var size float32
        switch i % 3 {
        case 0:
            size = 1.0 / float32(columns)
            position = float32(x) * size
        case 1:
            size = 1.0 / float32(rows)
            position = float32(y) * size
        default:
            continue
        }

        if points[i] < 0 {
            points[i] = (position * 2) - 1
        } else {
            points[i] = ((position + size) * 2) - 1
        }
    }

    return &cell{
        drawable: makeVao(points),

        x: x,
        y: y,
    }
}

func (c *cell) draw() {
    gl.BindVertexArray(c.drawable)
    gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square)/3))
}

// 初始化 glfw，返回一个可用的 Window
func initGlfw() *glfw.Window {
    if err := glfw.Init(); err != nil {
        panic(err)
    }
    glfw.WindowHint(glfw.Resizable, glfw.False)
    glfw.WindowHint(glfw.ContextVersionMajor, 4)
    glfw.WindowHint(glfw.ContextVersionMinor, 1)
    glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
    glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)

    window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
    if err != nil {
        panic(err)
    }
    window.MakeContextCurrent()

    return window
}

// 初始化 OpenGL 并返回一个可用的着色器程序
func initOpenGL() uint32 {
    if err := gl.Init(); err != nil {
        panic(err)
    }
    version := gl.GoStr(gl.GetString(gl.VERSION))
    log.Println("OpenGL version", version)

    vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
    if err != nil {
        panic(err)
    }

    fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
    if err != nil {
        panic(err)
    }

    prog := gl.CreateProgram()
    gl.AttachShader(prog, vertexShader)
    gl.AttachShader(prog, fragmentShader)
    gl.LinkProgram(prog)
    return prog
}

// 初始化并返回由 points 提供的顶点数组
func makeVao(points []float32) uint32 {
    var vbo uint32
    gl.GenBuffers(1, &vbo)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)

    var vao uint32
    gl.GenVertexArrays(1, &vao)
    gl.BindVertexArray(vao)
    gl.EnableVertexAttribArray(0)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)

    return vao
}

func compileShader(source string, shaderType uint32) (uint32, error) {
    shader := gl.CreateShader(shaderType)

    csources, free := gl.Strs(source)
    gl.ShaderSource(shader, 1, csources, nil)
    free()
    gl.CompileShader(shader)

    var status int32
    gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
    if status == gl.FALSE {
        var logLength int32
        gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)

        log := strings.Repeat("\x00", int(logLength+1))
        gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))

        return 0, fmt.Errorf("failed to compile %v: %v", source, log)
    }

    return shader, nil
}

```

让我知道这篇文章对你有没有帮助，在 Twitter [@kylewbanks](https://twitter.com/kylewbanks) 或者下方的连接，关注我以便获取最新的文章！

---

via: <https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-2-drawing-the-game-board>

作者：[kylewbanks](https://twitter.com/kylewbanks) 译者：[GitFtuture](https://github.com/GitFuture) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


运行在树莓派和 Arduino 上的开源社交机器人套件

![](/data/attachment/album/201710/08/114802qnyu7knmrfmk5yac.jpg)

>
> Thecorpora 的发布的 “Q.bo One” 机器人基于 RPi 3 和 Arduino，并提供立体相机、麦克风、扬声器，以及视觉和语言识别。
>
>
>

2010 年，作为一个开源概念验证和用于探索 AI 在多传感器、交互式机器人的能力的研究项目，机器人开发商 Francisco Paz 及它在巴塞罗那的 Thecorpora 公司推出了首款 [Qbo](http://linuxdevices.linuxgizmos.com/open-source-robot-is-all-eyes/) “Cue-be-oh” 机器人。在今年 2 月移动世界大会上的预览之后，Thecorpora 把它放到了 Indiegogo 上，与 Arrow 合作推出了第一个批量生产的社交机器人版本。

![](/data/attachment/album/201710/08/114803x81yy78rx157rp5y.jpg)

*Q.bo One 的左侧*

![](/data/attachment/album/201710/08/114803sto4bdnlhuonxrad.jpg)

*Q.bo One 的顶部*

像原来一样，新的 Q.bo One 有一个带眼睛的球形头（双立体相机）、耳朵（3 个麦克风）和嘴（扬声器），并由 WiFi 和蓝牙控制。 Q.bo One 也同样采用开源 Linux 软件和开放规格硬件。然而，它不是使用基于 Intel Atom 的 Mini-ITX 板，而是在与 Arduino 兼容的主板相连的 Raspberry Pi 3 上运行 Raspbian。

![](/data/attachment/album/201710/08/114804uz6ku6kr654hxqzk.jpg)

*Q.bo One 侧视图*\*

Q.bo One 于 7 月中旬在 Indiegogo 上架，起价为 369 美元（早期买家）或 399 美元，有包括内置的树莓派 3 和基于 Arduino 的 “Qboard” 控制器板。它还有售价 $499 的完整套装。目前，Indiegogo 上的众筹目标是 $100,000，现在大概达成了 15%，并且它 12 月出货。

更专业的机器人工程师和嵌入式开发人员可能会想要使用价值 $99 的只有树莓派和 Qboard PCB 和软件的版本，或者提供没有电路板的机器人套件的 $249 版本。使用此版本，你可以用自己的 Arduino 控制器替换 Qboard，并将树莓派 3 替换为另一个 Linux SBC。该公司列出了 Banana Pi、BeagleBone、Tinker Board 以及[即将退市的 Intel Edison](http://linuxgizmos.com/intel-pulls-the-plug-on-its-joule-edison-and-galileo-boards/)，作为兼容替代品的示例。

![](/data/attachment/album/201710/08/114805drpppilsll0z567i.jpg)

*Q.bo One 套件*

与 2010 年的 Qbo 不同，Q.bo One 除了球面头部之外无法移动，它可以在双重伺服系统的帮助下在底座上旋转，以便跟踪声音和动作。Robotis Dynamixel 舵机同样开源，树莓派基于 [TurtleBot 3](http://linuxgizmos.com/ubuntu-driven-turtlebot-gets-a-major-rev-with-a-pi-or-joule-in-the-drivers-seat/) 机器人工具包，除了左右之外，还可以上下移动。

![](/data/attachment/album/201710/08/114805hyi1klkkzh1l41pl.jpg)

*Q.bo One 细节*

![](/data/attachment/album/201710/08/114806u9lsndfssuedss2n.jpg)

*Qboard 细节*

Q.bo One 类似于基于 Linux 的 [Jibo](http://linuxgizmos.com/cheery-social-robot-owes-it-all-to-its-inner-linux/) “社交机器人”，它于 2014 年在 Indiegogo 众筹，最后达到 360 万美元。然而，Jibo 还没有出货，[最近的推迟](https://www.slashgear.com/jibo-delayed-to-2017-as-social-robot-hits-more-hurdles-20464725/)迫使它在今年的某个时候发布一个版本。

![](/data/attachment/album/201710/08/114807xpbuoby60znucgau.jpg)

*Q.bo One*

我们大胆预测 Q.bo One 将会在 2017 年接近 12 月出货。核心技术和 AI 软件已被证明，而树莓派和 Arduino 技术也是如此。Qboard 主板已经由 Arrow 制造和认证。

开源设计表明，即使是移动版本也不会有问题。这使它更像是滚动的人形生物 [Pepper](http://linuxgizmos.com/worlds-first-emotional-robot-runs-linux/)，这是一个来自 Softbank 和 Aldeberan 类似的人工智能对话机器人。

Q.bo One 自原始版以来添加了一些技巧，例如由 20 个 LED 组成的“嘴巴”， 它以不同的、可编程的方式在语音中模仿嘴唇开合。如果你想点击机器人获得关注，那么它的头上还有三个触摸传感器。但是，你其实只需要说话就行，而 Q.bo One 会像一个可卡犬一样转身并凝视着你。

接口和你在树莓派 3 上的一样，它在我们的 [2017 黑客电路板调查](http://linuxgizmos.com/2017-hacker-board-survey-raspberry-pi-still-rules-but-x86-sbcs-make-gains/)中消灭了其他对手。为树莓派 3 的 WiFi 和蓝牙安装了天线。

![](/data/attachment/album/201710/08/114808ua1iqicqbangwdm1.jpg)

*Q.bo One 软件架构*

![](/data/attachment/album/201710/08/114802qnyu7knmrfmk5yac.jpg)

*Q.bo One 及 Scratch 编程*

Qboard（也称为 Q.board）在 Atmel ATSAMD21 MCU 上运行 Arduino 代码，并有三个麦克风、扬声器、触摸传感器、Dynamixel 控制器和用于嘴巴的 LED 矩阵。其他功能包括 GPIO、I2C接口和可连接到台式机的 micro-USB 口。

Q.bo One 可以识别脸部和追踪移动，机器人甚至可以在镜子中识别自己。在云连接的帮助下，机器人可以识别并与其他 Q.bo One 机器人交谈。机器人可以在自然语言处理的帮助下回答问题，并通过文字转语音朗读。

可以使用 Scratch 编程，它是机器人的主要功能，可以教孩子关于机器人和编程。机器人也是为教育者和制造者设计的，可以作为老年人的伴侣。

基于 Raspbian 的软件使用 OpenCV 进行视觉处理，并可以使用各种语言（包括 C++）进行编程。该软件还提供了 IBM Bluemix、NodeRED 和 ROS 的钩子。大概你也可以整合 [Alexa](http://linuxgizmos.com/how-to-add-alexa-to-your-raspberry-pi-3-gizmo/) 或 [Google Assistant](http://linuxgizmos.com/free-raspberry-pi-voice-kit-taps-google-assistant-sdk/)语音代理，虽然 Thecorpora 没有提及这一点。

**更多信息**

Q.bo One 在 7 月中旬在 Indiegogo 上架，起价为完整套件 $369 和完整组合 $499 。出货预计在 2017 年 12 月。更多信息请参见 [Q.bo One 的 Indiegogo 页面](https://www.indiegogo.com/projects/q-bo-one-an-open-source-robot-for-everyone#/) 和[Thecorpora 网站](http://thecorpora.com/)。

---

via: <http://linuxgizmos.com/open-source-social-robot-kit-runs-on-raspberry-pi-and-arduino/>

作者：[Eric Brown](http://linuxgizmos.com/open-source-social-robot-kit-runs-on-raspberry-pi-and-arduino/) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出