当前位置: 首页 > news >正文

13 年后,我如何用 Go 编写 HTTP 服务(译)

在这里插入图片描述

原文:Mat Ryer - 2024.02.09

大约六年前,我写了一篇博客文章,概述了我是如何用 Go 编写 HTTP 服务的,现在我再次告诉你,我是如何写 HTTP 服务的。

那篇原始的文章引发了一些热烈的讨论,这些讨论影响了我今天的做事方式。在主持 Go Time podcast、在X/Twitter上讨论 Go 以及通过多年的代码维护经验后,我认为是时候进行一次更新了。

(对于那些注意到 Go 并不完全有 13 年历史的吹毛求疵者,我开始用 Go 编写 HTTP 服务是在 version .r59。)

这篇文章涵盖了一系列与在 Go 中构建服务相关的主题,包括:

  • 为了最大化的可维护性,如何结构化 servers 和 handlers
  • 优化快速启动和优雅关闭的技巧和窍门
  • 如何处理适用于多种类型请求的常见工作
  • 如何深入测试你的服务

从小项目到大项目,这些实践对我来说经受住了时间的考验,我希望它们也能对你有所帮助。

这篇文章适合谁?

这篇文章适合你。它适合所有打算用 Go 写某种 HTTP 服务的人。如果你正在学习 Go,你可能也会发现这篇文章很有用,因为很多例子都遵循了良好的实践。经验丰富的 gopher 也可能会发现一些不错的模式。

要想最大限度地利用这篇文章,你需要了解 Go 的基础知识。如果你觉得自己还没有达到这个水平,强烈推荐你阅读 Chris James 的通过测试学习 Go。如果你想听到更多来自 Chris 的内容,可以查看我们和 Ben Johnson 在 The files & folders of Go projects上做的 Go Time 的一期节目。

如果你熟悉这篇文章的前几个版本,这一节包含了现在有什么不同的快速总结。如果你想从头开始,请跳到下一节。

  1. 我的 handler 过去是挂在 server 结构体上的方法,但现在不再这么做了。如果一个 handler 函数需要一个依赖项,它可以很好地将其作为参数请求。当你只是试图测试一个单独的 handler 时,不再有意外的依赖项。
  2. 我过去更喜欢http.HandlerFunc而不是http.Handler —— 足够多的第三方库首先考虑的是http.Handler,所以接受这个事实是有意义的。http.HandlerFunc仍然非常有用,但现在大部分东西都被表示为接口类型。无论哪种方式,差别都不大。
  3. 我增加了更多关于测试的内容,包括一些观点 ™。
  4. 我增加了更多的章节,所以建议每个人都全文阅读。

(译者注:第 3 点结尾的 TradeMark 商标缩写,是一种幽默的说法,意味着作者对测试的观点是独特的,可能有些争议,但他自己非常坚信。)

NewServer构造函数

让我们从查看任何 Go 服务的核心开始:server 。NewServer函数创建主http.Handler。通常我每个服务有一个,依赖 HTTP 路由将流量引导到每个服务内的正确 handler ,因为:

  • NewServer是一个大的构造函数,它接受所有依赖项作为参数
  • 如果可能,它会返回一个http.Handler,这可以是一个专用类型,用于处理更复杂的情况
  • 它通常配置自己的 muxer(复用器),并调用routes.go

例如,你的代码可能看起来类似这样:

func NewServer(logger *Loggerconfig *ConfigcommentStore *commentStoreanotherStore *anotherStore
) http.Handler {mux := http.NewServeMux()addRoutes(mux,Logger,Config,commentStore,anotherStore,)var handler http.Handler = muxhandler = someMiddleware(handler)handler = someMiddleware2(handler)handler = someMiddleware3(handler)return handler
}

在不需要所有依赖项的测试用例中,我传入nil作为一个标识,表示它不会被使用。

NewServer构造函数负责所有适用于所有 API 端点的顶级 HTTP 事务,如 CORS、auth 中间件和日志:

var handler http.Handler = mux
handler = logging.NewLoggingMiddleware(logger, handler)
handler = logging.NewGoogleTraceIDMiddleware(logger, handler)
handler = checkAuthHeaders(handler)
return handler

server 通常是使用 Go 的内置http包来暴露它:

srv := NewServer(logger,config,tenantsStore,slackLinkStore,msteamsLinkStore,proxy,
)
httpServer := &http.Server{Addr:    net.JoinHostPort(config.Host, config.Port),Handler: srv,
}
go func() {log.Printf("listening on %s\n", httpServer.Addr)if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {fmt.Fprintf(os.Stderr, "error listening and serving: %s\n", err)}
}()
var wg sync.WaitGroup
wg.Add(1)
go func() {defer wg.Done()<-ctx.Done()if err := httpServer.Shutdown(ctx); err != nil {fmt.Fprintf(os.Stderr, "error shutting down http server: %s\n", err)}
}()
wg.Wait()
return nil

长参数列表

必须有一个限制点,到达这个点后,它就不再是正确的做法,但大多数时候,我都很乐意将依赖项列表作为参数添加进来。虽然它们有时会变得相当长,但我发现这仍然是值得的。

是的,它让我免于创建一个结构体,但真正的好处是,从参数中得到了稍微更多的类型安全性。我可以创建一个结构体,跳过不喜欢的任何字段,但函数强制我必须这样做。必须查找字段以知道如何在结构体中设置它们,而如果不传递正确的参数,我就无法调用函数。

如果你将它格式化为一个垂直列表,就像我在现代前端代码中看到的那样,那么它并不那么糟糕:

srv := NewServer(logger,config,tenantsStore,commentsStore,conversationService,chatGPTService,
)

routes.go中映射整个 API surface

这个文件是你的服务中所有路由都列出的地方。

有时候你无法避免让事情在一定程度上散布开来,但能够在每个项目中去一个文件中查看其 API surface 是非常有帮助的。

