标签 golang 下的文章

来学习下 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: 潜在的通过解压炸弹实现的 DoS
  • G201: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/md5
  • G502:导入黑名单列表:crypto/des
  • G503:导入黑名单列表:crypto/rc4
  • G504:导入黑名单列表:net/http/cgi
  • G505:导入黑名单列表:crypto/sha1
  • G601: 在 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/MobyKubernetesIstiocontainerdCoreDNS,以及许多其它的。语言纯粹主义者可能会认为一些项目比另外一些更地道,但这些都是很好的起点,可以看到在高度活跃的项目的大型代码库中使用 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 的数组有两个特性:

  1. 数组的长度是固定的;[5]int 是由 5 个 int 构成的数组,和 [3]int 不同。
  2. 数组是值类型。看下面这个示例:
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 中。改变 ba 中的内容没有影响,因为 ab 是相互独立的值。 1

切片

Go 语言的切片和数组的主要有如下两个区别:

  1. 切片没有一个固定的长度。切片的长度不是它类型定义的一部分,而是由切片内部自己维护的。我们可以使用内置的 len 函数知道它的长度。 2
  2. 将一个切片赋值给另一个切片时 不会 对切片内容进行复制操作。这是因为切片没有直接持有其内部数据,而是保留了一个指向 底层数组 3 的指针。数据都保留在底层数组里。

基于第二个特性,两个切片可以享有共同的底层数组。看下面的示例:

  1. 对切片取切片
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]
}

在这个例子里,ab 享有共同的底层数组 —— 尽管 b 在数组里的起始偏移量不同,两者的长度也不同。通过 b 修改底层数组的值也会导致 a 里的值的改变。

  1. 将切片传进函数
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 的形参 vmain 中声明的 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 的独立副本, 而不是 指向 mains 的指针。

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]

你可以注意到在每一个 levels 的值没有被别的 f 的调用影响,尽管当计算更高的 level 时作为 append 的副产品,调用栈内的四个 f 函数创建了四个底层数组 6 ,但是没有影响到当前各自的切片。

扩展阅读

如果你想要了解更多 Go 语言内切片运行的原理,我建议看看 Go 博客里的这些文章:

相关文章:

  1. If a map isn't a reference variable, what is it?
  2. What is the zero value, and why is it useful?
  3. The empty struct
  4. Should methods be declared on T or *T

  1. 这不是数组才有的特性,在 Go 语言里中 一切 赋值都是复制过去的。
  2. 你也可以在对数组使用 len 函数,但是其结果本来就人尽皆知。
  3. 有时也叫做 后台数组 backing array ,以及更不严谨的说法是后台切片。
  4. Go 语言里我们倾向于说值类型以及指针类型,因为 C++ 的 引用 reference 类型这个词产生误会。但在这里我认为调用数组作为引用类型是没有问题的。
  5. 如果你的结构体有定义在其上的方法或者用于满足某个接口,那么你传入结构体指针的比率可以飙升到接近 100%。
  6. 证明留做习题。

via: https://dave.cheney.net/2018/07/12/slices-from-the-ground-up

作者:Dave Cheney 译者:name1e5s 校对:pityonline

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

该教程的完整源代码可以从 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
}

这里我们添加了 alivealiveNext,前一个是细胞当前的专题,后一个是经过计算后下一回合的状态。

现在添加两个函数,我们会用它们来确定 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 的内嵌函数,它会对 XY 坐标做一些重复性的验证。它所做的事情是检查我们传递的数字是否超出了范围——比如说,如果细胞 (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.01.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.01.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

    ...
)

 “Conway's Game of Life” - 示例游戏

试着修改常量,看看它们是怎么影响模拟过程的 —— 这是你用 Go 语言写的第一个 OpenGL 程序,很酷吧?

进阶内容?

这是《OpenGL 与 Go 教程》的最后一节,但是这不意味着到此而止。这里有些新的挑战,能够增进你对 OpenGL (以及 Go)的理解。

  1. 给每个细胞一种不同的颜色。
  2. 让用户能够通过命令行参数指定格子尺寸、帧率、种子和阈值。在 GitHub 上的 github.com/KyleBanks/conways-gol 里你可以看到一个已经实现的程序。
  3. 把格子的形状变成其它更有意思的,比如六边形。
  4. 用颜色表示细胞的状态 —— 比如,在第一帧把存活状态的格子设成绿色,如果它们存活了超过三帧的时间,就变成黄色。
  5. 如果模拟过程结束了,就自动关闭窗口,也就是说所有细胞都消亡了,或者是最后两帧里没有格子的状态有改变。
  6. 将着色器源代码放到单独的文件中,而不是把它们用字符串的形式放在 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中国 荣誉推出

这篇教程的所有源代码都可以在 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),这就变成了像这样的三角形:

Conway's Game of Life  - 右弦三角形

很简单,对吧?现在让我们用两个这样的三角形顶点做成正方形。把 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,
}

注意:你也要把在 maindraw 里面命名的 triangle 改为 square

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

Conway's Game of Life - 两个三角形构成方形

很好,现在我们能够绘制正方形了!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 的切片,这些细胞的数量与列数相等。如果我们把 rowscolumns 都设定成 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.50-0.5 这样的点。如果点小于 0,我们就把它设置成原来的 2 倍(因为 OpenGL 坐标的范围在 -11 之间,范围大小是 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 - 全部格子

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

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

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

cells[2][3].draw()

Conway's Game of Life - 一个单独的细胞

这只绘制坐标在 (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

软件

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

.NET Core MVC

Logo 由 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 MVC

Logo 由 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 ClarkScott 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中国 荣誉推出