本文翻译自 Julia Evans 的 https://jvns.ca/blog/2024/09/27/some-go-web-dev-notes/
一些 Go Web 开发笔记
在过去的几周里,我花了很多时间用 Go 语言开发了一个网站,这个网站可能永远不会公开,但在此过程中我学到了一些我想记下来的东西。以下是我的一些心得:
Go 1.22 现在支持更好的路由功能
我从来没有动力去学习任何 Go 路由库(gorilla/mux、chi等),所以我一直在手动做所有的路由,像这样:
// 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在标准库中对路由有了更好的支持,所以上面的代码可以重写为:
mux.HandleFunc("DELETE /records/", app.deleteAllRecords)
mux.HandleFunc("POST /records/{record_id}", app.updateRecord)
尽管它还需要一个登录中间件,所以可能更像是这样,使用requireLogin中间件:
mux.Handle("DELETE /records/", requireLogin(http.HandlerFunc(app.deleteAllRecords)))
内置路由器的一个陷阱:带有斜杠的重定向
我遇到的一个令人烦恼的问题是:如果我为/records/
设置了一个路由,那么对/records
的请求将被重定向到[1]/records/
。
我遇到的一个问题是,发送POST请求到/records
被重定向到了GET请求/records/
,这破坏了POST请求,因为它去除了请求体。
我认为解决这个问题的方法就是使用像POST /records
这样的API端点,而不是POST /records/
,这似乎无论如何都是更正常的设计。
sqlc自动生成我的数据库查询代码
我对编写大量 SQL 查询的样板代码感到有些厌烦,但我并不想学习 ORM,因为我知道我想要编写什么样的SQL查询,我不想学习 ORM 的约定来将它们转换成 SQL 查询。
但后来我发现了 sqlc[2],它可以将这样的查询:
-- name: GetVariant :one
SELECT *
FROM variants
WHERE id = ?;
编译成这样的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 笔记[3]也给了我一些信心,认为这是我的小型程序可行的路径。那篇文章给出了一个非常有帮助的例子,展示了如何使用 CASE 语句有条件地更新表中的字段(例如,如果你有一个有20 列的表,而你只想更新其中的3列)。
sqlite 提示
有人在 Mastodon 上给我链接了一篇名为 Optimizing sqlite for servers[4]的文章。我的项目很小,我不太关心性能,但我的主要收获是:
有一个专用的对象用于写入数据库,并在它上面运行db.SetMaxOpenConns(1)
。我痛苦地学到,如果我不这样做,那么我将从两个试图同时写入数据库的线程那里得到SQLITE_BUSY错误。
如果我想加快读取速度,我可以有两个单独的数据库对象,一个用于写入,一个用于读取。
那篇文章中还有更多有用的技巧(比如“COUNT查询很慢”和“使用STRICT表”),但我还没有做。
另外,有时如果我有两个表,我知道我永远不需要在它们之间进行JOIN,那么我就把它们放在不同的数据库中,这样我就可以独立地连接到它们。
Go 1.19引入了设置GC内存限制的方法
我所有的 Go 项目都是在内存相对较少的VM中运行的,比如 256MB 或 512MB。我遇到的一个问题是我的应用程序不断被 OOM 杀死,这很令人困惑 - 我有内存泄漏吗?是什么?
经过一些 Google 搜索,我意识到也许我没有内存泄漏,也许我只是需要重新配置垃圾收集器!事实证明,默认情况下(根据 Go垃圾收集器指南[5]),Go的垃圾收集器将允许应用程序分配内存,直到达到当前堆大小的2倍。
Mess With DNS 的基础堆大小大约是 170MB,VM 上当前的空闲内存大约是 160MB,所以如果它的内存翻倍,它将被 OOM 杀死。
在 Go 1.19 中,他们增加了一种方法来告诉 Go "嘿,如果应用程序开始使用这么多内存,就运行 GC"。所以我将 GC 内存限制设置为250MB,它似乎使得应用程序被 OOM 杀死的次数减少了:
export GOMEMLIMIT=250MiB
我喜欢用 Go 制作网站的一些原因
在过去的 4 年左右的时间里,我断断续续地用 Go 制作了一些小型网站(像 [nginx playground](https://nginx-playground.wizardzines.com/ nginx playground)),它确实非常适合我。我认为我喜欢它是因为:
只有一个静态二进制文件,我所需要做的就是复制二进制文件来部署它。如果有静态文件,我可以只用 embed[6]将它们嵌入到二进制文件中。
有一个内置的 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 或类似的东西,那很容易做到。
总的来说,它给我的感觉是,它使得项目很容易在5天内工作,放下你的代码 2 年后,然后回到编写代码中,而不会有太多问题。
相比之下,我尝试过几次学习 Rails,我真的想喜欢 Rails - 我制作了几个 Rails 的玩具网站,它总是给人一种非常神奇的体验。但最终当我回到那些项目时,我记不得任何事情是如何工作的,我只是最终放弃了。对我来说,回到我的 Go项目更容易,这些项目充满了大量的重复样板代码,因为至少我可以阅读代码并弄清楚它是如何工作的。
我还没有弄清楚的事情
在 Go 中我还没有做太多的事情:
渲染 HTML 模板:通常我的 Go 服务器只是 API,我将前端制作成 Vue 的单页应用。我在 Hugo(我已经用了 8 年的博客)中使用了很多 html/template,但我仍然不确定我对它的感觉如何。
我从未制作过真正的登录系统,通常我的服务器根本没有用户。
我从未尝试过实现CSRF
总的来说,我不确定如何实现对安全敏感的特性,所以我不开始需要登录/CSRF等的项目。我想这就是框架会有所帮助的地方。
看到 Go 一直在添加的新功能很酷
我在这篇文章中提到的两个 Go 特性(GOMEMLIMIT和路由)都是在过去几年中新增的,我没有注意到它们何时出现的。这让我想到我应该更密切地关注新版本的 Go 的发布说明。
hdr-Trailing_slash_redirection-ServeMux: https://pkg.go.dev/net/http#hdr-Trailing_slash_redirection-ServeMux
[2]sqlc: https://sqlc.dev/
[3]sql-2024: https://brandur.org/fragments/sqlc-2024
[4]sqllite for servers: https://kerkour.com/sqlite-for-servers
[5]gc guide: https://tip.golang.org/doc/gc-guide
[6]embed: https://pkg.go.dev/embed