Some Go web dev notes

文摘   2024-10-02 19:07   中国台湾  

本文翻译自 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 的发布说明。

参考资料
[1]

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


Go Official Blog
Golang官方博客的资讯翻译及独家解读
 最新文章