来学习下 Go 语言的安全检查工具 gosec。Go 语言写的代码越来越常见，尤其是在容器、Kubernetes 或云生态相关的开发中。Docker 是最早采用 Golang 的项目之一，随后是 Kubernetes，之后大量的新项目在众多编程语言中选择了 Go。像其他语言一样，Go 也有它的长处和短处（如安全缺陷）。这些缺陷可能会因为语言本身的缺陷加上程序员编码不当而产生，例如，C 代码中的内存安全问题。无论它们出现的原因是什么，安全问题都应该在开发过程的早期修复，以免在封装好的软件中出现。幸运的是，静态分析工具可以帮你以更可重复的方式处理这些问题。静态分析工具通过解析用某种编程语言写的代码来找到问题。这类工具中很多被称为 linter。传统意义上，linter 更注重的是检查代码中编码问题、bug、代码风格之类的问题，它们可能不会发现代码中的安全问题。例如，Coverity 是一个很流行的工具，它可以帮助寻找 C/C++ 代码中的问题。然而，也有一些工具专门用来检查源码中的安全问题。例如，Bandit 可以检查 Python 代码中的安全缺陷。而 gosec 则用来搜寻 Go 源码中的安全缺陷。gosec 通过扫描 Go 的 AST（ 抽象语法树 abstract syntax tree ）来检查源码中的安全问题。开始使用 gosec在开始学习和使用 gosec 之前，你需要准备一个 Go 语言写的项目。有这么多开源软件，我相信这不是问题。你可以在 GitHub 的 热门 Golang 仓库中找一个。本文中，我随机选了 Docker CE 项目，但你可以选择任意的 Go 项目。安装 Go 和 gosec如果你还没安装 Go，你可以先从仓库中拉取下来。如果你用的是 Fedora 或其他基于 RPM 的 Linux 发行版本：$ dnf install golang.x86_64 如果你用的是其他操作系统，请参照 Golang 安装页面。使用 version 参数来验证 Go 是否安装成功：$ go version go version go1.14.6 linux/amd64 运行 go get 命令就可以轻松地安装 gosec：$ go get github.com/securego/gosec/cmd/gosec 上面这行命令会从 GitHub 下载 gosec 的源码，编译并安装到指定位置。在仓库的 README 中你还可以看到安装该工具的其他方法。gosec 的源码会被下载到 $GOPATH 的位置，编译出的二进制文件会被安装到你系统上设置的 bin 目录下。你可以运行下面的命令来查看 $GOPATH 和 $GOBIN 目录：$ go env | grep GOBIN GOBIN="/root/go/gobin" $ go env | grep GOPATH GOPATH="/root/go" 如果 go get 命令执行成功，那么 gosec 二进制应该就可以使用了：$ ls -l ~/go/bin/ total 9260 -rwxr-xr-x. 1 root root 9482175 Aug 20 04:17 gosec 你可以把 $GOPATH 下的 bin 目录添加到 $PATH 中。这样你就可以像使用系统上的其他命令一样来使用 gosec 命令行工具（CLI）了。$ which gosec /root/go/bin/gosec $ 使用 gosec 命令行工具的 -help 选项来看看运行是否符合预期：$ gosec -help gosec - Golang security checker gosec analyzes Go source code to look for common programming mistakes that can lead to security problems. VERSION: dev GIT TAG: BUILD DATE: USAGE: 之后，创建一个目录，把源码下载到这个目录作为实例项目（本例中，我用的是 Docker CE）：$ mkdir gosec-demo $ cd gosec-demo/ $ pwd /root/gosec-demo $ git clone https://github.com/docker/docker-ce.git Cloning into 'docker-ce'... remote: Enumerating objects: 1271, done. remote: Counting objects: 100% (1271/1271), done. remote: Compressing objects: 100% (722/722), done. remote: Total 431003 (delta 384), reused 981 (delta 318), pack-reused 429732 Receiving objects: 100% (431003/431003), 166.84 MiB | 28.94 MiB/s, done. Resolving deltas: 100% (221338/221338), done. Updating files: 100% (10861/10861), done. 代码统计工具（本例中用的是 cloc）显示这个项目大部分是用 Go 写的，恰好迎合了 gosec 的功能。$ ./cloc /root/gosec-demo/docker-ce/ 10771 text files. 8724 unique files. 2560 files ignored. ----------------------------------------------------------------------------------- Language files blank comment code ----------------------------------------------------------------------------------- Go 7222 190785 230478 1574580 YAML 37 4831 817 156762 Markdown 529 21422 0 67893 Protocol Buffers 149 5014 16562 10071 使用默认选项运行 gosec在 Docker CE 项目中使用默认选项运行 gosec，执行 gosec ./... 命令。屏幕上会有很多输出内容。在末尾你会看到一个简短的 “Summary”，列出了浏览的文件数、所有文件的总行数，以及源码中发现的问题数。$ pwd /root/gosec-demo/docker-ce $ time gosec ./... [gosec] 2020/08/20 04:44:15 Including rules: default [gosec] 2020/08/20 04:44:15 Excluding rules: default [gosec] 2020/08/20 04:44:15 Import directory: /root/gosec-demo/docker-ce/components/engine/opts [gosec] 2020/08/20 04:44:17 Checking package: opts [gosec] 2020/08/20 04:44:17 Checking file: /root/gosec-demo/docker-ce/components/engine/opts/address_pools.go [gosec] 2020/08/20 04:44:17 Checking file: /root/gosec-demo/docker-ce/components/engine/opts/env.go [gosec] 2020/08/20 04:44:17 Checking file: /root/gosec-demo/docker-ce/components/engine/opts/hosts.go # End of gosec run Summary: Files: 1278 Lines: 173979 Nosec: 4 Issues: 644 real 0m52.019s user 0m37.284s sys 0m12.734s $ 滚动屏幕你会看到不同颜色高亮的行：红色表示需要尽快查看的高优先级问题，黄色表示中优先级的问题。关于误判在开始检查代码之前，我想先分享几条基本原则。默认情况下，静态检查工具会基于一系列的规则对测试代码进行分析，并报告出它们发现的所有问题。这是否意味着工具报出来的每一个问题都需要修复？非也。这个问题最好的解答者是设计和开发这个软件的人。他们最熟悉代码，更重要的是，他们了解软件会在什么环境下部署以及会被怎样使用。这个知识点对于判定工具标记出来的某段代码到底是不是安全缺陷至关重要。随着工作时间和经验的积累，你会慢慢学会怎样让静态分析工具忽略非安全缺陷，使报告内容的可执行性更高。因此，要判定 gosec 报出来的某个问题是否需要修复，让一名有经验的开发者对源码做人工审计会是比较好的办法。高优先级问题从输出内容看，gosec 发现了 Docker CE 的一个高优先级问题，它使用的是低版本的 TLS（ 传输层安全 Transport Layer Security ）。无论什么时候，使用软件和库的最新版本都是确保它更新及时、没有安全问题的最好的方法。[/root/gosec-demo/docker-ce/components/engine/daemon/logger/splunk/splunk.go:173] - G402 (CWE-295): TLS MinVersion too low. (Confidence: HIGH, Severity: HIGH) 172: > 173: tlsConfig := &tls.Config{} 174: 它还发现了一个弱随机数生成器。它是不是一个安全缺陷，取决于生成的随机数的使用方式。[/root/gosec-demo/docker-ce/components/engine/pkg/namesgenerator/names-generator.go:843] - G404 (CWE-338): Use of weak random number generator (math/rand instead of crypto/rand) (Confidence: MEDIUM, Severity: HIGH) 842: begin: > 843: name := fmt.Sprintf("%s_%s", left[rand.Intn(len(left))], right[rand.Intn(len(right))]) 844: if name == "boring_wozniak" /* Steve Wozniak is not boring */ { 中优先级问题这个工具还发现了一些中优先级问题。它标记了一个通过与 tar 相关的解压炸弹这种方式实现的潜在的 DoS 威胁，这种方式可能会被恶意的攻击者利用。[/root/gosec-demo/docker-ce/components/engine/pkg/archive/copy.go:357] - G110 (CWE-409): Potential DoS vulnerability via decompression bomb (Confidence: MEDIUM, Severity: MEDIUM) 356: > 357: if _, err = io.Copy(rebasedTar, srcTar); err != nil { 358: w.CloseWithError(err) 它还发现了一个通过变量访问文件的问题。如果恶意使用者能访问这个变量，那么他们就可以改变变量的值去读其他文件。[/root/gosec-demo/docker-ce/components/cli/cli/context/tlsdata.go:80] - G304 (CWE-22): Potential file inclusion via variable (Confidence: HIGH, Severity: MEDIUM) 79: if caPath != "" { > 80: if ca, err = ioutil.ReadFile(caPath); err != nil { 81: return nil, err 文件和目录通常是操作系统安全的最基础的元素。这里，gosec 报出了一个可能需要你检查目录的权限是否安全的问题。[/root/gosec-demo/docker-ce/components/engine/contrib/apparmor/main.go:41] - G301 (CWE-276): Expect directory permissions to be 0750 or less (Confidence: HIGH, Severity: MEDIUM) 40: // make sure /etc/apparmor.d exists > 41: if err := os.MkdirAll(path.Dir(apparmorProfilePath), 0755); err != nil { 42: log.Fatal(err) 你经常需要在源码中启动命令行工具。Go 使用内建的 exec 库来实现。仔细地分析用来调用这些工具的变量，就能发现安全缺陷。[/root/gosec-demo/docker-ce/components/engine/testutil/fakestorage/fixtures.go:59] - G204 (CWE-78): Subprocess launched with variable (Confidence: HIGH, Severity: MEDIUM) 58: > 59: cmd := exec.Command(goCmd, "build", "-o", filepath.Join(tmp, "httpserver"), "github.com/docker/docker/contrib/httpserver") 60: cmd.Env = append(os.Environ(), []string{ 低优先级问题在这个输出中，gosec 报出了一个 unsafe 调用相关的低优先级问题，这个调用会绕开 Go 提供的内存保护。再仔细分析下你调用 unsafe 的方式，看看是否有被别人利用的可能性。[/root/gosec-demo/docker-ce/components/engine/pkg/archive/changes_linux.go:264] - G103 (CWE-242): Use of unsafe calls should be audited (Confidence: HIGH, Severity: LOW) 263: for len(buf) > 0 { > 264: dirent := (*unix.Dirent)(unsafe.Pointer(&buf[0])) 265: buf = buf[dirent.Reclen:] [/root/gosec-demo/docker-ce/components/engine/pkg/devicemapper/devmapper_wrapper.go:88] - G103 (CWE-242): Use of unsafe calls should be audited (Confidence: HIGH, Severity: LOW) 87: func free(p *C.char) { > 88: C.free(unsafe.Pointer(p)) 89: } 它还标记了源码中未处理的错误。源码中出现的错误你都应该处理。[/root/gosec-demo/docker-ce/components/cli/cli/command/image/build/context.go:172] - G104 (CWE-703): Errors unhandled. (Confidence: HIGH, Severity: LOW) 171: err := tar.Close() > 172: os.RemoveAll(dockerfileDir) 173: return err 自定义 gosec 扫描使用 gosec 的默认选项会带来很多的问题。然而，经过人工审计，随着时间推移你会掌握哪些问题是不需要标记的。你可以自己指定排除和包含哪些测试。我上面提到过，gosec 是基于一系列的规则从 Go 源码中查找问题的。下面是它使用的完整的规则列表：G101：查找硬编码凭证G102：绑定到所有接口G103：审计 unsafe 块的使用G104：审计未检查的错误G106：审计 ssh.InsecureIgnoreHostKey 的使用G107: 提供给 HTTP 请求的 url 作为污点输入G108: /debug/pprof 上自动暴露的剖析端点G109: strconv.Atoi 转换到 int16 或 int32 时潜在的整数溢出G110: 潜在的通过解压炸弹实现的 DoSG201：SQL 查询构造使用格式字符串G202：SQL 查询构造使用字符串连接G203：在 HTML 模板中使用未转义的数据G204：审计命令执行情况G301：创建目录时文件权限分配不合理G302：使用 chmod 时文件权限分配不合理G303：使用可预测的路径创建临时文件G304：通过污点输入提供的文件路径G305：提取 zip/tar 文档时遍历文件G306: 写到新文件时文件权限分配不合理G307: 把返回错误的函数放到 defer 内G401：检测 DES、RC4、MD5 或 SHA1 的使用G402：查找错误的 TLS 连接设置G403：确保最小 RSA 密钥长度为 2048 位G404：不安全的随机数源（rand）G501：导入黑名单列表：crypto/md5G502：导入黑名单列表：crypto/desG503：导入黑名单列表：crypto/rc4G504：导入黑名单列表：net/http/cgiG505：导入黑名单列表：crypto/sha1G601: 在 range 语句中使用隐式的元素别名排除指定的测试你可以自定义 gosec 来避免对已知为安全的问题进行扫描和报告。你可以使用 -exclude 选项和上面的规则编号来忽略指定的问题。例如，如果你不想让 gosec 检查源码中硬编码凭证相关的未处理的错误，那么你可以运行下面的命令来忽略这些错误：$ gosec -exclude=G104 ./... $ gosec -exclude=G104,G101 ./... 有时候你知道某段代码是安全的，但是 gosec 还是会报出问题。然而，你又不想完全排除掉整个检查，因为你想让 gosec 检查新增的代码。通过在你已知为安全的代码块添加 #nosec 标记可以避免 gosec 扫描。这样 gosec 会继续扫描新增代码，而忽略掉 #nosec 标记的代码块。运行指定的检查另一方面，如果你只想检查指定的问题，你可以通过 -include 选项和规则编号来告诉 gosec 运行哪些检查：$ gosec -include=G201,G202 ./... 扫描测试文件Go 语言自带对测试的支持，通过单元测试来检验一个元素是否符合预期。在默认模式下，gosec 会忽略测试文件，你可以使用 -tests 选项把它们包含进来：gosec -tests ./... 修改输出的格式找出问题只是它的一半功能；另一半功能是把它检查到的问题以用户友好同时又方便工具处理的方式报告出来。幸运的是，gosec 可以用不同的方式输出。例如，如果你想看 JSON 格式的报告，那么就使用 -fmt 选项指定 JSON 格式并把结果保存到 results.json 文件中：$ gosec -fmt=json -out=results.json ./... $ ls -l results.json -rw-r--r--. 1 root root 748098 Aug 20 05:06 results.json $ { "severity": "LOW", "confidence": "HIGH", "cwe": { "ID": "242", "URL": "https://cwe.mitre.org/data/definitions/242.html" }, "rule_id": "G103", "details": "Use of unsafe calls should be audited", "file": "/root/gosec-demo/docker-ce/components/engine/daemon/graphdriver/graphtest/graphtest_unix.go", "code": "304: \t// Cast to []byte\n305: \theader := *(*reflect.SliceHeader)(unsafe.Pointer(\u0026buf))\n306: \theader. Len *= 8\n", "line": "305", "column": "36" }, 用 gosec 检查容易被发现的问题静态检查工具不能完全代替人工代码审计。然而，当代码量变大、有众多开发者时，这样的工具往往有助于以可重复的方式找出容易被发现的问题。它对于帮助新开发者识别和在编码时避免引入这些安全缺陷很有用。via: https://opensource.com/article/20/9/gosec作者：Gaurav Kamathe 选题：lujun9972 译者：lxbowlf 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
到达 Golang 大陆：一位资深开发者之旅。2014 年夏天……IBM：“我们需要你弄清楚这个 Docker。”我：“没问题。”IBM：“那就开始吧。”我：“好的。”（内心声音）：”Docker 是用 Go 编写的。是吗？“（Google 一下）“哦，一门编程语言。我在我的岗位上已经学习了很多了。这不会太难。”我的大学新生编程课是使用 VAX 汇编程序教授的。在数据结构课上，我们使用 Pascal —— 在图书馆计算机中心的旧电脑上使用软盘加载。在一门更高一级的课程中，我的教授教授喜欢用 ADA 去展示所有的例子。在我们的 Sun 工作站上，我通过各种 UNIX 的实用源代码学到了一点 C。在 IBM，OS/2 源代码中我们使用了 C 和一些 x86 汇编程序；在一个与 Apple 合作的项目中我们大量使用 C++ 的面向对象功能。不久后我学到了 shell 脚本，开始是 csh，但是在 90 年代中期发现 Linux 后就转到了 Bash。在 90 年代后期，我在将 IBM 的定制的 JVM 代码中的即时（JIT）编译器移植到 Linux 时，我不得不开始学习 m4（与其说是编程语言，不如说是一种宏处理器）。一晃 20 年……我从未因为学习一门新的编程语言而焦灼。但是 Go 让我感觉有些不同。我打算公开贡献，上传到 GitHub，让任何有兴趣的人都可以看到！作为一个 40 多岁的资深开发者的 Go 新手，我不想成为一个笑话。我们都知道程序员的骄傲，不想丢人，不论你的经验水平如何。我早期的调研显示，Go 似乎比某些语言更 “地道”。它不仅仅是让代码可以编译；也需要让代码可以 “Go Go Go”。现在，我的个人的 Go 之旅四年间有了几百个拉取请求（PR），我不是致力于成为一个专家，但是现在我觉得贡献和编写代码比我在 2014 年的时候更舒服了。所以，你该怎么教一个老人新的技能或者一门编程语言呢？以下是我自己在前往 Golang 大陆之旅的四个步骤。1、不要跳过基础虽然你可以通过复制代码来进行你早期的学习（谁还有时间阅读手册！？），Go 有一个非常易读的 语言规范，它写的很易于理解，即便你在语言或者编译理论方面没有取得硕士学位。鉴于 Go 的 参数：类型 顺序的特有习惯，以及一些有趣的语言功能，例如通道和 go 协程，搞定这些新概念是非常重要的是事情。阅读这个附属的文档 高效 Go 编程，这是 Golang 创造者提供的另一个重要资源，它将为你提供有效和正确使用语言的准备。2、从最好的中学习有许多宝贵的资源可供挖掘，可以将你的 Go 知识提升到下一个等级。最近在 GopherCon 上的所有讲演都可以在网上找到，如这个 GopherCon US 2018 的详尽列表。这些讲演的专业知识和技术水平各不相同，但是你可以通过它们轻松地找到一些你所不了解的事情。Francesc Campoy 创建了一个名叫 JustForFunc 的 Go 编程视频系列，其不断增多的剧集可以用来拓宽你的 Go 知识和理解。直接搜索 “Golang" 可以为那些想要了解更多信息的人们展示许多其它视频和在线资源。想要看代码？在 GitHub 上许多受欢迎的云原生项目都是用 Go 写的：Docker/Moby、Kubernetes、Istio、containerd、CoreDNS，以及许多其它的。语言纯粹主义者可能会认为一些项目比另外一些更地道，但这些都是很好的起点，可以看到在高度活跃的项目的大型代码库中使用 Go 的程度。3、使用优秀的语言工具你会很快了解到 gofmt 的宝贵之处。Go 最漂亮的一个地方就在于没有关于每个项目代码格式的争论 —— gofmt 内置在语言的运行环境中，并且根据一系列可靠的、易于理解的语言规则对 Go 代码进行格式化。我不知道有哪个基于 Golang 的项目会在持续集成中不坚持使用 gofmt 检查拉取请求。除了直接构建于运行环境和 SDK 中的一系列有价值的工具之外，我强烈建议使用一个对 Golang 的特性有良好支持的编辑器或者 IDE。由于我经常在命令行中进行工作，我依赖于 Vim 加上强大的 vim-go 插件。我也喜欢微软提供的 VS Code，特别是它的 Go 语言 插件。想要一个调试器？Delve 项目在不断的改进和成熟，它是在 Go 二进制文件上进行 gdb 式调试的强有力的竞争者。4、写一些代码你要是不开始尝试使用 Go 写代码，你永远不知道它有什么好的地方。找一个有 “需要帮助” 问题标签的项目，然后开始贡献代码。如果你已经使用了一个用 Go 编写的开源项目，找出它是否有一些可以用初学者方式解决的 Bug，然后开始你的第一个拉取请求。与生活中的大多数事情一样，实践出真知，所以开始吧。事实证明，你可以教会一个资深的老开发者一门新的技能甚至编程语言。via: https://opensource.com/article/18/11/learning-golang作者：Phill Estes 选题：lujun9972 译者：dianbanjiu 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
这篇文章受到了我与同事讨论使用 切片 slice 作为 栈 stack 的一次聊天的启发。后来话题聊到了 Go 语言中的切片是如何工作的。我认为这些信息对别人也有用，所以就把它记录了下来。数组任何关于 Go 语言切片的讨论都要从另一个数据结构也就是 数组 array 开始。Go 的数组有两个特性：数组的长度是固定的；[5]int 是由 5 个 int 构成的数组，和 [3]int 不同。数组是值类型。看下面这个示例：package main import "fmt" func main() { var a [5]int b := a b[2] = 7 fmt.Println(a, b) // prints [0 0 0 0 0] [0 0 7 0 0] }语句 b := a 定义了一个类型是 [5]int 的新变量 b，然后把 a 中的内容 复制 到 b 中。改变 b 对 a 中的内容没有影响，因为 a 和 b 是相互独立的值。 1 切片Go 语言的切片和数组的主要有如下两个区别：切片没有一个固定的长度。切片的长度不是它类型定义的一部分，而是由切片内部自己维护的。我们可以使用内置的 len 函数知道它的长度。 2 将一个切片赋值给另一个切片时 不会 对切片内容进行复制操作。这是因为切片没有直接持有其内部数据，而是保留了一个指向 底层数组 3 的指针。数据都保留在底层数组里。基于第二个特性，两个切片可以享有共同的底层数组。看下面的示例：对切片取切片package main import "fmt" func main() { var a = []int{1,2,3,4,5} b := a[2:] b[0] = 0 fmt.Println(a, b) // prints [1 2 0 4 5] [0 4 5] }在这个例子里，a 和 b 享有共同的底层数组 —— 尽管 b 在数组里的起始偏移量不同，两者的长度也不同。通过 b 修改底层数组的值也会导致 a 里的值的改变。将切片传进函数package main import "fmt" func negate(s []int) { for i := range s { s[i] = -s[i] } } func main() { var a = []int{1, 2, 3, 4, 5} negate(a) fmt.Println(a) // prints [-1 -2 -3 -4 -5] }在这个例子里，a 作为形参 s 的实参传进了 negate 函数，这个函数遍历 s 内的元素并改变其符号。尽管 nagate 没有返回值，且没有访问到 main 函数里的 a。但是当将之传进 negate 函数内时，a 里面的值却被改变了。大多数程序员都能直观地了解 Go 语言切片的底层数组是如何工作的，因为它与其它语言中类似数组的工作方式类似。比如下面就是使用 Python 重写的这一小节的第一个示例：Python 2.7.10 (default, Feb 7 2017, 00:08:15) [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> a = [1,2,3,4,5] >>> b = a >>> b[2] = 0 >>> a [1, 2, 0, 4, 5]以及使用 Ruby 重写的版本:irb(main):001:0> a = [1,2,3,4,5] => [1, 2, 3, 4, 5] irb(main):002:0> b = a => [1, 2, 3, 4, 5] irb(main):003:0> b[2] = 0 => 0 irb(main):004:0> a => [1, 2, 0, 4, 5]在大多数将数组视为对象或者是引用类型的语言也是如此。 4 切片头切片同时拥有值和指针特性的神奇之处在于理解切片实际上是一个 结构体 struct 类型。通常在 反射 reflect 包内相应部分之后的这个结构体被称作 切片头 slice header 。切片头的定义大致如下：package runtime type slice struct { ptr unsafe.Pointer len int cap int }这很重要，因为和 map 以及 chan 这两个类型不同，切片是值类型，当被赋值或者被作为参数传入函数时候会被复制过去。程序员们都能理解 square 的形参 v 和 main 中声明的 v 的是相互独立的。请看下面的例子：package main import "fmt" func square(v int) { v = v * v } func main() { v := 3 square(v) fmt.Println(v) // prints 3, not 9 }因此 square 对自己的形参 v 的操作没有影响到 main 中的 v。下面这个示例中的 s 也是 main 中声明的切片 s 的独立副本， 而不是 指向 main 的 s 的指针。package main import "fmt" func double(s []int) { s = append(s, s...) } func main() { s := []int{1, 2, 3} double(s) fmt.Println(s, len(s)) // prints [1 2 3] 3 }Go 的切片是作为值传递而不是指针这一点不太寻常。当你在 Go 内定义一个结构体时，90% 的时间里传递的都是这个结构体的指针 5 。切片的传递方式真的很不寻常，我能想到的唯一与之相同的例子只有 time.Time。切片作为值传递而不是作为指针传递这一特殊行为会让很多想要理解切片的工作原理的 Go 程序员感到困惑。你只需要记住，当你对切片进行赋值、取切片、传参或者作为返回值等操作时，你是在复制切片头结构的三个字段：指向底层数组的指针、长度，以及容量。总结我们来用引出这一话题的切片作为栈的例子来总结下本文的内容：package main import "fmt" func f(s []string, level int) { if level > 5 { return } s = append(s, fmt.Sprint(level)) f(s, level+1) fmt.Println("level:", level, "slice:", s) } func main() { f(nil, 0) }在 main 函数的最开始我们把一个 nil 切片传给了函数 f 作为 level 0 。在函数 f 里我们把当前的 level 添加到切片的后面，之后增加 level 的值并进行递归。一旦 level 大于 5，函数返回，打印出当前的 level 以及它们复制到的 s 的内容。level: 5 slice: [0 1 2 3 4 5] level: 4 slice: [0 1 2 3 4] level: 3 slice: [0 1 2 3] level: 2 slice: [0 1 2] level: 1 slice: [0 1] level: 0 slice: [0]你可以注意到在每一个 level 内 s 的值没有被别的 f 的调用影响，尽管当计算更高的 level 时作为 append 的副产品，调用栈内的四个 f 函数创建了四个底层数组 6 ，但是没有影响到当前各自的切片。扩展阅读如果你想要了解更多 Go 语言内切片运行的原理，我建议看看 Go 博客里的这些文章：Go Slices: usage and internals (blog.golang.org)Arrays, slices (and strings): The mechanics of 'append' (blog.golang.org)相关文章：If a map isn't a reference variable, what is it?What is the zero value, and why is it useful?The empty structShould methods be declared on T or *T这不是数组才有的特性，在 Go 语言里中 一切 赋值都是复制过去的。 ↩你也可以在对数组使用 len 函数，但是其结果本来就人尽皆知。 ↩有时也叫做 后台数组 backing array ，以及更不严谨的说法是后台切片。 ↩Go 语言里我们倾向于说值类型以及指针类型，因为 C++ 的 引用 reference 类型这个词产生误会。但在这里我认为调用数组作为引用类型是没有问题的。 ↩如果你的结构体有定义在其上的方法或者用于满足某个接口，那么你传入结构体指针的比率可以飙升到接近 100%。 ↩证明留做习题。 ↩via: https://dave.cheney.net/2018/07/12/slices-from-the-ground-up 作者：Dave Cheney 译者：name1e5s 校对：pityonline本文由 LCTT 原创编译，Linux中国 荣誉推出
第一节: Hello, OpenGL第二节: 绘制游戏面板第三节：实现游戏功能该教程的完整源代码可以从 GitHub 上找到。欢迎回到《OpenGL 与 Go 教程》！如果你还没有看过 第一节 和 第二节)，那就要回过头去看一看。到目前为止，你应该懂得如何创建网格系统以及创建代表方格中每一个单元的格子阵列。现在可以开始把网格当作游戏面板实现 康威生命游戏 Conway's Game of Life 。开始吧！实现康威生命游戏康威生命游戏的其中一个要点是所有 细胞 cell 必须同时基于当前细胞在面板中的状态确定下一个细胞的状态。也就是说如果细胞 (X=3,Y=4) 在计算过程中状态发生了改变，那么邻近的细胞 (X=4,Y=4) 必须基于 (X=3,Y=4) 的状态决定自己的状态变化，而不是基于自己现在的状态。简单的讲，这意味着我们必须遍历细胞，确定下一个细胞的状态，而在绘制之前不改变他们的当前状态，然后在下一次循环中我们将新状态应用到游戏里，依此循环往复。为了完成这个功能，我们需要在 cell 结构体中添加两个布尔型变量：type cell struct { drawable uint32 alive bool aliveNext bool x int y int } 这里我们添加了 alive 和 aliveNext，前一个是细胞当前的专题，后一个是经过计算后下一回合的状态。现在添加两个函数，我们会用它们来确定 cell 的状态：// checkState 函数决定下一次游戏循环时的 cell 状态 func (c *cell) checkState(cells [][]*cell) { c.alive = c.aliveNext c.aliveNext = c.alive liveCount := c.liveNeighbors(cells) if c.alive { // 1. 当任何一个存活的 cell 的附近少于 2 个存活的 cell 时，该 cell 将会消亡，就像人口过少所导致的结果一样 if liveCount < 2 { c.aliveNext = false } // 2. 当任何一个存活的 cell 的附近有 2 至 3 个存活的 cell 时，该 cell 在下一代中仍然存活。 if liveCount == 2 || liveCount == 3 { c.aliveNext = true } // 3. 当任何一个存活的 cell 的附近多于 3 个存活的 cell 时，该 cell 将会消亡，就像人口过多所导致的结果一样 if liveCount > 3 { c.aliveNext = false } } else { // 4. 任何一个消亡的 cell 附近刚好有 3 个存活的 cell，该 cell 会变为存活的状态，就像重生一样。 if liveCount == 3 { c.aliveNext = true } } } // liveNeighbors 函数返回当前 cell 附近存活的 cell 数 func (c *cell) liveNeighbors(cells [][]*cell) int { var liveCount int add := func(x, y int) { // If we're at an edge, check the other side of the board. if x == len(cells) { x = 0 } else if x == -1 { x = len(cells) - 1 } if y == len(cells[x]) { y = 0 } else if y == -1 { y = len(cells[x]) - 1 } if cells[x][y].alive { liveCount++ } } add(c.x-1, c.y) // To the left add(c.x+1, c.y) // To the right add(c.x, c.y+1) // up add(c.x, c.y-1) // down add(c.x-1, c.y+1) // top-left add(c.x+1, c.y+1) // top-right add(c.x-1, c.y-1) // bottom-left add(c.x+1, c.y-1) // bottom-right return liveCount } 在 checkState 中我们设置当前状态（alive） 等于我们最近迭代结果（aliveNext）。接下来我们计数邻居数量，并根据游戏的规则来决定 aliveNext 状态。该规则是比较清晰的，而且我们在上面的代码当中也有说明，所以这里不再赘述。更加值得注意的是 liveNeighbors 函数里，我们返回的是当前处于存活（alive）状态的细胞的邻居个数。我们定义了一个叫做 add 的内嵌函数，它会对 X 和 Y 坐标做一些重复性的验证。它所做的事情是检查我们传递的数字是否超出了范围——比如说，如果细胞 (X=0,Y=5) 想要验证它左边的细胞，它就得验证面板另一边的细胞 (X=9,Y=5)，Y 轴与之类似。在 add 内嵌函数后面，我们给当前细胞附近的八个细胞分别调用 add 函数，示意如下：[ [-, -, -], [N, N, N], [N, C, N], [N, N, N], [-, -, -] ] 在该示意中，每一个叫做 N 的细胞是 C 的邻居。现在是我们的 main 函数，这里我们执行核心游戏循环，调用每个细胞的 checkState 函数进行绘制：func main() { ... for !window.ShouldClose() { for x := range cells { for _, c := range cells[x] { c.checkState(cells) } } draw(cells, window, program) } } 现在我们的游戏逻辑全都设置好了，我们需要修改细胞绘制函数来跳过绘制不存活的细胞：func (c *cell) draw() { if !c.alive { return } gl.BindVertexArray(c.drawable) gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square)/3)) } 如果我们现在运行这个游戏，你将看到一个纯黑的屏幕，而不是我们辛苦工作后应该看到生命模拟。为什么呢？其实这正是模拟在工作。因为我们没有活着的细胞，所以就一个都不会绘制出来。现在完善这个函数。回到 makeCells 函数，我们用 0.0 到 1.0 之间的一个随机数来设置游戏的初始状态。我们会定义一个大小为 0.15 的常量阈值，也就是说每个细胞都有 15% 的几率处于存活状态。import ( "math/rand" "time" ... ) const ( ... threshold = 0.15 ) func makeCells() [][]*cell { rand.Seed(time.Now().UnixNano()) cells := make([][]*cell, rows, rows) for x := 0; x < rows; x++ { for y := 0; y < columns; y++ { c := newCell(x, y) c.alive = rand.Float64() < threshold c.aliveNext = c.alive cells[x] = append(cells[x], c) } } return cells } 我们首先增加两个引入：随机（math/rand）和时间（time），并定义我们的常量阈值。然后在 makeCells 中我们使用当前时间作为随机种子，给每个游戏一个独特的起始状态。你也可也指定一个特定的种子值，来始终得到一个相同的游戏，这在你想重放某个有趣的模拟时很有用。接下来在循环中，在用 newCell 函数创造一个新的细胞时，我们根据随机浮点数的大小设置它的存活状态，随机数在 0.0 到 1.0 之间，如果比阈值（0.15）小，就是存活状态。再次强调，这意味着每个细胞在开始时都有 15% 的几率是存活的。你可以修改数值大小，增加或者减少当前游戏中存活的细胞。我们还把 aliveNext 设成 alive 状态，否则在第一次迭代之后我们会发现一大片细胞消亡了，这是因为 aliveNext 将永远是 false。现在继续运行它，你很有可能看到细胞们一闪而过，但你却无法理解这是为什么。原因可能在于你的电脑太快了，在你能够看清楚之前就运行了（甚至完成了）模拟过程。让我们降低游戏速度，在主循环中引入一个帧率（FPS）限制：const ( ... fps = 2 ) func main() { ... for !window.ShouldClose() { t := time.Now() for x := range cells { for _, c := range cells[x] { c.checkState(cells) } } if err := draw(prog, window, cells); err != nil { panic(err) } time.Sleep(time.Second/time.Duration(fps) - time.Since(t)) } } 现在你能给看出一些图案了，尽管它变换的很慢。把 FPS 加到 10，把方格的尺寸加到 100x100，你就能看到更真实的模拟：const ( ... rows = 100 columns = 100 fps = 10 ... ) 试着修改常量，看看它们是怎么影响模拟过程的 —— 这是你用 Go 语言写的第一个 OpenGL 程序，很酷吧？进阶内容？这是《OpenGL 与 Go 教程》的最后一节，但是这不意味着到此而止。这里有些新的挑战，能够增进你对 OpenGL （以及 Go）的理解。给每个细胞一种不同的颜色。让用户能够通过命令行参数指定格子尺寸、帧率、种子和阈值。在 GitHub 上的 github.com/KyleBanks/conways-gol 里你可以看到一个已经实现的程序。把格子的形状变成其它更有意思的，比如六边形。用颜色表示细胞的状态 —— 比如，在第一帧把存活状态的格子设成绿色，如果它们存活了超过三帧的时间，就变成黄色。如果模拟过程结束了，就自动关闭窗口，也就是说所有细胞都消亡了，或者是最后两帧里没有格子的状态有改变。将着色器源代码放到单独的文件中，而不是把它们用字符串的形式放在 Go 的源代码中。总结希望这篇教程对想要入门 OpenGL （或者是 Go）的人有所帮助！这很有趣，因此我也希望理解学习它也很有趣。正如我所说的，OpenGL 可能是非常恐怖的，但只要你开始着手了就不会太差。你只用制定一个个可达成的小目标，然后享受每一次成功，因为尽管 OpenGL 不会总像它看上去的那么难，但也肯定有些难懂的东西。我发现，当遇到一个难于理解用 go-gl 生成的代码的 OpenGL 问题时，你总是可以参考一下在网上更流行的当作教程的 C 语言代码，这很有用。通常 C 语言和 Go 语言的唯一区别是在 Go 中，gl 函数的前缀是 gl. 而不是 gl，常量的前缀是 gl 而不是 GL_。这可以极大地增加了你的绘制知识！该教程的完整源代码可从 GitHub 上获得。回顾这是 main.go 文件最终的内容：package main import ( "fmt" "log" "math/rand" "runtime" "strings" "time" "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 = 100 columns = 100 threshold = 0.15 fps = 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 alive bool aliveNext bool x int y int } func main() { runtime.LockOSThread() window := initGlfw() defer glfw.Terminate() program := initOpenGL() cells := makeCells() for !window.ShouldClose() { t := time.Now() for x := range cells { for _, c := range cells[x] { c.checkState(cells) } } draw(cells, window, program) time.Sleep(time.Second/time.Duration(fps) - time.Since(t)) } } 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 { rand.Seed(time.Now().UnixNano()) cells := make([][]*cell, rows, rows) for x := 0; x < rows; x++ { for y := 0; y < columns; y++ { c := newCell(x, y) c.alive = rand.Float64() < threshold c.aliveNext = c.alive 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() { if !c.alive { return } gl.BindVertexArray(c.drawable) gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square)/3)) } // checkState 函数决定下一次游戏循环时的 cell 状态 func (c *cell) checkState(cells [][]*cell) { c.alive = c.aliveNext c.aliveNext = c.alive liveCount := c.liveNeighbors(cells) if c.alive { // 1. 当任何一个存活的 cell 的附近少于 2 个存活的 cell 时，该 cell 将会消亡，就像人口过少所导致的结果一样 if liveCount < 2 { c.aliveNext = false } // 2. 当任何一个存活的 cell 的附近有 2 至 3 个存活的 cell 时，该 cell 在下一代中仍然存活。 if liveCount == 2 || liveCount == 3 { c.aliveNext = true } // 3. 当任何一个存活的 cell 的附近多于 3 个存活的 cell 时，该 cell 将会消亡，就像人口过多所导致的结果一样 if liveCount > 3 { c.aliveNext = false } } else { // 4. 任何一个消亡的 cell 附近刚好有 3 个存活的 cell，该 cell 会变为存活的状态，就像重生一样。 if liveCount == 3 { c.aliveNext = true } } } // liveNeighbors 函数返回当前 cell 附近存活的 cell 数 func (c *cell) liveNeighbors(cells [][]*cell) int { var liveCount int add := func(x, y int) { // If we're at an edge, check the other side of the board. if x == len(cells) { x = 0 } else if x == -1 { x = len(cells) - 1 } if y == len(cells[x]) { y = 0 } else if y == -1 { y = len(cells[x]) - 1 } if cells[x][y].alive { liveCount++ } } add(c.x-1, c.y) // To the left add(c.x+1, c.y) // To the right add(c.x, c.y+1) // up add(c.x, c.y-1) // down add(c.x-1, c.y+1) // top-left add(c.x+1, c.y+1) // top-right add(c.x-1, c.y-1) // bottom-left add(c.x+1, c.y-1) // bottom-right return liveCount } // initGlfw 初始化 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 } // 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 告诉我这篇文章对你是否有帮助，或者在 Twitter 下方关注我以便及时获取最新文章！via: https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-3-implementing-the-game作者：kylewbanks 译者：GitFuture 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
第一节: Hello, OpenGL第二节: 绘制游戏面板第三节：实现游戏功能这篇教程的所有源代码都可以在 GitHub 上找到。欢迎回到《OpenGL 与 Go 教程》。如果你还没有看过第一节，那就要回过头去看看那一节。你现在应该能够创造一个漂亮的白色三角形，但我们不会把三角形当成我们游戏的基本单元，是时候把三角形变成正方形了，然后我们会做出一个完整的方格。让我们现在开始做吧！利用三角形绘制方形在我们绘制方形之前，先把三角形变成直角三角形。打开 main.go 文件，把 triangle 的定义改成像这个样子：triangle = []float32{ -0.5, 0.5, 0, -0.5, -0.5, 0, 0.5, -0.5, 0, } 我们做的事情是，把最上面的顶点 X 坐标移动到左边（也就是变为 -0.5），这就变成了像这样的三角形：很简单，对吧？现在让我们用两个这样的三角形顶点做成正方形。把 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。我们通过添加三个顶点，把顶点数增加了一倍，这三个顶点就是右上角的三角形，用来拼成方形。运行它看看效果：很好，现在我们能够绘制正方形了！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 } 这里我们创建多维的 切片 slice ，代表我们的游戏面板，用名为 newCell 的新函数创建的 cell 来填充矩阵的每个元素，我们待会就来实现 newCell 这个函数。在接着往下阅读前，我们先花一点时间来看看 makeCells 函数做了些什么。我们创造了一个切片，这个切片的长度和格子的行数相等，每一个切片里面都有一个 细胞 cell 的切片，这些细胞的数量与列数相等。如果我们把 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 函数。如果运行这段代码，你能看到像下面这样的东西：这是你想看到的吗？我们做的是在格子里为每一行每一列创建了一个方块，然后给它上色，这就填满了整个面板！注释掉 for 循环，我们就可以看到一个明显独立的细胞，像这样：// for x := range cells { // for _, c := range cells[x] { // c.draw() // } // } cells[2][3].draw() 这只绘制坐标在 (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 或者下方的连接，关注我以便获取最新的文章！via: https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-2-drawing-the-game-board作者：kylewbanks 译者：GitFtuture 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
朋友们，你们好！近来，我听到了大量的关于新出的 .NET Core 和其性能的讨论，尤其在 Web 服务方面的讨论更甚。因为是新出的，我不想立马就比较两个不同的东西，所以我耐心等待，想等发布更稳定的版本后再进行。本周一（8 月 14 日），微软发布 .NET Core 2.0 版本，因此，我准备开始。您们认为呢？如前面所提的，我们会比较它们相同的东西，比如应用程序、预期响应及运行时的稳定性，所以我们不会把像对 JSON 或者 XML 的编码、解码这些烦多的事情加入比较游戏中来，仅仅只会使用简单的文本消息。为了公平起见，我们会分别使用 Go 和 .NET Core 的 MVC 架构模式。参赛选手Go （或称 Golang）： 是一种快速增长的开源编程语言，旨在构建出简单、快捷和稳定可靠的应用软件。用于支持 Go 语言的 MVC web 框架并不多，还好我们找到了 Iris ，可胜任此工作。Iris： 支持 Go 语言的快速、简单和高效的微型 Web 框架。它为您的下一代网站、API 或分布式应用程序奠定了精美的表现方式和易于使用的基础。C#： 是一种通用的、面向对象的编程语言。其开发团队由 Anders Hejlsberg 领导。.NET Core： 跨平台，可以在极少时间内开发出高性能的应用程序。可从 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 构建的最新 8.3 版本。两个应用程序都通过请求路径 “api/values/{id}” 返回文本“值”。.NET Core MVCLogo 由 Pablo Iglesias 设计。可以使用 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 MVCLogo 由 Santosh Anand 设计。源代码：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 想通过图片来理解的人，我也把我的屏幕截屏出来了！请点击这儿可以看到这些屏幕快照。总结完成 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 和 Scott Hanselman在此 tweet 评论上指出，.NET Core Startup.cs 文件中 services.AddMvc(); 这行可以替换为 services.AddMvcCore();。我听从他们的意见，修改代码，重新运行基准，该文章的 .NET Core 应用程序的基准输出已经修改。@topdawgevh @shanselman 他们也在使用 AddMvc() 而不是 AddMvcCore() ...，难道都不包含中间件？ — @clarkis117@clarkis117 @topdawgevh Cool @MakisMaropoulos @benaadams @davidfowl 我们来看看。认真学习下怎么使用更简单的性能默认值。 — @shanselman@shanselman @clarkis117 @topdawgevh @benaadams @davidfowl @shanselman @benaadams @davidfowl 谢谢您们的反馈意见。我已经修改，更新了结果，没什么不同。对其它的建议，我非常欢迎。 — @MakisMaropoulos它有点稍微的不同但相差不大（从 8.61MB/s 到 8.91MB/s）想要了解跟 services.AddMvc() 标准比较结果的，可以点击这儿。想再多了解点儿吗?我们再制定一个基准，产生 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。接下来呢？这里有上面所示的源代码，请下载下来，在您本地以同样的基准运行，然后把运行结果在这儿给大家分享。想添加 Go 或 C# .net core WEB 服务框架到列表的朋友请向这个仓库的 _benchmarks 目录推送 PR。我也需要亲自感谢下 dev.to 团队，感谢把我的这篇文章分享到他们的 Twitter 账户。感谢大家真心反馈，玩得开心！更新 : 2017 年 8 月 21 ，周一很多人联系我，希望看到一个基于 .NET Core 的较低级别 Kestrel 的基准测试文章。因此我完成了，请点击下面的链接来了解 Kestrel 和 Iris 之间的性能差异，它还包含一个会话存储管理基准！via: https://hackernoon.com/go-vs-net-core-in-terms-of-http-performance-7535a61b67b8作者：Gerasimos Maropoulos 译者：runningwater 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
在 XO 公司，我们最初使用 Node 和 Ruby 构建相互连接的服务系统。我们享受 Node 带来的明显性能优势，以及可以访问已有的大型软件包仓库。我们也可以轻松地在公司内部发布并复用已有的插件和模块。极大地提高了开发效率，使得我们可以快速编写出可拓展的和可靠的应用。而且，庞大的 Node 社区使我们的工程师向开源软件贡献更加容易（比如 BunnyBus 和 Felicity）。虽然我在大学时期和刚刚工作的一些时间在使用更严谨的编译语言，比如 C++ 和 C#，而后来我开始使用 JavaScript。我很喜欢它的自由和灵活，但是我最近开始怀念静态和结构化的语言，因为当时有一个同事让我对 Go 语言产生了兴趣。我从写 JavaScript 到写 Go，我发现两种语言有很多相似之处。两者学习起来都很快并且易于上手，都具有充满表现力的语法，并且在开发者社区中都有很多工作机会。没有完美的编程语言，所以你应该总是选择一个适合手头项目的语言。在这篇文章中，我将要说明这两种语言深层次上的关键区别，希望能鼓励没有用过 Go 语言的用户可以有机会使用 Go 。大体上的差异在深入细节之前，我们应该先了解一下两种语言之间的重要区别。Go，或称 Golang，是 Google 在 2007 年创建的自由开源编程语言。它以快速和简单为设计目标。Go 被直接编译成机器码，这就是它速度的来源。使用编译语言调试是相当容易的，因为你可以在早期捕获大量错误。Go 也是一种强类型的语言，它有助于数据完整，并可以在编译时查找类型错误。另一方面，JavaScript 是一种弱类型语言。除了忽略验证数据的类型和真值判断陷阱所带来的额外负担之外，使用弱类型语言也有自己的好处。比起使用 接口 interfaces 和 范型 generics ， 柯里化 currying 和 可变的形参个数 flexible arity 让函数变得更加灵活。JavaScript 在运行时进行解释，这可能导致错误处理和调试的问题。Node 是一款基于 Google V8 虚拟机的 JavaScript 运行库，这使它成为一个轻量和快速的 Web 开发平台。语法作为原来的 JavaScript 开发者，Go 简单和直观的语法很吸引我。由于两种语言的语法可以说都是从 C 语言演变而来的，所以它们的语法有很多相同之处。Go 被普遍认为是一种“容易学习的语言”。那是因为它的对开发者友好的工具、精简的语法和固守惯例（LCTT 译注：惯例优先）。Go 包含大量有助于简化开发的内置特性。你可以用标准 Go 构建工具把你的程序用 go build 命令编译成二进制可执行文件。使用内置的测试套件进行测试只需要运行 go test 即可。 诸如原生支持的并发等特性甚至在语言层面上提供。Google 的 Go 开发者认为，现在的编程太复杂了，太多的“记账一样，重复劳动和文书工作”。这就是为什么 Go 的语法被设计得如此简单和干净，以减少混乱、提高效率和增强可读性。它还鼓励开发人员编写明确的、易于理解的代码。Go 只有 25 个保留关键字和一种循环（for 循环），而不像 JavaScript 有 大约 84 个关键字（包括保留关键字字、对象、属性和方法）。为了说明语法的一些差异和相似之处，我们来看几个例子：标点符号： Go 去除了所有多余的符号以提高效率和可读性。尽管 JavaScript 中需要符号的地方也不多（参见： Lisp），而且经常是可选的，但我更加喜欢 Go 的简单。// JavaScript 的逗号和分号 for (var i = 0; i < 10; i++) { console.log(i); } JavaScript 中的标点// Go 使用最少数量标点 for i := 0; i < 10; i++ { fmt.Println(i) } Go 中的标点赋值：由于 Go 是强类型语言，所以你在初始化变量时可以使用 := 操作符来进行类型推断，以避免重复声明，而 JavaScript 则在运行时声明类型。// JavaScript 赋值 var foo = "bar"; JavaScript 中的赋值// Go 的赋值 var foo string //不使用类型推导 foo = "bar" foo := "bar" //使用类型推导 Go 的赋值导出：在 JavaScript 中，你必须从某个模块中显式地导出。 在 Go 中，任何大写的函数将被默认导出。const Bar = () => {}; module.exports = { Bar } JavaScript 中的导出// Go 中的导出 package foo // 定义包名 func Bar (s string) string { // Bar 将被导出 } Go 中的导出导入：在 JavaScript 中 required 库是导入依赖项和模块所必需的，而 Go 则利用原生的 import 关键字通过包的路径导入模块。另一个区别是，与 Node 的中央 NPM 存储库不同，Go 使用 URL 作为路径来导入非标准库的包，这是为了从包的源码仓库直接克隆依赖。// Javascript 的导入 var foo = require('foo'); foo.bar(); JavaScript 的导入// Go 的导入 import ( "fmt" // Go 的标准库部分 "github.com/foo/foo" // 直接从仓库导入 ) foo.Bar() Go 的导入返回值：通过 Go 的多值返回特性可以优雅地传递和处理返回值和错误，并且通过传递引用减少了不正确的值传递。在 JavaScript 中需要通过一个对象或者数组来返回多个值。// Javascript - 返回多值 function foo() { return {a: 1, b: 2}; } const { a, b } = foo(); JavaScript 的返回// Go - 返回多值 func foo() (int, int) { return 1, 2 } a, b := foo() Go 的返回错误处理：Go 推荐在错误出现的地方捕获它们，而不是像 Node 一样在回调中让错误冒泡。// Node 的错误处理 foo('bar', function(err, data) { // 处理错误 } JavaScript 的错误处理//Go 的错误处理 foo, err := bar() if err != nil { // 用 defer、 panic、 recover 或 log.fatal 等等处理错误. } Go 的错误处理可变参数函数：Go 和 JavaScript 的函数都支持传入不定数量的参数。function foo (...args) { console.log(args.length); } foo(); // 0 foo(1, 2, 3); // 3 JavaScript 中的可变参数函数func foo (args ...int) { fmt.Println(len(args)) } func main() { foo() // 0 foo(1,2,3) // 3 } Go 中的可变参数函数社区当比较 Go 和 Node 提供的编程范式哪种更方便时，两边都有不同的拥护者。Node 在软件包数量和社区的大小上完全胜过了 Go。Node 包管理器（NPM），是世界上最大的软件仓库，拥有超过 410,000 个软件包，每天以 555 个新软件包的惊人速度增长。这个数字可能看起来令人吃惊（确实是），但是需要注意的是，这些包许多是重复的，且质量不足以用在生产环境。 相比之下，Go 大约有 13 万个包。Node 和 Go 包的数量尽管 Node 和 Go 岁数相仿，但 JavaScript 使用更加广泛，并拥有巨大的开发者和开源社区。因为 Node 是为所有人开发的，并在开始的时候就带有一个强壮的包管理器，而 Go 是特地为 Google 开发的。下面的Spectrum 排行榜显示了当前流行的的顶尖 Web 开发语言。Web 开发语言排行榜前 7 名JavaScript 的受欢迎程度近年来似乎保持相对稳定，而 Go 一直在保持上升趋势。编程语言趋势性能如果你的主要关注点是速度呢？当今似乎人们比以前更重视性能的优化。用户不喜欢等待信息。 事实上，如果网页的加载时间超过 3 秒，40％ 的用户会放弃访问您的网站。因为它的非阻塞异步 I/O，Node 经常被认为是高性能的语言。另外，正如我之前提到的，Node 运行在针对动态语言进行了优化的 Google V8 引擎上。而 Go 的设计也考虑到速度。Google 的开发者们通过建立了一个“充满表现力而轻量级的类型系统；并发和垃圾回收机制；强制地指定依赖版本等等”，达成了这一目标。我运行了一些测试来比较 Node 和 Go 之间的性能。这些测试注重于语言提供的初级能力。如果我准备测试例如 HTTP 请求或者 CPU 密集型运算，我会使用 Go 语言级别的并发工具（goroutines/channels）。但是我更注重于各个语言提供的基本特性（参见 三种并发方法 了解关于 goroutines 和 channels 的更多知识）。我在基准测试中也加入了 Python，所以无论如何我们对 Node 和 Go 的结果都很满意。循环/算术迭代十亿项并把它们相加：var r = 0; for (var c = 0; c < 1000000000; c++) { r += c; } Nodepackage main func main() { var r int for c := 0; c < 1000000000; c++ { r += c } } Gosum(xrange(1000000000)) Python结果这里的输家无疑是 Python，花了超过 7 秒的 CPU 时间。而 Node 和 Go 都相当高效，分别用了 900 ms 和 408 ms。修正：由于一些评论表明 Python 的性能还可以提高。我更新了结果来反映这些变化。同时，使用 PyPy 大大地提高了性能。当使用 Python 3.6.1 和 PyPy 3.5.7 运行时，性能提升到 1.234 秒，但仍然不及 Go 和 Node 。I/O遍历一百万个数字并将其写入一个文件。var fs = require('fs'); var wstream = fs.createWriteStream('node'); for (var c = 0; c < 1000000; ++c) { wstream.write(c.toString()); } wstream.end(); Nodepackage main import ( "bufio" "os" "strconv" ) func main() { file, _ := os.Create("go") b := bufio.NewWriter(file) for c := 0; c < 1000000; c++ { num := strconv.Itoa(c) b.WriteString(num) } file.Close() } Gowith open("python", "a") as text_file: for i in range(1000000): text_file.write(str(i)) Python结果Python 以 7.82 秒再次排名第三。 这次测试中，Node 和 Go 之间的差距很大，Node 花费大约 1.172 秒，Go 花费了 213 毫秒。真正令人印象深刻的是，Go 大部分的处理时间花费在编译上。如果我们将代码编译，以二进制运行，这个 I/O 测试仅花费 78 毫秒——要比 Node 快 15 倍。修正：修改了 Go 代码以实现缓存 I/O。冒泡排序将含有十个元素的数组排序一千万次。function bubbleSort(input) { var n = input.length; var swapped = true; while (swapped) { swapped = false; for (var i = 0; i < n; i++) { if (input[i - 1] > input [i]) { [input[i], input[i - 1]] = [input[i - 1], input[i]]; swapped = true; } } } } for (var c = 0; c < 1000000; c++) { const toBeSorted = [1, 3, 2, 4, 8, 6, 7, 2, 3, 0]; bubbleSort(toBeSorted); } Nodepackage main var toBeSorted [10]int = [10]int{1, 3, 2, 4, 8, 6, 7, 2, 3, 0} func bubbleSort(input [10]int) { n := len(input) swapped := true for swapped { swapped = false for i := 1; i < n; i++ { if input[i-1] > input[i] { input[i], input[i-1] = input[i-1], input[i] swapped = true } } } } func main() { for c := 0; c < 1000000; c++ { bubbleSort(toBeSorted) } } Godef bubbleSort(input): length = len(input) swapped = True while swapped: swapped = False for i in range(1,length): if input[i - 1] > input[i]: input[i], input[i - 1] = input[i - 1], input[i] swapped = True for i in range(1000000): toBeSorted = [1, 3, 2, 4, 8, 6, 7, 2, 3, 0] bubbleSort(toBeSorted) Python结果像刚才一样，Python 的表现是最差的，大约花费 15 秒完成了任务。 Go 完成任务的速度是 Node 的 16 倍。判决Go 无疑是这三个测试中的赢家，而 Node 大部分表现都很出色。Python 也表现不错。要清楚，性能不是选择编程语言需要考虑的全部内容。如果您的应用不需要处理大量数据，那么 Node 和 Go 之间的性能差异可能是微不足道的。 有关性能的一些其他比较，请参阅以下内容：Node Vs. GoMultiple Language Performance TestBenchmarks Game结论这个帖子不是为了证明一种语言比另一种语言更好。由于各种原因，每种编程语言都在软件开发社区中占有一席之地。 我的意图是强调 Go 和 Node 之间的差异，并且促进展示一种新的 Web 开发语言。 在为一个项目选择语言时，有各种因素需要考虑，比如开发人员的熟悉程度、花费和实用性。 我鼓励在决定哪种语言适合您时进行一次彻底的底层分析。正如我们所看到的，Go 有如下的优点：接近底层语言的性能，简单的语法和相对简单的学习曲线使它成为构建可拓展和安全的 Web 应用的理想选择。随着 Go 的使用率和社区活动的快速增长，它将会成为现代网络开发中的重要角色。话虽如此，我相信如果 Node 被正确地实现，它正在向正确的方向努力，仍然是一种强大而有用的语言。它具有大量的追随者和活跃的社区，使其成为一个简单的平台，可以让 Web 应用在任何时候启动和运行。资料如果你对学习 Go 语言感兴趣，可以参阅下面的资源：Golang 网站Golang WikiGolang Subredditvia: https://medium.com/xo-tech/from-node-to-go-a-high-level-comparison-56c8b717324a#.byltlz535作者：John Stamatakos 译者：trnhoe 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
快速测试 - 下面的代码输出什么？vals := make([]int, 5) for i := 0; i < 5; i++ { vals = append(vals, i) } fmt.Println(vals) 在 Go Playground 运行一下如果你猜测的是 [0 0 0 0 0 0 1 2 3 4]，那你是对的。等等，什么？ 为什么不是 [0 1 2 3 4]?如果你在测试中做错了，你也不用担心。这是在过渡到 Go 语言的过程中相当常见的错误，在这篇文章中，我们将说明为什么输出不是你预期的，以及如何利用 Go 的细微差别来使你的代码更有效率。切片 vs 数组在 Go 中同时有数组（array）和切片（slice）。这可能令人困惑，但一旦你习惯了，你会喜欢上它。请相信我。切片和数组之间存在许多差异，但我们要在本文中重点介绍的内容是数组的大小是其类型的一部分，而切片可以具有动态大小，因为它们是围绕数组的封装。这在实践中意味着什么？那么假设我们有数组 val a [10]int。该数组具有固定大小，且无法更改。如果我们调用 len(a)，它总是返回 10，因为这个大小是类型的一部分。因此，如果你突然需要在数组中超过 10 个项，则必须创建一个完全不同类型的新对象，例如 val b [11]int，然后将所有值从 a 复制到 b。在特定情况下，含有集合大小的数组是有价值的，但一般而言，这不是开发人员想要的。相反，他们希望在 Go 中使用类似于数组的东西，但是随着时间的推移，它们能够随时增长。一个粗略的方式是创建一个比它需要大得多的数组，然后将数组的一个子集视为数组。下面的代码是个例子。var vals [20]int for i := 0; i < 5; i++ { vals[i] = i * i } subsetLen := 5 fmt.Println("The subset of our array has a length of:", subsetLen) // Add a new item to our array vals[subsetLen] = 123 subsetLen++ fmt.Println("The subset of our array has a length of:", subsetLen) 在 Go Playground 中运行在代码中，我们有一个长度为 20 的数组，但是由于我们只使用一个子集，代码中我们可以假定数组的长度是 5，然后在我们向数组中添加一个新的项之后是 6。这是（非常粗略地说）切片是如何工作的。它们包含一个具有设置大小的数组，就像我们前面的例子中的数组一样，它的大小为 20。它们还跟踪程序中使用的数组的子集 - 这就是 append 属性，它类似于上一个例子中的 subsetLen 变量。最后，一个切片还有一个 capacity，类似于前面例子中我们的数组的总长度（20）。这是很有用的，因为它会告诉你的子集在无法容纳切片数组之前可以增长的大小。当发生这种情况时，需要分配一个新的数组，但所有这些逻辑都隐藏在 append 函数的后面。简而言之，使用 append 函数组合切片给我们一个非常类似于数组的类型，但随着时间的推移，它可以处理更多的元素。我们再来看一下前面的例子，但是这次我们将使用切片而不是数组。var vals []int for i := 0; i < 5; i++ { vals = append(vals, i) fmt.Println("The length of our slice is:", len(vals)) fmt.Println("The capacity of our slice is:", cap(vals)) } // Add a new item to our array vals = append(vals, 123) fmt.Println("The length of our slice is:", len(vals)) fmt.Println("The capacity of our slice is:", cap(vals)) // Accessing items is the same as an array fmt.Println(vals[5]) fmt.Println(vals[2]) 在 Go Playground 中运行我们仍然可以像数组一样访问我们的切片中的元素，但是通过使用切片和 append 函数，我们不再需要考虑背后数组的大小。我们仍然可以通过使用 len 和 cap 函数来计算出这些东西，但是我们不用担心太多。简洁吧？回到测试记住这点，让我们回顾前面的测试，看下什么出错了。vals := make([]int, 5) for i := 0; i < 5; i++ { vals = append(vals, i) } fmt.Println(vals) 当调用 make 时，我们允许最多传入 3 个参数。第一个是我们分配的类型，第二个是类型的“长度”，第三个是类型的“容量”（这个参数是可选的）。通过传递参数 make([]int, 5)，我们告诉程序我们要创建一个长度为 5 的切片，在这种情况下，默认的容量与长度相同 - 本例中是 5。虽然这可能看起来像我们想要的那样，这里的重要区别是我们告诉我们的切片，我们要将“长度”和“容量”设置为 5，假设你想要在初始的 5 个元素之后添加新的元素，我们接着调用 append 函数，那么它会增加容量的大小，并且会在切片的最后添加新的元素。如果在代码中添加一条 Println() 语句，你可以看到容量的变化。vals := make([]int, 5) fmt.Println("Capacity was:", cap(vals)) for i := 0; i < 5; i++ { vals = append(vals, i) fmt.Println("Capacity is now:", cap(vals)) } fmt.Println(vals) 在 Go Playground 中运行最后，我们最终得到 [0 0 0 0 0 0 1 2 3 4] 的输出而不是希望的 [0 1 2 3 4]。如何修复它呢？好的，这有几种方法，我们将讲解两种，你可以选取任何一种在你的场景中最有用的方法。直接使用索引写入而不是 append第一种修复是保留 make 调用不变，并且显式地使用索引来设置每个元素。这样，我们就得到如下的代码：vals := make([]int, 5) for i := 0; i < 5; i++ { vals[i] = i } fmt.Println(vals) 在 Go Playground 中运行在这种情况下，我们设置的值恰好与我们要使用的索引相同，但是你也可以独立跟踪索引。比如，如果你想要获取 map 的键，你可以使用下面的代码。package main import "fmt" func main() { fmt.Println(keys(map[string]struct{}{ "dog": struct{}{}, "cat": struct{}{}, })) } func keys(m map[string]struct{}) []string { ret := make([]string, len(m)) i := 0 for key := range m { ret[i] = key i++ } return ret } 在 Go Playground 中运行这样做很好，因为我们知道我们返回的切片的长度将与 map 的长度相同，因此我们可以用该长度初始化我们的切片，然后将每个元素分配到适当的索引中。这种方法的缺点是我们必须跟踪 i，以便了解每个索引要设置的值。这就让我们引出了第二种方法……使用 0 作为你的长度并指定容量与其跟踪我们要添加的值的索引，我们可以更新我们的 make 调用，并在切片类型之后提供两个参数。第一个，我们的新切片的长度将被设置为 0，因为我们还没有添加任何新的元素到切片中。第二个，我们新切片的容量将被设置为 map 参数的长度，因为我们知道我们的切片最终会添加许多字符串。这会如前面的例子那样仍旧会在背后构建相同的数组，但是现在当我们调用 append 时，它会将它们放在切片开始处，因为切片的长度是 0。package main import "fmt" func main() { fmt.Println(keys(map[string]struct{}{ "dog": struct{}{}, "cat": struct{}{}, })) } func keys(m map[string]struct{}) []string { ret := make([]string, 0, len(m)) for key := range m { ret = append(ret, key) } return ret } 在 Go Playground 中运行如果 append 处理它，为什么我们还要担心容量呢？接下来你可能会问：“如果 append 函数可以为我增加切片的容量，那我们为什么要告诉程序容量呢？”事实是，在大多数情况下，你不必担心这太多。如果它使你的代码变得更复杂，只需用 var vals []int 初始化你的切片，然后让 append 函数处理接下来的事。但这种情况是不同的。它并不是声明容量困难的例子，实际上这很容易确定我们的切片的最后容量，因为我们知道它将直接映射到提供的 map 中。因此，当我们初始化它时，我们可以声明切片的容量，并免于让我们的程序执行不必要的内存分配。如果要查看额外的内存分配情况，请在 Go Playground 上运行以下代码。每次增加容量，程序都需要做一次内存分配。package main import "fmt" func main() { fmt.Println(keys(map[string]struct{}{ "dog": struct{}{}, "cat": struct{}{}, "mouse": struct{}{}, "wolf": struct{}{}, "alligator": struct{}{}, })) } func keys(m map[string]struct{}) []string { var ret []string fmt.Println(cap(ret)) for key := range m { ret = append(ret, key) fmt.Println(cap(ret)) } return ret } 在 Go Playground 中运行现在将此与相同的代码进行比较，但具有预定义的容量。package main import "fmt" func main() { fmt.Println(keys(map[string]struct{}{ "dog": struct{}{}, "cat": struct{}{}, "mouse": struct{}{}, "wolf": struct{}{}, "alligator": struct{}{}, })) } func keys(m map[string]struct{}) []string { ret := make([]string, 0, len(m)) fmt.Println(cap(ret)) for key := range m { ret = append(ret, key) fmt.Println(cap(ret)) } return ret } 在 Go Playground 中运行在第一个代码示例中，我们的容量从 0 开始，然后增加到 1、 2、 4, 最后是 8，这意味着我们不得不分配 5 次数组，最后一个容纳我们切片的数组的容量是 8，这比我们最终需要的要大。另一方面，我们的第二个例子开始和结束都是相同的容量（5），它只需要在 keys() 函数的开头分配一次。我们还避免了浪费任何额外的内存，并返回一个能放下这个数组的完美大小的切片。不要过分优化如前所述，我通常不鼓励任何人做这样的小优化，但如果最后大小的效果真的很明显，那么我强烈建议你尝试为切片设置适当的容量或长度。这不仅有助于提高程序的性能，还可以通过明确说明输入的大小和输出的大小之间的关系来帮助澄清你的代码。总结你好！我写了很多关于Go、Web 开发和其他我觉得有趣的话题。如果你想跟上最新的文章，请注册我的邮件列表。我会给你发送我新书的样例、Go 的 Web 开发、以及每当有新文章（通常每周 1-2 次）会给你发送邮件。哦，我保证不会发垃圾邮件。我像你一样讨厌它 :)本文并不是对切片或数组之间差异的详细讨论，而是简要介绍了容量和长度如何影响切片，以及它们在方案中的用途。为了进一步阅读，我强烈推荐 Go 博客中的以下文章：Go Slices：使用及内部数组、切片（和字符串）：“append” 的机制切片技巧作者简介：Jon 是一名软件顾问，也是 《Web Development with Go》 一书的作者。在此之前，他创立了 EasyPost，一家 Y Combinator 支持的创业公司，并在 Google 工作。https://www.usegolang.comvia: https://www.calhoun.io/how-to-use-slice-capacity-and-length-in-go作者：Jon Calhoun 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
嗨，在本教程中，我们将学习如何使用 docker 部署 golang web 应用程序。 你可能已经知道，由于 golang 的高性能和可靠性，docker 是完全是用 golang 写的。在我们详细介绍之前，请确保你已经安装了 docker 以及 golang 并对它们有基本了解。关于 dockerDocker 是一个开源程序，它可以将应用及其完整的依赖包捆绑到一起，并打包为容器，与宿主机共享相同的 Linux 内核。另一方面，像 VMware 这样的基于 hypervisor 的虚拟化操作系统容器提供了高级别的隔离和安全性，这是由于客户机和主机之间的通信是通过 hypervisor 来实现的，它们不共享内核空间。但是硬件仿真也导致了性能的开销，所以容器虚拟化诞生了，以提供一个轻量级的虚拟环境，它将一组进程和资源与主机以及其它容器分组及隔离，因此，容器内部的进程无法看到容器外部的进程或资源。用 Go 语言创建一个 “Hello World” web 应用首先我们为 Go 应用创建一个目录，它会在浏览器中显示 “Hello World”。创建一个 web-app 目录并使它成为当前目录。进入 web-app 应用目录并编辑一个名为 main.go 的文件。root@demohost:~# mkdir web-app root@demohost:~# cd web-app/ root@demohost:~/web-app# vim.tiny main.go package main import ( "fmt" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello %s", r.URL.Path[1:]) } func main() { http.HandleFunc("/World", handler) http.ListenAndServe(":8080", nil) } 使用下面的命令运行上面的 “Hello World” Go 程序。在浏览器中输入 http://127.0.0.1:8080/World 测试，你会在浏览器中看到 “Hello World”。root@demohost:~/web-app# PORT=8080 go run main.go 下一步是将上面的应用在 docker 中容器化。因此我们会创建一个 dockerfile 文件，它会告诉 docker 如何容器化我们的 web 应用。root@demohost:~/web-app# vim.tiny Dockerfile # 得到最新的 golang docker 镜像 FROM golang:latest # 在容器内部创建一个目录来存储我们的 web 应用，接着使它成为工作目录。 RUN mkdir -p /go/src/web-app WORKDIR /go/src/web-app # 复制 web-app 目录到容器中 COPY . /go/src/web-app # 下载并安装第三方依赖到容器中 RUN go-wrapper download RUN go-wrapper install # 设置 PORT 环境变量 ENV PORT 8080 # 给主机暴露 8080 端口，这样外部网络可以访问你的应用 EXPOSE 8080 # 告诉 Docker 启动容器运行的命令 CMD ["go-wrapper", "run"] 构建/运行容器使用下面的命令构建你的 Go web-app，你会在成功构建后获得确认。root@demohost:~/web-app# docker build --rm -t web-app . Sending build context to Docker daemon 3.584 kB Step 1 : FROM golang:latest latest: Pulling from library/golang 386a066cd84a: Already exists 75ea84187083: Pull complete 88b459c9f665: Pull complete a31e17eb9485: Pull complete 1b272d7ab8a4: Pull complete eca636a985c1: Pull complete 08158782d330: Pull complete Digest: sha256:02718aef869a8b00d4a36883c82782b47fc01e774d0ac1afd434934d8ccfee8c Status: Downloaded newer image for golang:latest ---> 9752d71739d2 Step 2 : RUN mkdir -p /go/src/web-app ---> Running in 9aef92fff9e8 ---> 49936ff4f50c Removing intermediate container 9aef92fff9e8 Step 3 : WORKDIR /go/src/web-app ---> Running in 58440a93534c ---> 0703574296dd Removing intermediate container 58440a93534c Step 4 : COPY . /go/src/web-app ---> 82be55bc8e9f Removing intermediate container cae309ac7757 Step 5 : RUN go-wrapper download ---> Running in 6168e4e96ab1 + exec go get -v -d ---> 59664b190fee Removing intermediate container 6168e4e96ab1 Step 6 : RUN go-wrapper install ---> Running in e56f093b6f03 + exec go install -v web-app ---> 584cd410fdcd Removing intermediate container e56f093b6f03 Step 7 : ENV PORT 8080 ---> Running in 298e2a415819 ---> c87fd2b43977 Removing intermediate container 298e2a415819 Step 8 : EXPOSE 8080 ---> Running in 4f639a3790a7 ---> 291167229d6f Removing intermediate container 4f639a3790a7 Step 9 : CMD go-wrapper run ---> Running in 6cb6bc28e406 ---> b32ca91bdfe0 Removing intermediate container 6cb6bc28e406 Successfully built b32ca91bdfe0 现在可以运行我们的 web-app 了，可以执行下面的命令。root@demohost:~/web-app# docker run -p 8080:8080 --name="test" -d web-app 7644606b9af28a3ef1befd926f216f3058f500ffad44522c1d4756c576cfa85b 进入 http://localhost:8080/World 浏览你的 web 应用。你已经成功容器化了一个可重复的/确定性的 Go web 应用。使用下面的命令来启动、停止并检查容器的状态。### 列出所有容器 root@demohost:~/ docker ps -a ### 使用 id 启动容器 root@demohost:~/ docker start CONTAINER_ID_OF_WEB_APP ### 使用 id 停止容器 root@demohost:~/ docker stop CONTAINER_ID_OF_WEB_APP 重新构建镜像假设你正在开发 web 应用程序并在更改代码。现在要在更新代码后查看结果，你需要重新生成 docker 镜像、停止旧镜像并运行新镜像，并且每次更改代码时都要这样做。为了使这个过程自动化，我们将使用 docker 卷在主机和容器之间共享一个目录。这意味着你不必为在容器内进行更改而重新构建镜像。容器如何检测你是否对 web 程序的源码进行了更改？答案是有一个名为 “Gin” 的好工具 https://github.com/codegangsta/gin，它能检测是否对源码进行了任何更改，然后重建镜像/二进制文件并在容器内运行更新过代码的进程。要使这个过程自动化，我们将编辑 Dockerfile 并安装 Gin 将其作为入口命令来执行。我们将开放 3030 端口（Gin 代理），而不是 8080。 Gin 代理将转发流量到 web 程序的 8080 端口。root@demohost:~/web-app# vim.tiny Dockerfile # 得到最新的 golang docker 镜像 FROM golang:latest # 在容器内部创建一个目录来存储我们的 web 应用，接着使它称为工作目录。 RUN mkdir -p /go/src/web-app WORKDIR /go/src/web-app # 复制 web 程序到容器中 COPY . /go/src/web-app # 下载并安装第三方依赖到容器中 RUN go get github.com/codegangsta/gin RUN go-wrapper download RUN go-wrapper install # 设置 PORT 环境变量 ENV PORT 8080 # 给主机暴露 8080 端口，这样外部网络可以访问你的应用 EXPOSE 3030 # 启动容器时运行 Gin CMD gin run # 告诉 Docker 启动容器运行的命令 CMD ["go-wrapper", "run"] 现在构建镜像并启动容器：root@demohost:~/web-app# docker build --rm -t web-app . 我们会在当前 web 程序的根目录下运行 docker，并通过暴露的 3030 端口链接 CWD （当前工作目录）到容器中的应用目录下。root@demohost:~/web-app# docker run -p 3030:3030 -v `pwd`:/go/src/web-app --name="test" -d web-app 打开 http://localhost:3030/World， 你就能看到你的 web 程序了。现在如果你改变了任何代码，会在浏览器刷新后反映在你的浏览器中。总结就是这样，我们的 Go web 应用已经运行在 Ubuntu 16.04 Docker 容器中运行了！你可以通过使用 Go 框架来快速开发 API、网络应用和后端服务，从而扩展当前的网络应用。via: http://linoxide.com/containers/setup-go-docker-deploy-application/作者：Dwijadas Dey 译者：geekpi 校对：jasminepeng本文由 LCTT 原创编译，Linux中国 荣誉推出