由于NewServer构造函数中的大量依赖项参数列表,通常会在你的路由函数中遇到相同的列表。但再次,这并不那么糟糕。如果你忘记了什么或者顺序错了,由于 Go 的类型检查,你很快就会知道。

func addRoutes(mux                 *http.ServeMux,logger              *logging.Logger,config              Config,tenantsStore        *TenantsStore,commentsStore       *CommentsStore,conversationService *ConversationService,chatGPTService      *ChatGPTService,authProxy           *authProxy
) {mux.Handle("/api/v1/", handleTenantsGet(logger, tenantsStore))mux.Handle("/oauth2/", handleOAuth2Proxy(logger, authProxy))mux.HandleFunc("/healthz", handleHealthzPlease(logger))mux.Handle("/", http.NotFoundHandler())
}

在我的例子中,addRoutes不返回错误。任何可能抛出错误的事情都被移动到run函数中,并在到达这一点之前解决,使这个函数保持简单和扁平。当然,如果你的任何 handler 因为某种原因返回错误,那么好的,这个也可以返回错误。

func main()只调用run()

run函数就像main函数,除了它将操作系统的基本功能作为参数,并返回(你猜对了)一个错误。

我希望func main()func main() error。或者像 C 语言那样,可以返回退出代码:func main() int。通过拥有一个超级简单的 main 函数,也可以实现你的梦想:

func run(ctx context.Context, w io.Writer, args []string) error {// ...
}func main() {ctx := context.Background()ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)defer cancel()if err := run(ctx, os.Stdout, os.Args); err != nil {fmt.Fprintf(os.Stderr, "%s\n", err)os.Exit(1)}
}

上述代码创建了一个 context,当Ctrl+C或等效操作被调用时,它会被取消,并调用run函数。如果run返回nil,函数正常退出。如果返回一个错误,我们将其写入 stderr 并以非零代码退出。如果正在写一个命令行工具,其中退出代码很重要,我会返回一个 int,这样就可以编写测试来断言返回了正确的代码。

操作系统的基础内容可以作为参数传入run。例如,你可能会传入os.Args(如果它支持 flag),甚至os.Stdinos.Stdoutos.Stderr依赖项。这使得你的程序更容易测试,因为测试代码可以调用run来执行,通过传递不同的参数控制参数和所有流(streams)。

以下表格显示了运行函数的输入参数的示例:

类型描述
os.Args[]string执行程序时传入的参数。它也用于解析 flags。
os.Stdinio.Reader用于读取输入
os.Stdoutio.Writer用于写入输出
os.Stderrio.Writer用于写入错误日志
os.Getenvfunc(string) string用于读取环境变量
os.Getwdfunc() (string, error)获取工作目录

如果你远离任何全局范围的数据,通常就可以在更多的地方使用t.Parallel(),以加速测试套件(test suites)。所有的东西都是自包含的,所以多次调用run不会相互干扰。

我经常会写这样的run函数声明:

func run(ctx    context.Context,args   []string,getenv func(string) string,stdin  io.Reader,stdout, stderr io.Writer,
) error

现在我们在run函数内部,可以编写正常的 Go 代码,可以随心的返回错误。我们 gophers 就喜欢返回错误,越早承认这一点,那些在互联网上的人就可以赢得胜利并离开。

优雅地关闭

如果你正在运行大量的测试,那么当每一个都完成时,程序停止是很重要的。(或者,你可能决定为所有的测试保持一个实例运行,但那取决于你。)

context 被传递下去。如果程序收到了终止信号,它就会被取消,所以在每个层级都要重视它。至少,将它传递给你的依赖项。最好在任何长时间运行或循环的代码中,检查Err()方法,如果它返回一个错误,停止正在做的事情并将其返回。这将帮助 server 优雅地关闭。如果你启动了其他的 goroutines,也可以使用 context 来决定是否停止它们。

控制环境

argsgetenv参数为我们提供了几种通过 flags 和环境变量控制程序行为的方式。flags 是通过 args 进行处理的(只要你不使用全局 flags,而是在run内部使用flags.NewFlagSet),所以我们可以通过不同的值来调用run

args := []string{"myapp","--out", outFile,"--fmt", "markdown",
}
go run(ctx, args, etc.)

如果你的程序优先使用环境变量而不是 flags(或者两者都用),那么getenv函数允许你插入不同的值,而不用改变实际的env

getenv := func(key string) string {switch key {case "MYAPP_FORMAT":return "markdown"case "MYAPP_TIMEOUT":return "5s"default:return ""
}
go run(ctx, args, getenv)

对我来说,使用这种getenv技术比使用t.SetEnv来控制环境变量更好,因为可以继续并行运行测试,通过调用t.Parallel(),而t.SetEnv不允许这样做。

这种技术在编写命令行工具时尤其有用,因为你经常想要以不同的配置来测试所有的程序行为。

main函数中,我们可以传入真实的内容:

func main() {ctx := context.Background()ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)defer cancel()if err := run(ctx, os.Getenv, os.Stderr); err != nil {fmt.Fprintf(os.Stderr, "%s\n", err)os.Exit(1)}
}

Maker funcs 返回 handler

我的 handler 函数不直接实现http.Handlerhttp.HandlerFunc,而是返回自身。具体来说,它们返回http.Handler类型。

