一些 Go Web 开发笔记

原文Julia Evans - 2024.09.27

在过去的几周里,我花了很多时间在用 Go 开发一个网站,虽然不知道它最终会不会发布,但在这个过程中我学到了一些东西,想记录下来。以下是我的一些收获:

Go 1.22 现在有了更好的路由支持

我一直没有动力去学习任何 Go 的路由库(比如 gorilla/muxchi 等),所以我一直是手动处理路由的,像这样:

go 复制代码
	// DELETE /records:
	case r.Method == "DELETE" && n == 1 && p[0] == "records":
		if !requireLogin(username, r.URL.Path, r, w) {
			return
		}
		deleteAllRecords(ctx, username, rs, w, r)
	// POST /records/<ID>
	case r.Method == "POST" && n == 2 && p[0] == "records" && len(p[1]) > 0:
		if !requireLogin(username, r.URL.Path, r, w) {
			return
		}
		updateRecord(ctx, username, p[1], rs, w, r)

但显然从 Go 1.22 开始,Go 的标准库现在有了更好的路由支持,因此代码可以像这样重写:

go 复制代码
	mux.HandleFunc("DELETE /records/", app.deleteAllRecords)
	mux.HandleFunc("POST /records/{record_id}", app.updateRecord)

不过它也需要一个登录中间件,所以可能更像这样,使用 requireLogin 作为中间件:

go 复制代码
	mux.Handle("DELETE /records/", requireLogin(http.HandlerFunc(app.deleteAllRecords)))

内置路由的一个坑:带斜杠的重定向

我遇到了一个烦人的问题:如果我为 /records/ 创建了一个路由,那么对 /records 的请求会被重定向/records/

遇到的问题是,当我向 /records 发送 POST 请求时,它会被重定向到对 /records/ 的 GET 请求,这导致 POST 请求的请求体被移除,从而破坏了请求。幸运的是,Xe Iaso 写了一篇关于同样问题的博文,这让我更容易调试这个问题。

我认为解决方案就是使用像 POST /records 这样的 API 端点,而不是 POST /records/,这看起来本身也是一个更常见的设计。

sqlc 自动为我的数据库查询生成代码

我有点厌倦了为我的 SQL 查询写那么多样板代码,但并不想学习 ORM,因为我知道自己想写什么 SQL 查询,也不太想了解 ORM 如何将它们转换为 SQL 查询的规则。

但后来我发现了 sqlc,它可以将像这样的查询:

sql 复制代码
-- name: GetVariant :one
SELECT *
FROM variants
WHERE id = ?;

编译成这样的 Go 代码:

go 复制代码
const getVariant = `-- name: GetVariant :one
SELECT id, created_at, updated_at, disabled, product_name, variant_name
FROM variants
WHERE id = ?
`

func (q *Queries) GetVariant(ctx context.Context, id int64) (Variant, error) {
	row := q.db.QueryRowContext(ctx, getVariant, id)
	var i Variant
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Disabled,
		&i.ProductName,
		&i.VariantName,
	)
	return i, err
}

我喜欢这种方式,因为如果我不确定该为某个 SQL 查询编写什么 Go 代码,我可以直接写出我想要的查询,然后读取生成的函数,它会准确告诉我如何调用它。对我来说,这比翻阅 ORM 文档去搞清楚如何构建我想要的 SQL 查询要容易得多。

阅读了 Brandur 的 2024 年 sqlc 笔记后,我对使用这种方式来处理我的小项目更有信心。那篇文章提供了一个非常有用的案例,展示了如何使用 CASE 语句有条件地更新表中的字段(例如,当你有一个包含 20 列的表并且只想更新其中 3 列时)。

sqlite 小贴士

有人在 Mastodon 上给我发了这篇文章:优化 sqlite 以用于服务器。我的项目很小,对性能没有太多顾虑,但我从中得出的主要结论是:

  • 为数据库的写入 操作准备一个专用的对象,并对它运行 db.SetMaxOpenConns(1)。我通过惨痛的教训了解到,如果不这样做,两个线程同时尝试写入数据库时会出现 SQLITE_BUSY 错误。
  • 如果我想让读取速度更快,可以有两个单独的数据库对象,一个用于写入,一个用于读取。

那篇文章中还有更多看起来有用的建议(比如"COUNT 查询很慢"和"使用 STRICT 表"),不过我还没有尝试这些。

另外,有时如果我有两个表,并且知道永远不需要在它们之间进行 JOIN,我就会把它们放在不同的数据库中,这样就可以独立连接它们。

Go 1.19 引入了一种设置 GC 内存限制的方法

我在内存相对较少的虚拟机(VM)上运行所有 Go 项目,比如 256MB 或 512MB。我遇到了一个问题:应用程序不断被 OOM(内存不足)终止,这让我很困惑------难道我有内存泄漏吗?到底怎么回事?

经过一些谷歌搜索,我意识到或许我并没有内存泄漏,只是需要重新配置垃圾收集器!默认情况下(根据Go 垃圾收集器指南),Go 的垃圾收集器允许应用程序分配的内存达到当前堆大小的 2 倍

Mess With DNS 的基本堆大小大约是 170MB,而 VM 上的可用内存大约只有 160MB,如果内存翻倍,它就会被 OOM 终止。

在 Go 1.19 中,添加了一种方法,可以告诉 Go "嘿,如果应用程序开始使用这么多内存,运行一次 GC"。于是我将 GC 内存限制设置为 250MB,似乎这样做后应用程序被 OOM 终止的次数减少了:

go 复制代码
export GOMEMLIMIT=250MiB

我喜欢用 Go 做网站的几个原因

过去 4 年里,我断断续续地用 Go 做一些小网站(比如 nginx playground),这种方式对我来说很适用。我喜欢它的原因是:

  • 只有一个静态二进制文件,部署时只需复制这个二进制文件。如果有静态文件,我可以用 embed 把它们嵌入到二进制文件里。
  • 内置了一个可以在生产环境中使用的 web 服务器,所以我不需要配置 WSGI 等东西来让它工作。我可以把它放在 Caddy 后面,或者直接在 fly.io 上运行。
  • Go 的工具链非常容易安装,我只需 apt-get install golang-go 之类的命令,然后 go build 就能构建我的项目。
  • 开始发送 HTTP 响应所需记住的东西非常少------基本上就是一些像 Serve(w http.ResponseWriter, r *http.Request) 这样的函数,读取请求并发送响应。如果我需要记住某个具体细节,只需要查看这个函数就可以了!
  • 而且 net/http 是标准库的一部分,所以你不需要安装任何库就可以开始创建网站。我真的很喜欢这一点。
  • Go 是一个比较系统层面的语言,所以如果我需要运行像 ioctl 之类的操作,也很容易做到。

总的来说,它给我的感觉是,Go 让项目变得容易上手,你可以用 5 天时间开发一个项目,放下 2 年,然后再捡起来写代码也不会有太多问题。

相比之下,我尝试学习 Rails 几次了,我真的喜欢 Rails------我用 Rails 做过几个小型网站,每次都觉得非常神奇。但最终每次我回到这些项目时,我都不记得任何东西是如何工作的,最后只能放弃。相比之下,虽然我的 Go 项目里充满了很多重复的样板代码,但至少我可以读懂代码,搞清楚它是怎么工作的。

我还没有搞明白的事情

一些我在 Go 中还没有做过的事情:

  • 渲染 HTML 模板:通常我的 Go 服务器只是 API,我会用 Vue 做前端单页应用。我在 Hugo 中大量使用过 html/template(过去 8 年我一直用 Hugo 写这个博客),但我还不确定对它的感觉。
  • 我从没做过真正的登录系统,通常我的服务器根本不需要用户。
  • 我从没尝试过实现 CSRF(跨站请求伪造)。

总的来说,我不确定如何实现安全敏感的功能,所以我不会启动那些需要登录/CSRF 等功能的项目。我猜这可能是框架派上用场的地方。

很高兴看到 Go 添加的新功能

我在这篇文章中提到的两个 Go 功能(GOMEMLIMIT 和路由)都是过去几年里添加的,而我在它们发布时没有注意到。这让我觉得应该更密切关注 Go 新版本的发布说明。

相关推荐
Pandaconda2 分钟前
【Golang 面试题】每日 3 题(三十九)
开发语言·经验分享·笔记·后端·面试·golang·go
加油,旭杏6 分钟前
【go语言】变量和常量
服务器·开发语言·golang
行路见知7 分钟前
3.3 Go 返回值详解
开发语言·golang
编程小筑37 分钟前
R语言的编程范式
开发语言·后端·golang
技术的探险家40 分钟前
Elixir语言的文件操作
开发语言·后端·golang
ss2731 小时前
【2025小年源码免费送】
前端·后端
Ai 编码助手1 小时前
Golang 中强大的重试机制,解决瞬态错误
开发语言·后端·golang
齐雅彤2 小时前
Lisp语言的区块链
开发语言·后端·golang
齐雅彤2 小时前
Lisp语言的循环实现
开发语言·后端·golang
梁雨珈2 小时前
Lisp语言的物联网
开发语言·后端·golang