// handleSomething handles one of those web requests
// that you hear so much about.
func handleSomething(logger *Logger) http.Handler {thing := prepareThing()return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {// use thing to handle requestlogger.Info(r.Context(), "msg", "handleSomething")})
}

这种模式为每个 handler 提供了自己的闭包环境。你可以在这个空间中做一些初始化工作,当 handler 被调用时,数据将可用。

确保只读取共享数据。如果 handler 修改任何东西,你将需要一个互斥锁或其他东西来保护它。

在这里存储程序状态通常不是你想要的。在大多数云环境中,不能相信代码会在长时间内继续运行。根据你的生产环境,servers 通常会关闭以节省资源,甚至因为其他原因崩溃。也可能有许多服务实例正在运行,请求在它们之间以不可预测的方式负载均衡。在这种情况下,一个实例只能访问自己的本地数据。所以在真实项目中,最好使用数据库或其他存储 API 来持久化数据。

在一个地方处理解码/编码

每个服务都需要解码请求体和编码响应体。这是一个经得起时间考验的明智的抽象。

我通常有一对叫做 encode 和 decode 的辅助函数。一个使用泛型的例子向你展示了只包装了一些基本的代码,我通常不会这样做,然而当你需要为所有 API 在这里做出改变时,这变得有用。(例如,假设你有一个新老板被困在 90 年代,他们想添加 XML 支持。)

func encode[T any](w http.ResponseWriter, r *http.Request, status int, v T) error {w.WriteHeader(status)w.Header().Set("Content-Type", "application/json")if err := json.NewEncoder(w).Encode(v); err != nil {return fmt.Errorf("encode json: %w", err)}return nil
}func decode[T any](r *http.Request) (T, error) {var v Tif err := json.NewDecoder(r.Body).Decode(&v); err != nil {return v, fmt.Errorf("decode json: %w", err)}return v, nil
}

有趣的是,编译器能够从参数中推断出类型,所以你不需要在调用 encode 时传递它:

err := encode(w, r, http.StatusOK, obj)

但由于它是 decode 中的返回参数,需要指定你期望的类型:

decoded, err := decode[CreateSomethingRequest](r)

尽量不要过度使用这些函数,但之前我对一个简单的验证接口非常满意,它很好地融入了 decode 函数。

验证数据

我喜欢一个简单的接口。实际上,非常喜欢它们。单方法接口非常容易实现。所以当涉及到验证对象时,我喜欢这样做:

// Validator is an object that can be validated.
type Validator interface {// Valid checks the object and returns any// problems. If len(problems) == 0 then// the object is valid.Valid(ctx context.Context) (problems map[string]string)
}

Valid方法接受一个 context(这是可选的,但过去对我有用)并返回一个 map。如果一个字段有问题,它的名字被用作键,一个详细解释被设置为值。

该方法可以做任何需要验证结构字段的事情。例如,它可以检查确保:

  • 必需的字段不为空
  • 具有特定格式(如电子邮件)的字符串是正确的
  • 数字在可接受的范围内

如果你需要做任何更复杂的事情,比如在数据库中检查字段,那应该在其他地方进行;它可能太重要了,不能被视为一个快速的验证检查,而且你不希望在这样的函数中找到那种东西,所以它可能会很容易被隐藏起来。

然后我使用类型断言来看对象是否实现了接口。或者,在泛型世界中,可能会选择更明确地说明正在发生什么事情,通过改变 decode 方法来实现那个接口。

func decodeValid[T Validator](r *http.Request) (T, map[string]string, error) {var v Tif err := json.NewDecoder(r.Body).Decode(&v); err != nil {return v, nil, fmt.Errorf("decode json: %w", err)}if problems := v.Valid(r.Context()); len(problems) > 0 {return v, problems, fmt.Errorf("invalid %T: %d problems", v, len(problems))}return v, nil, nil
}

在这段代码中,T必须实现Validator接口,并且Valid方法必须空 map,才能认为对象被成功解码。

对于校验的错误,返回nil是安全的,因为我们将检查len(problems),对于nil映射,它将是0,不会引发 panic。

中间件的适配器模式

中间件函数(Middleware functions)接受http.Handler参数并返回一个新的http.Handler,它可以在调用原始 handler 之前和/或之后运行代码 —— 或者根本不调用原始 handler 。

一个例子是检查用户是否是管理员:

func (s *server) adminOnly(h http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {if !currentUser(r).IsAdmin {http.NotFound(w, r)return}h.ServeHTTP(w, r)})
}

handler 内部的逻辑可以选择是否调用原始 handler 。在上述例子中,如果IsAdmin为假, handler 将返回HTTP 404 Not Found并返回(或中止);注意,h 处理器没有被调用。如果IsAdmin为真,用户被允许访问路由,因此执行被传递给 h 处理器。

通常我会在routes.go文件中列出中间件:

package appfunc addRoutes(mux *http.ServeMux) {mux.HandleFunc("/api/", s.handleAPI())mux.HandleFunc("/about", s.handleAbout())mux.HandleFunc("/", s.handleIndex())mux.HandleFunc("/admin", s.adminOnly(s.handleAdminIndex()))
}

这使得代码非常清晰,只需查看 API 端点的映射,就可以知道哪个中间件应用于哪些路由。如果列表开始变得越来越大,尝试将它们分布在多行中 —— 我知道,我知道,但你会习惯的。

有时我会返回中间件

上述方法对于简单的情况非常好,但如果中间件需要大量的依赖项(一个 logger,一个 database,一些 API clients,一个包含“Never Gonna Give You Up”数据的数组,用于以后的恶作剧),那我可能会有一个返回中间件的函数。

问题是,你最终会得到这样的代码:

mux.Handle("/route1", middleware(logger, db, slackClient, rroll []byte, handleSomething(handlerSpecificDeps))
mux.Handle("/route2", middleware(logger, db, slackClient, rroll []byte, handleSomething2(handlerSpecificDeps))
mux.Handle("/route3", middleware(logger, db, slackClient, rroll []byte, handleSomething3(handlerSpecificDeps))
mux.Handle("/route4", middleware(logger, db, slackClient, rroll []byte, handleSomething4(handlerSpecificDeps))

这会使代码膨胀,而且没有提供任何有用的东西。相反,我会让中间件函数接受依赖项,然后返回一个函数,该函数只接受下一个 handler。

func newMiddleware(logger Logger,db *DB,slackClient *slack.Client,rroll []byte,
) func(h http.Handler) http.Handler

返回类型func(h http.Handler) http.Handler是我们在设置路由时将调用的函数。

middleware := newMiddleware(logger, db, slackClient, rroll)
mux.Handle("/route1", middleware(handleSomething(handlerSpecificDeps))
mux.Handle("/route2", middleware(handleSomething2(handlerSpecificDeps))
mux.Handle("/route3", middleware(handleSomething3(handlerSpecificDeps))
mux.Handle("/route4", middleware(handleSomething4(handlerSpecificDeps))

有些人(但不是我)喜欢将该函数作为类型定义,就像这样:

// middleware is a function that wraps http.Handlers
// proving functionality before and after execution
// of the h handler.
type middleware func(h http.Handler) http.Handler

这样做是可以的。如果你喜欢,就这样做。我不会来到你的工作地点,等你出来,然后用我的手臂搭在你的肩膀上以一种恐吓的方式走在你旁边,问你是否对自己感到满意。

我不这样做的原因是,它增加了一个额外的间接级别。当你查看上面的newMiddleware函数的声明时,很清楚发生了什么事情。如果返回类型是中间件,你需要做一点额外的工作。从本质上讲,我优化的是阅读代码,而不是编写代码。

隐藏请求/响应类型的机会

如果一个 API 端点有自己的请求和响应类型,通常它们只对该特定 handler 有用。

如果是这样,你可以在函数内部定义它们。

func handleSomething() http.HandlerFunc {type request struct {Name string}type response struct {Greeting string `json:"greeting"`}return func(w http.ResponseWriter, r *http.Request) {...}
}

这样可以保持全局空间清晰,并防止其他 handler 依赖你可能认为不稳定的数据。

当你的测试代码需要使用相同的类型时,有时会遇到这种方法的阻力。公平地说,如果你想这样做,这是一个很好的理由来打破它们。

使用内联请求/响应类型来讲述额外的测试故事

如果请求/响应类型隐藏在 handler 内部,可以在测试代码中声明新的类型。

这是一个讲述故事的机会,对于将来需要理解你代码的人来说。

例如,假设我们在代码中有一个Person类型,并且我们在许多 API 端点上重用它。比如 /greet端点,我们可能只关心他们的名字,可以在测试代码中表达这一点:

func TestGreet(t *testing.T) {is := is.New(t)person := struct {Name string `json:"name"`}{Name: "Mat Ryer",}var buf bytes.Buffererr := json.NewEncoder(&buf).Encode(person)is.NoErr(err) // json.NewEncoderreq, err := http.NewRequest(http.MethodPost, "/greet", &buf)is.NoErr(err)//... more test code here

从这个测试中可以看出,我们关心的唯一字段是Name

sync.Once来推迟配置

如果我在准备 handler 时需要做任何昂贵的工作,我会推迟到该 handler 首次被调用。

这可以改善应用程序的启动时间。

func (s *server) handleTemplate(files string...) http.HandlerFunc {var (init    sync.Oncetpl     *template.Templatetplerr  error)return func(w http.ResponseWriter, r *http.Request) {init.Do(func(){tpl, tplerr = template.ParseFiles(files...)})if tplerr != nil {http.Error(w, tplerr.Error(), http.StatusInternalServerError)return}// use tpl}
}

sync.Once确保代码只执行一次,其他调用(其他人发出的相同请求)将等待直到它完成。

  • 错误检查在init函数之外,如果出现问题,我们仍然会显示错误,并且不会在日志中丢失
  • 如果 handler 没有被调用,昂贵的工作就不会被完成 —— 这可以根据你的代码部署方式带来很大的好处

请记住,通过这样做,你将初始化时间从启动时移动到运行时(当第一次访问 API 端点时)。我经常使用 Google App Engine,所以这对我有意义,但你的情况可能会有所不同,所以值得思考在何处以及何时以这种方式使用sync.Once

为可测试性设计

这些模式的发展部分是因为它们测试代码非常容易。run 函数是直接从测试代码运行程序的简单方法。

在 Go 中进行测试时,你有很多选择,它无关对错,更多的是:

  • 通过查看测试,是否能帮助你理解程序的功能
  • 更改现有代码时,是否不再担心改坏老功能
  • 如果所有的测试都通过,能否推送到生产环境?还是它需要覆盖更多的内容?

单元测试(UT)的单元是什么?

遵循这些模式,handler 本身也可以独立地进行测试,但我倾向于不这样做,这将在下面解释原因。必须考虑什么是你项目的最佳方法。

只测试 handler,你可以:

  1. 调用函数来获取 http.Handler —— 你必须传入所有的依赖项(这是一个特性)。
  2. 使用 http.RequesthttptestResponseRecorder 返回的 http.Handler 调用 ServeHTTP 方法包(参见 https://pkg.go.dev/net/http/httptest#ResponseRecorder)
  3. 对响应进行断言(检查状态码,解码 body 并确保它是正确的,检查任何重要的 headers 等)。

如果这样做,你将裁剪掉像 auth 这样的中间件,直接执行 handler 代码。如果你想要围绕某些特定的复杂性构建一些测试支持,这是很好的。然而,当你的测试代码以与你的用户以相同的方式调用 API 时,这是一个优势。我倾向于在这个级别进行端到端测试,而不是对所有内部的片段进行单元测试。

我宁愿调用 run 函数来尽可能接近生产环境的方式执行整个程序。这将解析任何参数,连接到任何依赖项,迁移数据库,无论在实际运行中它会做什么,最终启动服务器。然后,当我从测试代码中调用 API 时,遍历所有的层级,甚至与一个真实的数据库进行交互,同时也在测试 routes.go

我发现通过这种方法可以更早地发现更多问题,并且可以避免专门测试样板代码 (译者注:比如错误处理、数据库连接、日志记录等代码)。它也减少了我测试中的重复工作。如果认真测试每一层,我可能会以稍微不同的方式多次重复相同的内容。你必须维护所有这些,如果之后你想改变一些代码,更新一个函数和三个测试并不感觉很有成效。使用端到端测试,你只需要有一套描述用户和系统之间交互的主要测试 case。

我仍然在适当的地方使用单元测试。如果使用 TDD(我经常这样做)那么通常已经完成了很多测试,我很乐意维护这些 UT。但如果 UT 与端到端测试重复了相同的内容,我会删除 UT。

这个决定将取决于很多事情,从你周围的人的观点到项目的复杂性,所以就像这篇文章中的所有建议一样,如果它不适合你,不要强行采用。

使用 run 函数进行测试

我喜欢在每个测试中调用 run 函数,它们都有自己的程序实体。对于每个测试,我可以传递不同的参数,flag 值,标准输入和输出流,甚至环境变量。

由于 run 函数接受一个 context.Context,并且由于我们所有的代码都重视上下文(对吧,大家?它重视上下文,是吧?)我们可以通过调用 context.WithCancel 获取一个取消函数。通过推迟执行 cancel 函数,当测试函数返回时(即,测试完成运行)上下文将被取消,程序被优雅地关闭。在 Go 1.14 中,他们添加了 t.Cleanup 方法,这是一个替代你自己使用 defer 关键字的方法,如果你想了解更多原因,请查看此问题:https://github.com/golang/go/issues/37333。

这一切都可以用令人惊讶的少量代码实现。当然,你也必须在各处不断检查 ctx.Errctx.Done

func Test(t *testing.T) {ctx := context.Background()ctx, cancel := context.WithCancel(ctx)t.Cleanup(cancel)go run(ctx)// test code goes here

等待准备就绪

由于 run 函数在一个 goroutine 中执行,我们并不真正知道它何时会启动。如果我们要像真正的用户一样开始使用 API,需要知道它何时准备就绪。

我们可以设置某种方式的准备就绪信号,比如通道或其他 —— 但我更喜欢在服务器上运行一个 /healthz/readyz API 端点。就像我老奶奶常说的,布丁的证明在于实际的 HTTP 请求(她在那的时代遥遥领先)。

(译者注:该处使用了英文谚语 “The proof of the pudding is in the eating” 来比喻,“布丁好不好吃,吃了才知道”)

在这个示例中,我们努力使代码更具可测试性,这使我们能够深入了解用户的需求。他们可能想知道服务是否已准备好,那么为什么不通过官方方式来找出答案呢?

为了等待服务准备就绪,可以写一个循环:

// waitForReady calls the specified endpoint until it gets a 200
// response or until the context is cancelled or the timeout is
// reached.
func waitForReady(ctx context.Context,timeout time.Duration,endpoint string,
) error {client := http.Client{}startTime := time.Now()for {req, err := http.NewRequestWithContext(ctx,http.MethodGet,endpoint,nil,)if err != nil {return fmt.Errorf("failed to create request: %w", err)}resp, err := client.Do(req)if err != nil {fmt.Printf("Error making request: %s\n", err.Error())continue}if resp.StatusCode == http.StatusOK {fmt.Println("Endpoint is ready!")resp.Body.Close()return nil}resp.Body.Close()select {case <-ctx.Done():return ctx.Err()default:if time.Since(startTime) >= timeout {return fmt.Errorf("timeout reached while waiting for endpoint")}// wait a little while between checkstime.Sleep(250 * time.Millisecond)}}
}

把这些都付诸实践

使用这些技术实现简单的 API 仍然是我最喜欢的方式。它符合我的目标,即实现优秀的可维护性,代码易读,易于通过复制模式进行扩展,新人易于使用,易于改变而不用担心,明确地没有任何魔法。即使我使用代码生成框架(如我们自己的 Oto 包)根据自定义的模板为我编写样板代码的情况下,也是如此。

在更大的项目或组织中,特别是像 Grafana Labs 这样的组织,经常会遇到影响这些决策的特定技术选择。gRPC 就是一个很好的例子。在已建立的模式和经验,或其他广泛使用的工具或抽象的情况下,你通常会发现自己做出了与流行方式一致的务实选择,尽管我怀疑(或者是希望?)这篇文章对你仍然有所帮助。

我目前的工作是与 Grafana Labs 内的一群才华横溢的人一起构建新的 Grafana IRM 套件。本文中讨论的模式帮助我们提供人们可以依赖的工具。我听到你在显示器前大喊:“告诉我更多关于这些优秀工具的信息!”。

大多数人使用 Grafana 来可视化他们的系统运行情况,而 Grafana Alerting 则在指标超出边界值时通知他们。使用 Grafana OnCall,你的日程安排和升级规则可以在出现问题时自动联系合适的人员。

Grafana Incident 让你管理那些我们大多数人熟悉的不可避免的全员参与的时刻。它为你创建一个 Zoom 房间讨论问题,一个专门的 Slack 频道,并在你专注于灭火的同时跟踪事件的时间线。在 Slack 中,你在频道中用 emoji 表情作为反应标记的任何事情都会被添加到时间线中。这使得可以非常轻松地收集关键事件,从而使汇报或事故后审查讨论变得更加容易。

今天就在 Grafana Cloud 中试试,或者如果你有幸有 Grafana 联系人,就联系他们询问一下。

Grafana Cloud 是开始使用 metrics, logs, traces, dashboards 等的最简单的方式。我们有一个慷慨的永久免费套餐和计划。现在就免费注册!

相关文章:

13 年后,我如何用 Go 编写 HTTP 服务(译)

原文&#xff1a;Mat Ryer - 2024.02.09 大约六年前&#xff0c;我写了一篇博客文章&#xff0c;概述了我是如何用 Go 编写 HTTP 服务的&#xff0c;现在我再次告诉你&#xff0c;我是如何写 HTTP 服务的。 那篇原始的文章引发了一些热烈的讨论&#xff0c;这些讨论影响了我今…...

flask+python高校学生综合测评管理系统 phl8b

系统包括管理员、教师和学生三个角色&#xff1b; 。通过研究&#xff0c;以MySQL为后端数据库&#xff0c;以python为前端技术&#xff0c;以pycharm为开发平台&#xff0c;采用vue架构&#xff0c;建立一个提供个人中心、学生管理、教师管理、课程类型管理、课程信息管理、学…...

【GameFramework框架内置模块】1、全局配置(Config)

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 【GameFramework框架】系列教程目录&#xff1a; https://blog.csdn.net/q7…...

PySpark(四)PySpark SQL、Catalyst优化器、Spark SQL的执行流程、Spark新特性

目录 PySpark SQL 基础 SparkSession对象 DataFrame入门 DataFrame构建 DataFrame代码风格 DSL SQL SparkSQL Shuffle 分区数目 DataFrame数据写出 Spark UDF Catalyst优化器 Spark SQL的执行流程 Spark新特性 自适应查询(SparkSQL) 动态合并 动态调整Join策略 …...

2024第六届中国济南国际福祉及残疾人用品展览会/失能护理展

龘龘龙年-第六届山东福祉展会-将于5月27-29日在济南黄河国际会展中心举办&#xff1b; 一、引言 2024年&#xff0c;中国龙年&#xff0c;龙象征着力量、繁荣与希望。在这个特殊的年份&#xff0c;一场备受瞩目的盛会即将拉开帷幕。2024年第六届中国&#xff08;济南&#xf…...

SegmentAnything官网demo使用vue+python实现

一、效果&准备工作 1.效果 没啥好说的&#xff0c;低质量复刻SAM官网 https://segment-anything.com/ 需要提一点&#xff1a;所有生成embedding和mask的操作都是python后端做的&#xff0c;计算mask不是onnxruntime-web实现的&#xff0c;前端只负责了把rle编码的mask解…...

Java:字符集、IO流 --黑马笔记

一、字符集 1.1 字符集的来历 我们知道计算机是美国人发明的&#xff0c;由于计算机能够处理的数据只能是0和1组成的二进制数据&#xff0c;为了让计算机能够处理字符&#xff0c;于是美国人就把他们会用到的每一个字符进行了编码&#xff08;所谓编码&#xff0c;就是为一个…...

RabbitMQ之五种消息模型

1、 环境准备 创建Virtual Hosts 虚拟主机&#xff1a;类似于mysql中的database。他们都是以“/”开头 设置权限 2. 五种消息模型 RabbitMQ提供了6种消息模型&#xff0c;但是第6种其实是RPC&#xff0c;并不是MQ&#xff0c;因此不予学习。那么也就剩下5种。 但是其实3、4…...

项目02《游戏-14-开发》Unity3D

基于 项目02《游戏-13-开发》Unity3D &#xff0c; 任务&#xff1a;战斗系统之击败怪物与怪物UI血条信息 using UnityEngine; public abstract class Living : MonoBehaviour{ protected float hp; protected float attack; protected float define; …...

【Java数据结构】单向 不带头 非循环 链表实现

模拟实现LinkedList&#xff1a;下一篇文章 LinkedList底层是双向、不带头结点、非循环的链表 /*** LinkedList的模拟实现*单向 不带头 非循环链表实现*/ class SingleLinkedList {class ListNode {public int val;public ListNode next;public ListNode(int val) {this.val …...

【ES6】模块化

nodejs遵循了CommonJs的模块化规范 导入 require() 导出 module.exports 模块化的好处&#xff1a; 模块化可以避免命名冲突的问题大家都遵循同样的模块化写代码&#xff0c;降低了沟通的成本&#xff0c;极大方便了各个模块之间的相互调用需要啥模块&#xff0c;调用就行 …...

腾讯云4核8G服务器可以用来干嘛?怎么收费?

腾讯云4核8G服务器适合做什么&#xff1f;搭建网站博客、企业官网、小程序、小游戏后端服务器、电商应用、云盘和图床等均可以&#xff0c;腾讯云4核8G服务器可以选择轻量应用服务器4核8G12M或云服务器CVM&#xff0c;轻量服务器和标准型CVM服务器性能是差不多的&#xff0c;轻…...

怎么在bash shell中操作复杂json对象

怎么在bash shell中操作复杂json对象 在bash shell中操作复杂JSON对象&#xff0c;jq可以帮助我们在bash环境下轻松地处理这类数据&#xff0c;本文将详细介绍如何使用jq在bash中操作复杂的JSON对象。 jq是一个轻量级且灵活的命令行JSON处理器&#xff0c;它允许你以非常高效的…...

11.div函数

文章目录 函数简介1.函数原型2.div_t结构体3.引用头文件 代码运行 函数简介 1.函数原型 div_t div(int numerator, int denominator);div函数把numerator除以denominator&#xff0c;产生商和余数&#xff0c;用一个div_t的结构体返回。 2.div_t结构体 typedef struct _div…...

windows11 MSYS2下载安装教程

MSYS2 可以理解为在windows平台上模拟linux编程环境的开源工具集 当前环境&#xff1a;windows11 1. 下载 官网地址可下载最新版本&#xff0c;需要科学上网 https://www.msys2.org/ 2. 安装 按照正常安装软件流程一路next就可以 打开 3. 配置环境 网上很多教程提到需…...

Excel+VBA处理高斯光束

文章目录 1 图片导入与裁剪2 获取图片数据3 数据拟合 1 图片导入与裁剪 插入图片没什么好说的&#xff0c;新建Excel&#xff0c;【插入】->【图片】。 由于图像比较大&#xff0c;所以要对数据进行截取&#xff0c;选中图片之后&#xff0c;点击选项卡右端的【图片格式】…...

如何启动若依框架

Mysql安装 一、下载 链接&#xff1a;https://pan.baidu.com/s/1s8-Y1ooaRtwP9KnmP3rxlQ?pwd1234 提取码&#xff1a;1234 二、安装(解压) 下载完成后我们得到的是一个压缩包&#xff0c;将其解压&#xff0c;我们就可以得到MySQL 5.7.24的软件本体了(就是一个文件夹)&…...

案例:CentOS8 在 MySQL8.0 实现半同步复制

异步复制 MySQL 默认的复制即是异步的&#xff0c;主库在执行完客户端提交的事务后会立即将结果返给给客户端&#xff0c;并不关心从库是否已经接收并处理&#xff0c;这样就会有一个问题&#xff0c;主节点如果 crash 掉了&#xff0c;此时主节点上已经提交的事务可能并没有传…...

阿里云带宽计费模式怎么选?如何收费的?

阿里云服务器带宽计费模式分为“按固定带宽”和“按使用流量”&#xff0c;有什么区别&#xff1f;按固定带宽是指直接购买多少M带宽&#xff0c;比如1M、5M、10M、100M等&#xff0c;阿里云直接分配用户所购买的带宽值&#xff0c;根据带宽大小先付费再使用&#xff1b;按使用…...

c#记录几个问题

最近在看c#&#xff0c;有几个问题记录下 1&#xff09;全局变量&#xff0c;其实是声明一个public static类&#xff0c;里面包含一些public static变量和函数&#xff0c;也就是在程序开始运行后就创生了一个对应的存储空间&#xff0c;调用时就要写明是谁的什么变量。针对的…...

第69讲后端登录逻辑实现

Admin实体&#xff1a; TableName("t_admin") Data public class Admin {TableId(type IdType.AUTO)private Integer id; // 编号private String userName; // 用户名private String password; // 密码TableField(select false)private String newPassword; // 新…...

Qt 字符串类应用与常用基本数据类型

目录 操作字符串 查询字符串 Qt 常见数据类型 操作字符串 创建一个控制台项目 &#xff08;1&#xff09;QString提供一个二元的 “” 操作符&#xff0c;主要用于组合两个字符串。QString str1 "Hello World 传递给QString一个 const char* 类型的ASCII字符串 “He…...

JAVA面试题15

当然&#xff0c;我可以提供给您一些常见的Java面试题及其答案。以下是一些示例&#xff1a; 什么是Java的四种基本数据类型&#xff1f; 答案&#xff1a;Java的四种基本数据类型是整型&#xff08;byte、short、int、long&#xff09;、浮点型&#xff08;float、double&…...

git安装及使用

1、下载git 官网 Windows系统Git安装教程&#xff08;详解Git安装过程&#xff09; 官网打不开的话&#xff0c;可以使用镜像地址 镜像地址 2、使用git Git的下载、安装与使用(Windows) 30分钟带你精通git使用 3、注册github https://github.com/ 4、github文档 h…...

电力负荷预测 | Matlab实现基于LSTM长短期记忆神经网络的电力负荷预测模型(结合时间序列)

文章目录 效果一览文章概述源码设计参考资料效果一览 文章概述 电力负荷预测 | Matlab实现基于LSTM长短期记忆神经网络的电力负荷预测模型(结合时间序列) 所谓预测,就是指通过对事物进行分析及研究,并运用合理的方法探索事物的发展变化规律,对其未来发展做出预先估计和判断…...

力扣:455. 分发饼干

贪心解法思路&#xff1a; 1.先把两个数组按顺序遍历好&#xff0c;之后用最大的饼干来喂最大的胃口&#xff0c;如果最大的饼干不能喂饱最大的胃口&#xff0c;就除去这个最大的胃口&#xff0c;在剩下的为胃口中找最大的胃口来进行比对。这题主要历用了通过局部的优解&#…...

SpringCloud-项目引入Nacos

一、安装Nacos服务 首先&#xff0c;我们需要从 Nacos 的官方网站下载发布版本。下载地址&#xff1a;Releases alibaba/nacos GitHub 选择合适的版本并下载&#xff0c;解压缩得到 Nacos 的安装包。 在解压后的 Nacos 目录中&#xff0c;找到 bin 文件夹。 用写字板编辑…...

如何在 Windows 10/11 上恢复回收站永久删除的文件夹?

经验丰富的 Windows 用户将使用 Windows 备份和还原或文件历史记录来恢复不在回收站中的已删除文件夹。这些工具确实有助于 Windows 文件夹恢复&#xff0c;但并不总是有效。现在有许多专用的 Windows 数据恢复软件和免费解决方案可以替代它们&#xff0c;为 Windows 用户提供了…...

七、滚动条操作——调整图像对比度

对比度调整&#xff1a;是在原来图像基础上进行相应的公式调整&#xff0c;是类似乘法操作&#xff0c;本身像数值越大&#xff0c;对比度增加之后其与低像素点值差距越大&#xff0c;导致对比增强 项目最终效果&#xff1a;通过滚动条trackbar来实现调整图片亮度的功能 我这里…...

免费生成ios证书的方法(无需mac电脑)

使用hbuilderx的uniapp框架开发移动端程序很方便&#xff0c;可以很方便地开发出移动端的小程序和app。但是打包ios版本的app的时候却很麻烦&#xff0c;官方提供的教程需要使用mac电脑来生成证书&#xff0c;但是mac电脑却不便宜&#xff0c;一般的型号都差不多上万。 因此&a…...

gtkmm4 应用程序使用 CSS 样式

文章目录 前言css选择器css文件示例源代码效果动态设置css-classes 前言 程序样式和代码逻辑分离开 使代码逻辑更可观 css选择器 Cambalache提供了两种css-classes 相当于css里的类名:class“类名”css-name 相当于css里的标签名:spin div p 啥的 如上我设置了这个按钮控件的…...

科研绘图-半小提琴图-

文章目录 前言1.软件安装-Origin 20222.绘制半小提琴图3.绘制径向条形图 前言 本文叙述记录的是一些科研绘图的实现方法&#xff0c;具体介绍从软件安装到实现图表绘制的详细过程。 1.软件安装-Origin 2022 Origin是一款具有丰富绘图功能的科研绘图软件&#xff0c;安装过程…...

机器学习 | 深入集成学习的精髓及实战技巧挑战

目录 xgboost算法简介 泰坦尼克号乘客生存预测(实操) lightGBM算法简介 《绝地求生》玩家排名预测(实操) xgboost算法简介 XGBoost全名叫极端梯度提升树&#xff0c;XGBoost是集成学习方法的王牌&#xff0c;在Kaggle数据挖掘比赛中&#xff0c;大部分获胜者用了XGBoost。…...

SNMP(简单网络管理协议)介绍

简介 作为系统管理员的重要工作之一是收集关于服务器和基础设施的准确信息。有许多工具和选项可用于收集和处理这种类型的信息。其中许多工具都是建立在一种称为SNMP的技术之上。 SNMP代表简单网络管理协议。这是服务器可以共享有关其当前状态的信息的一种方式&#xff0c;也…...

Spring中常见的设计模式

使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性、更具有灵活、优雅&#xff0c;而Spring中共有九种常见的设计模式 工厂模式 工厂模式&#xff08;Factory Pattern&#xff09;是 Java 中最常用的设计模式之一。这种类型的设计模式属于…...

【MySQL】——数值函数的学习

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-Z1fAnfrxGD7I5gqp {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…...

LLMs模型选择,LLMs复读机问题,LLMs长文本处理方案

为什么会出现 LLMs 复读机问题&#xff1f; LLMs 复读机问题&#xff08;LLMs Parroting Problem&#xff09;是指大型语言模型&#xff08;LLMs&#xff09;在生成文本时可能出现的重复或重复先前输入内容的现象。出现LLMs复读机问题可能有以下几个原因&#xff1a; 数据偏差…...

LeetCode.144. 二叉树的前序遍历

题目 144. 二叉树的前序遍历 分析 这道题目是比较基础的题目&#xff0c;我们首先要知道二叉树的前序遍历是什么&#xff1f; 就是【根 左 右】 的顺序&#xff0c;然后利用递归的思想&#xff0c;就可以得到这道题的答案&#xff0c;任何的递归都可以采用 栈 的结构来实现…...

Redis复制

文章目录 1.Redis复制是什么2.Redis能干嘛3.权限细节4.基本操作命令5.常用三招5.1 一主二仆5.2 薪火相传5.3 反客为主 6.复制原理和工作流程7.复制的缺点 1.Redis复制是什么 就是主从复制&#xff0c;master以写为主&#xff0c;Slave以读为主。当master数据变化的时候&#x…...

C++入门学习(二十七)跳转语句—break语句

1、与switch语句联合使用 C入门学习&#xff08;二十三&#xff09;选择结构-switch语句-CSDN博客 #include <iostream> #include <string> using namespace std;int main() { int number;cout<<"请为《斗萝大路》打星(1~5※)&#xff1a;" &…...

Spark安装(Yarn模式)

一、解压 链接&#xff1a;https://pan.baidu.com/s/1O8u1SEuLOQv2Yietea_Uxg 提取码&#xff1a;mb4h tar -zxvf /opt/software/spark-3.0.3-bin-hadoop3.2.tgz -C /opt/module/spark-yarn mv spark-3.0.3-bin-hadoop3.2/ spark-yarn 二、配置环境变量 vim /etc/profile…...

1.4 Binance_interface API U本位合约行情

Binance_interface API U本位合约行情 Github地址PyTed量化交易研究院 1. API U本位合约行情接口总览 方法解释Pathget_ping测试服务器连通性 PING/fapi/v1/pingget_time获取服务器时间/fapi/v1/timeget_exchangeInfo获取交易规则和交易对/fapi/v1/exchangeInfoget_depth深度…...

单片机学习笔记---AT24C02(I2C总线)

目录 有关储存器的介绍 存储器的简介 存储器简化模型 AT24C02介绍 AT24C02引脚及应用电路 I2C总线介绍 I2C电路规范 开漏输出模式和弱上拉模式 其中一个设备的内部结构 I2C通信是怎么实现的 I2C时序结构 起始条件和终止条件 发送一个字节 接收一个字节 发送应答…...

c++恶魔轮盘制造第1期输赢

小常识&#xff0c;恶魔叫DEALER。 赢了很简单 void sheng() { cout<<"你获胜了&#xff01;";MessageBox(NULL,TEXT("你的钱~~~~~~给你"),TEXT("DEALER"),MB_OK);system("pause");system("cls"); } 输了我用了个选…...

60-JS-Ajax

ajax取数据的一种手段,局部刷新,例如弹幕 1.ajax的使用,创建ajax对象,发起对服务器请求 2.核心对象XMLHttpRequest对象(简称XHR) CSS:Cascading Style Sheets(层叠样式表) HTML:Hypertext Markup Language(超文本标记语言) 3.发起对服务器的请求 浏览器方式请求:打…...

C# Avalonia 折线图

线图开发在C# Avalonia框架中可以通过多种方式实现。由于Avalonia旨在成为跨平台的UI框架&#xff0c;您可以利用多种库和方法来绘制折线图。以下是一个简单的例子&#xff0c;展示了如何在Avalonia应用程序中创建一个基本的折线图。 首先&#xff0c;您需要在Avalonia项目中包…...

Vue3中Setup概述和使用(三)

一、引入Setup 1、Person.Vue 与Vue3编写简单的App组件(二) 中的区别是&#xff1a;取消data、methods等方法,而是将数据和方法定义全部放进setup中。 <template><div class"person"><h1>姓名:{{name}}</h1><h1>年龄:{{age}}</h…...

hexo 博客搭建以及踩雷总结

搭建时的坑 文章置顶 安装一下这个依赖 npm install hexo-generator-topindex --save然后再文章的上面设置 top: number&#xff0c;数字越大&#xff0c;权重越大&#xff0c;也就是越靠顶部 hexo 每次推送 nginx 都访问不到 宝塔自带的 nginx 的 config 里默认的角色是 …...

WordPress后台编辑个人资料页面直接修改用户名插件Change Username

前面跟大家介绍了『如何修改WordPress后台管理员用户名&#xff1f;推荐2种简单方法』一文&#xff0c;但是对于新站长或者有很多用户的站长来说&#xff0c;操作有点复杂&#xff0c;所以今天向大家推荐一款可以直接在WordPress后台编辑个人&#xff08;用户&#xff09;资料页…...

ssm+vue的医药垃圾分类管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的医药垃圾分类管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结…...