10 个示例快速入门 Go-Spring|v1.3.0 正式发布

终于,Go-Spring 在经历了又一轮漫长的开发之后,正式迈入了一个全新的里程碑。

最新版本的 Go-Spring 将彻底终结长期以来围绕配置、日志与启动标准化管理的纷争。 如今,只需简简单单的一行 gs.Run(),便可以优雅地启动整个 Go-Spring 应用,实在令人畅快!

从此,我们只需要依照既定规范,对配置、日志与启动进行扩展,便能够从容应对各种日常开发场景, 既能保持代码简洁高效,也为系统留下足够的可扩展空间。


接下来,本文将通过 10 个示例,带你一步一步、由浅入深地快速入门 Go-Spring。 每个示例都能独立运行,完整代码在 github.com/lvan100/go-... 这里。

1. 启动一个最小 Go-Spring 应用

第一步咱们先不写业务代码,只来确认 Go-Spring 应用怎么启动。

代码如下:

go 复制代码
func main() {
	gs.Run()
}

完整代码在 examples/01-run-only/main.go

上面这段代码虽然看起来很短,但是已经足够让程序进入 Go-Spring 的应用生命周期。 gs.Run() 会创建应用,加载配置,初始化日志,刷新 IoC 容器,启动内置 HTTP Server, 并监听 SIGINT / SIGTERM,最后在进程退出时还能执行优雅关闭。

使用下面的命令运行示例:

bash 复制代码
cd examples/01-run-only
go run .

此时控制台会打印如下信息:

text 复制代码
   ____    ___            ____    ____    ____    ___   _   _    ____ 
  / ___|  / _ \          / ___|  |  _ \  |  _ \  |_ _| | \ | |  / ___|
 | |  _  | | | |  _____  \___ \  | |_) | | |_) |  | |  |  \| | | |  _ 
 | |_| | | |_| | |_____|  ___) | |  __/  |  _ <   | |  | |\  | | |_| |
  \____|  \___/          |____/  |_|     |_| \_\ |___| |_| \_|  \____| 

            go-spring@v1.3.0  https://github.com/go-spring/

[INFO][2026-05-02T19:13:07.837][...ing/spring-core/gs/internal/gs_app/app.go:289] _app_def||msg=ready to serve requests

ready to serve requests 表示应用已经启动,并且成功监听 :9090。 使用下面的命令访问根路径:

bash 复制代码
curl http://127.0.0.1:9090/

会得到:

text 复制代码
404 page not found

这里 404 是预期结果。它说明 HTTP Server 已经启动了,只是还没有 handler 能处理这个路径。 按下 Ctrl+C 可以停止程序,接着 Go-Spring 进入关闭流程。

2. 添加一个标准库 HTTP 路由

上一章的应用已经能够启动,但是还没有业务入口,所以任何请求都会返回 404。 现在咱们先不着急引入 IoC,而是用 Go 标准库注册一个最普通的 HTTP handler。

代码如下:

go 复制代码
func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("hello from net/http\n"))
	})

	gs.Run()
}

完整代码在 examples/02-stdlib-http/main.go

使用下面的命令运行示例:

bash 复制代码
cd examples/02-stdlib-http
go run .

然后访问新增的 /hello 路由:

bash 复制代码
curl http://127.0.0.1:9090/hello

这次不再是 404 了,而是返回预期中的响应:

text 复制代码
hello from net/http

可以看到,应用已经从"只能启动"变成了"能处理 HTTP 请求"。 不过 handler 目前还是一个匿名函数,业务状态和配置都写不进去。

3. 把业务对象注册为 root bean

在上一章中,/hello 是直接写在 main 函数里的匿名函数。 它能验证 HTTP 处理有效,但不好继续扩展。 因为一旦问候语、目标用户、校验规则需要变成配置,匿名函数就会显得很别扭。 所以本章会创建一个业务对象 GreetingRoot,让它持有配置,并且把它的方法实现成 handler。

代码如下:

go 复制代码
type GreetingRoot struct {
	Greeting string `value:"${demo.greeting:=Hello}" expr:"$ != ''"`
	Audience string `value:"${demo.audience:=Go-Spring}" expr:"$ != ''"`
}

func (g *GreetingRoot) Hello(w http.ResponseWriter, r *http.Request) {
	_, _ = fmt.Fprintf(w, "%s, %s!\n", g.Greeting, g.Audience)
}

func main() {
	root := &GreetingRoot{}
	http.HandleFunc("/hello", root.Hello)

	gs.Configure(func(app gs.App) {
		app.Root(root)
	}).Run()
}

完整代码在 examples/03-configure-root-bean/main.go

与上一章相比,这次的代码有两个实质性变化:

  • handler 不再是匿名函数,而是 GreetingRoot.Hello 方法,业务状态进入了结构体。
  • root 被传给了 app.Root(root),所以 Go-Spring 会在启动过程中处理它的字段标签。

GreetingRoot 字段上的 value tag 表示配置绑定关系:

  • ${demo.greeting:=Hello} 表示读取配置项 demo.greeting 的值,如果没有在任何地方配置,就使用默认值 Hello
  • expr:"$ != ''" 表示绑定后的值不能为空,如果不满足条件,应用就会在启动阶段失败,而不是等到请求进来才暴露问题。

使用下面的命令运行示例:

bash 复制代码
cd examples/03-configure-root-bean
go run .

然后访问 /hello 路由:

bash 复制代码
curl http://127.0.0.1:9090/hello

我们会得到预期中的响应:

text 复制代码
Hello, Go-Spring!

这里的 HelloGo-Spring 都来自字段 tag 中的默认值。 也就是说,尽管应用仍然使用标准库路由,但业务对象已经进入 Go-Spring 的配置绑定流程了。

4. 用外部配置覆盖默认值

上一章咱们已经把配置绑定关系写进了 GreetingRoot,但运行结果还完全依赖 tag 里的默认值。 真实应用一般不会只靠默认值运行,环境之间的差异常常会放在配置文件、环境变量或启动参数里。 这一章仍然沿用上一章的代码,代码没有任何变化,只是在示例目录中增加一个配置文件。

GreetingRoot 仍然绑定同样的两个配置项:

go 复制代码
type GreetingRoot struct {
	Greeting string `value:"${demo.greeting:=Hello}" expr:"$ != ''"`
	Audience string `value:"${demo.audience:=Go-Spring}" expr:"$ != ''"`
}

但在 ./conf 目录下新增一个配置文件 app.properties,内容如下:

properties 复制代码
demo.greeting=Hello from ./conf/app.properties
demo.audience=config file

完整代码在 examples/04-config-overrides/main.go

使用下面的命令运行示例:

bash 复制代码
cd examples/04-config-overrides
go run .

然后访问 /hello 路由:

bash 复制代码
curl http://127.0.0.1:9090/hello

这时响应会从默认值变成配置文件里的值:

text 复制代码
Hello from ./conf/app.properties, config file!

不改变配置文件的内容,咱们可以用环境变量覆盖其中一个配置项, 比如 GS_DEMO_AUDIENCE,它会映射成 demo.audience

bash 复制代码
GS_DEMO_AUDIENCE="env var" go run .
curl http://127.0.0.1:9090/hello

执行上面的命令,会看到响应从配置文件里的值变成了环境变量里的值:

text 复制代码
Hello from ./conf/app.properties, env var!

咱们还可以使用命令行参数覆盖配置,写法是 -Dkey=value

bash 复制代码
go run . -Ddemo.audience="cmd arg"
curl http://127.0.0.1:9090/hello

执行上面的命令,会看到响应从配置文件里的值变成了命令行参数里的值:

text 复制代码
Hello from ./conf/app.properties, cmd arg!

这一章咱们没有改变任何代码,就让上一章的配置绑定变得更方便运维了。 不过 GreetingRoot 虽然已经可以被 Go-Spring 绑定配置了,但它还是在 main 中手动创建的。

5. 用容器装配 HTTP mux 和中间件

前面几章咱们一直把对象创建和路由注册写在 main 函数里, 虽然适合入门,但是当请求日志、耗时统计、请求 ID、panic recovery 这类横切逻辑出现时, HTTP 入口就不应该散落在 main 中了。 因此这一章咱们使用 Go-Spring 来构造所有的组件。

首先咱们把配置收拢成一个配置结构体。 注意这里的 tag 不再写 demo.greeting,而是写 greetingaudience, 因为注册构造函数时会指定整体前缀 ${demo}

代码如下:

go 复制代码
type GreetingConfig struct {
	Greeting string `value:"${greeting:=Hello}" expr:"$ != ''"`
	Audience string `value:"${audience:=Go-Spring}" expr:"$ != ''"`
}

type Controller struct {
	cfg GreetingConfig
}

func NewController(cfg GreetingConfig) *Controller {
	return &Controller{cfg: cfg}
}

func (c *Controller) Hello(w http.ResponseWriter, r *http.Request) {
	_, _ = fmt.Fprintf(w, "%s, %s!\n", c.cfg.Greeting, c.cfg.Audience)
}

然后咱们显式创建一个 *gs.HttpServeMux。 它的内部仍然使用标准库 http.NewServeMux(),只是最终返回给 Go-Spring 的是带中间件的 handler。

代码如下:

go 复制代码
func NewHTTPMux(c *Controller) *gs.HttpServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("/hello", c.Hello)
	return &gs.HttpServeMux{Handler: logging(mux)}
}

func logging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("method=%s path=%s elapsed=%s",
			r.Method, r.URL.Path, time.Since(start))
	})
}

这次,咱们在代码中添加了一个 logging 中间件,可以记录请求的方法、路径和耗时。

最后咱们把构造函数注册给容器:

go 复制代码
func init() {
	gs.Provide(NewController, gs.TagArg("${demo}"))
	gs.Provide(NewHTTPMux)
}

func main() {
	gs.Run()
}

完整代码在 examples/05-http-middleware-mux/main.go

使用下面的命令运行示例:

bash 复制代码
cd examples/05-http-middleware-mux
go run .

然后访问 /hello 路由:

bash 复制代码
curl -i http://127.0.0.1:9090/hello

可以看到这次响应变成了下面这样:

text 复制代码
Hello with middleware, custom mux!

同时,控制台上还会打印请求的方法、路径和耗时,说明请求确实经过了 logging 中间件。

这一章咱们完成了一个重要转折: main 又回到了只负责 gs.Run(),对象创建、配置绑定、HTTP mux 组装则统统交给容器。

6. 把 controller 和 service 拆成多个 bean

上一章咱们已经用容器创建了 controller 和 HTTP mux,但问候语仍然由 controller 自己拼出来。 随着业务增长,controller 应该更专注于 HTTP 请求和响应,业务逻辑和规则应该放进 service。 因此这一章咱们新增一个 GreetingService,让 controller 通过构造函数依赖 service。

代码如下:

go 复制代码
type GreetingService struct {
	Greeting string `value:"${demo.greeting:=Hello}" expr:"$ != ''"`
}

func NewGreetingService() *GreetingService {
	return &GreetingService{}
}

func (s *GreetingService) Message(audience string) string {
	return fmt.Sprintf("%s, %s!", s.Greeting, audience)
}

type Controller struct {
	service  *GreetingService
	Audience string `value:"${demo.audience:=Go-Spring}" expr:"$ != ''"`
}

func NewController(service *GreetingService) *Controller {
	return &Controller{service: service}
}

func (c *Controller) Hello(w http.ResponseWriter, r *http.Request) {
	_, _ = fmt.Fprintln(w, c.service.Message(c.Audience))
}

注册代码也不复杂,只需要多提供一个构造函数:

go 复制代码
func init() {
	gs.Provide(NewGreetingService)
	gs.Provide(NewController)
	gs.Provide(NewHTTPMux)
}

完整代码在 examples/06-multi-bean-di/main.go

使用下面的命令运行示例:

bash 复制代码
cd examples/06-multi-bean-di
go run .

然后访问 /hello 路由:

bash 复制代码
curl http://127.0.0.1:9090/hello

可以看到预期中的响应:

text 复制代码
Hello from service, controller config!

这一章咱们展示的是构造函数注入。 NewController 的参数声明了它需要 *GreetingService, Go-Spring 就会先创建 service,再把它传给 controller。 业务代码不需要自己查找依赖,也不需要在 main 中手工组装对象图。

7. 注册一个外部客户端 bean

上一章的 service 内部只有一个字段,但真实服务通常还会依赖 Redis、数据库、消息队列等外部客户端。 为了让示例聚焦在 Go-Spring 的注册方式上,本章用一个轻量的 RedisClient 来模拟外部客户端: 它会读取配置并打印日志,但不会连接真实的 Redis。

首先定义 Redis 配置和客户端构造函数:

go 复制代码
type RedisConfig struct {
	Addr     string `value:"${addr}" expr:"$ != ''"`
	Password string `value:"${password:=}"`
}

type RedisClient struct {
	cfg RedisConfig
}

func NewRedisClient(cfg RedisConfig) (*RedisClient, error) {
	log.Printf("create redis client addr=%s", cfg.Addr)
	return &RedisClient{cfg: cfg}, nil
}

func CloseRedis(*RedisClient) error {
	return nil
}

func (c *RedisClient) Ping(context.Context) error {
	log.Printf("redis ping addr=%s", c.cfg.Addr)
	return nil
}

然后让 service 依赖 *RedisClient,并在处理请求时调用它:

go 复制代码
type GreetingService struct {
	redis    *RedisClient
	Greeting string `value:"${demo.greeting:=Hello}" expr:"$ != ''"`
}

func NewGreetingService(redis *RedisClient) *GreetingService {
	return &GreetingService{redis: redis}
}

func (s *GreetingService) Message(ctx context.Context, audience string) string {
	_ = s.redis.Ping(ctx)
	return fmt.Sprintf("%s, %s!", s.Greeting, audience)
}

最后咱们需要增加 Redis client 的注册代码:

go 复制代码
func init() {
	gs.Provide(NewRedisClient, gs.TagArg("${spring.go-redis}")).Destroy(CloseRedis)
	gs.Provide(NewGreetingService)
	gs.Provide(NewController)
	gs.Provide(NewHTTPMux)
}

注册 Redis client 时:

  • gs.TagArg("${spring.go-redis}") 表示构造函数参数 RedisConfigspring.go-redis 前缀读取配置;
  • Destroy(CloseRedis) 表示容器关闭时调用销毁函数。

完整代码在 examples/07-redis-single-client/main.go

咱们还需要在配置文件中增加一个配置项,用于指定 Redis 地址:

properties 复制代码
spring.go-redis.addr=127.0.0.1:6379

使用下面的命令运行示例:

bash 复制代码
cd examples/07-redis-single-client
go run .

会看到控制台上打印了创建客户端的日志:

text 复制代码
create redis client addr=127.0.0.1:6379

访问 /hello 路由:

bash 复制代码
curl http://127.0.0.1:9090/hello

可以看到预期中的响应:

text 复制代码
Hello with Redis, single client!

另外,咱们还能在控制台上看到请求过程中打印出的 redis ping 日志。 虽然这一章的 Redis Client 只是模拟对象,但它的注册方式和真实客户端没有区别: 配置绑定、依赖注入、资源销毁都交给容器。

8. 条件注册和多实例客户端

上一章咱们注册的只有一个 Redis client,所以 service 直接依赖 *RedisClient 就够了。 但真实应用里更常见的是同一种客户端有多个实例,例如默认 Redis、cache Redis、queue Redis。 如果咱们继续手写多个 NewRedisClient,那么注册很快就会变乱,所以这一章咱们引入条件注册、命名 bean 和配置分组。

首先在默认客户端注册的时候增加两个声明:

go 复制代码
gs.Provide(NewRedisClient, gs.TagArg("${spring.go-redis}")).
	Condition(gs.OnProperty("spring.go-redis.addr")).
	Destroy(CloseRedis).
	Name("__default__")
  • Condition(gs.OnProperty("spring.go-redis.addr")) 表示只有配置里存在 spring.go-redis.addr 时才创建默认客户端。
  • Name("__default__") 表示给这个 bean 一个名字,后续同类型实例变多时,注入方就可以明确选择它。

然后注册其他 Redis 实例,不过咱们不需要一条条手写注册,而是交给 gs.Group, 它可以根据配置批量创建多个同类型的实例:

go 复制代码
gs.Group("${spring.go-redis.instances}", NewRedisClient, CloseRedis)

咱们需要在配置文件中新增一个 spring.go-redis.instances 配置项,它是一个 map, 键是实例名称,值是实例的配置。

properties 复制代码
spring.go-redis.addr=127.0.0.1:6379
spring.go-redis.instances.cache.addr=127.0.0.1:6380
spring.go-redis.instances.queue.addr=127.0.0.1:6381

现在需要对 service 做一些调整,因为现在同类型 *RedisClient 有多个注册实例。 咱们可以通过它字段上的 autowire 指定注入名为 __default__ 的实例:

go 复制代码
type GreetingService struct {
	Client   *RedisClient `autowire:"__default__?"`
	Greeting string       `value:"${demo.greeting:=Hello}" expr:"$ != ''"`
}

完整代码在 examples/08-conditional-multi-redis/main.go

使用下面的命令运行示例:

bash 复制代码
cd examples/08-conditional-multi-redis
go run .

然后会看到启动时控制台上打印了 __default__ 创建的日志, 但是并没有 cachequeue 创建的日志。 这是因为 Go-Spring 是按需实例化的,用不到的实例不会被创建。

访问 /hello 路由:

bash 复制代码
curl http://127.0.0.1:9090/hello

可以看到预期中的响应:

text 复制代码
Hello with conditional Redis, conditional clients!

咱们可以修改 service,让它注入 cache 实例:

go 复制代码
type GreetingService struct {
	Client   *RedisClient `autowire:"cache?"`
	Greeting string       `value:"${demo.greeting:=Hello}" expr:"$ != ''"`
}

然后会看到启动时只有 cache 实例被创建。 同样的方式,咱们也可以注入 queue 实例。

这一章咱们解决的是"同类型多个实例如何管理"的问题。 Condition 可以控制 bean 是否创建, Name 可以给 bean 命名, autowire 可以让依赖方选择具体实例, Group 可以把一组配置批量转换成一组客户端。

9. 接入结构化日志

到目前为止,示例已经展示了 HTTP、配置、依赖注入和客户端注册,但日志还只是普通文本。 真实服务需要更容易检索和关联的日志:业务日志要能标识来源,请求日志要能记录方法、路径和耗时, 同一次请求中的日志最好带上同一个 request id,等等。 所以这一章咱们引入 Go-Spring 的日志系统。

首先注册两个日志标签,一个用于业务日志,一个用于 HTTP 访问日志:

go 复制代码
var (
	tagBizGreeting = log.RegisterBizTag("greeting", "serve")
	tagHTTPRequest = log.RegisterRPCTag("http", "request")
)

service 中不再使用标准库打印日志,而是使用 Go-Spring 的日志系统记录结构化字段:

go 复制代码
func (s *GreetingService) Summary(ctx context.Context) string {
	log.Info(ctx, tagBizGreeting,
		log.String("greeting", s.Greeting),
		log.Msg("building greeting"),
	)
	return s.Greeting + ", structured logs!"
}

为 HTTP 入口新增一个中间件 requestID,它可以从请求头读取或生成 request id。 对于 request id 这类信息,咱们希望它们能被自动记录到日志中,而不是每次打印日志时手动添加。 所以,咱们把 request id 放进 context 中,方便日志系统自动提取。

go 复制代码
type requestIDKey struct{}

func NewHTTPMux(c *Controller) *gs.HttpServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("/hello", c.Hello)
	return &gs.HttpServeMux{Handler: requestID(logging(mux))}
}

func requestID(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Request-ID")
		if id == "" {
			id = fmt.Sprintf("%d", time.Now().UnixNano())
		}
		w.Header().Set("X-Request-ID", id)
		ctx := context.WithValue(r.Context(), requestIDKey{}, id)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

咱们需要给日志系统设置一个上下文提取回调 log.FieldsFromContext, 这样它就可以从 context 中自动提取 request id,然后和其他字段一起被记录下来。

go 复制代码
log.FieldsFromContext = func(ctx context.Context) []log.Field {
	id, ok := ctx.Value(requestIDKey{}).(string)
	if !ok || id == "" {
		return nil
	}
	return []log.Field{log.String("request_id", id)}
}

完整代码在 examples/09-logging/main.go

最后咱们在配置文件中添加日志系统的配置,将日志以 JSON 格式输出到控制台。

properties 复制代码
logging.logger.root.type=ConsoleLogger
logging.logger.root.level=INFO
logging.logger.root.layout.type=JSONLayout
logging.logger.root.layout.fileLineMaxLength=30

使用下面的命令运行示例:

bash 复制代码
cd examples/09-logging
go run .

带上请求 ID 访问 /hello 路由:

bash 复制代码
curl -H "X-Request-ID: demo-1" http://127.0.0.1:9090/hello

可以看到预期中的响应:

text 复制代码
Hello with logging, structured logs!

同时可以看到控制台会输出 JSON 日志,包含 tag、request_id、HTTP 方法、路径、耗时和业务字段。

text 复制代码
{"level":"info","time":"2026-05-03T08:57:21.525","fileLine":"...mples/09-logging/main.go:29","tag":"_biz_greeting_serve","request_id":"demo-1","greeting":"Hello with logging","msg":"building greeting"}
{"level":"info","time":"2026-05-03T08:57:21.526","fileLine":"...mples/09-logging/main.go:80","tag":"_rpc_http_request","request_id":"demo-1","method":"GET","path":"/hello","elapsed":"783.125µs","msg":"http request completed"}

这一章的重点不是"打印更多内容",而是让日志变成结构化事件: 标签说明事件类型,字段承载可检索数据,context 把一次请求中的公共字段串起来。

10. 让组件可以脱离真实服务测试

经过前面的步骤,应用已经具备了一个 Web 服务常见的核心结构: HTTP 入口、controller/service 分层、配置绑定、外部客户端和结构化日志。 还剩最后一个问题:测试

如果 service 直接依赖具体 Redis client,测试时就会很难替换; 如果测试必须启动真实的 HTTP Server,也会让反馈变慢。 这一章咱们把依赖改成接口,并使用 Go-Spring 的测试容器验证装配关系。

第一处变化是定义接口,让 service 依赖行为而不是具体实现:

go 复制代码
type RedisPinger interface {
	Ping(context.Context) error
}

type GreetingService struct {
	redis    RedisPinger
	Greeting string `value:"${demo.greeting:=Hello}" expr:"$ != ''"`
}

func NewGreetingService(redis RedisPinger) *GreetingService {
	return &GreetingService{redis: redis}
}

生产环境咱们仍然使用 RedisClient,不过这次注册时还需要把它导出为 RedisPinger

go 复制代码
gs.Provide(NewRedisClient, gs.TagArg("${spring.go-redis}")).
	Condition(gs.OnProperty("spring.go-redis.addr")).
	Destroy(CloseRedis).
	Export(gs.As[RedisPinger]())

这样一来,咱们就可以在测试代码里用一个很小的 fakeRedis 来替代真实 Redis:

go 复制代码
type fakeRedis struct {
	err   error
	calls int
}

func (f *fakeRedis) Ping(context.Context) error {
	f.calls++
	return f.err
}

有了这个 fakeRedis,service 就可以直接测试了:

go 复制代码
func TestGreetingServiceWithFakeRedis(t *testing.T) {
	redis := &fakeRedis{}
	service := &GreetingService{redis: redis, Greeting: "Hi"}

	got := service.Message(context.Background(), "tester")
	if got != "Hi, tester!" {
		t.Fatalf("unexpected greeting: %q", got)
	}
	if redis.calls != 1 {
		t.Fatalf("expected one redis ping, got %d", redis.calls)
	}
}

对于 controller,咱们也可以不启动真实的 HTTP Server,而是使用 httptest 来测试 handler:

go 复制代码
func TestControllerWithFakeRedis(t *testing.T) {
	service := &GreetingService{redis: &fakeRedis{}, Greeting: "Hi"}
	controller := &Controller{service: service, Audience: "controller"}
	req := httptest.NewRequest(http.MethodGet, "/hello", nil)
	rec := httptest.NewRecorder()

	controller.Hello(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("unexpected status: %d", rec.Code)
	}
	if strings.TrimSpace(rec.Body.String()) != "Hi, controller!" {
		t.Fatalf("unexpected body: %q", rec.Body.String())
	}
}

上面都是非常纯粹的 Go 原生单元测试,并没有依赖 Go-Spring 容器。 如果咱们还想验证 Go-Spring 容器里的装配关系,可以按照下面的步骤进行。

  • 首先使用 gs.Web(false) 关闭真实的 HTTP Server,
  • 然后使用 app.Provide(&fakeRedis{}).Export(...) 把 fakeRedis 注册成接口实现,
  • 最后可以在 gs.RunTest() 中注入要检查的对象。

代码如下:

go 复制代码
func TestIoCContainerWithFakeRedis(t *testing.T) {
	gs.Web(false).Configure(func(app gs.App) {
		app.Property("spring.app.config.dir", "./testdata/empty-conf")
		// The built-in Redis client is not enabled
		app.Provide(&fakeRedis{}).Export(gs.As[RedisPinger]())
	}).RunTest(t, func(ts *struct {
		Service    *GreetingService `autowire:""`
		Controller *Controller      `autowire:""`
	}) {
		if ts.Service == nil {
			t.Fatal("service was not injected")
		}
		if ts.Controller == nil {
			t.Fatal("controller was not injected")
		}
		got := ts.Service.Message(context.Background(), "ioc")
		if got != "Hello, ioc!" {
			t.Fatalf("unexpected ioc greeting: %q", got)
		}
	})
}

gs.RunTest() 在运行的时候会启动完整的 Go-Spring 容器,对 init 注册的对象进行装配。 它接受一个回调函数,回调函数的参数是一个结构体,用来注入要检查的对象, 可以使用 autowirevalue 标签来注入对象或者配置。

完整代码在 examples/10-unit-tests/main.go

测试代码在 examples/10-unit-tests/main_test.go

使用下面的命令运行测试:

bash 复制代码
cd examples/10-unit-tests
go test

可以看到,所有测试都通过了。

这一章把前面所有的能力都落到可测试性上。 接口让外部依赖可以被 fakeRedis 替换,Export(gs.As[...]) 让生产实现按接口进入容器, gs.Web(false)gs.RunTest() 让容器装配本身也能被测试。

至此,一个 Go-Spring 应用从最小启动、HTTP 路由、配置绑定、容器装配、外部客户端、条件多实例、 结构化日志到测试的完整路径就串起来了。


从上面十个示例可以看到,Go-Spring 的核心价值并不是要替代 Go 生态中已有的标准库和工具, 而是提供了一套非常工程化的组织方式: 把应用启动、配置绑定、对象装配、资源生命周期、日志和测试这些横向能力组织起来。

对于很小的程序,直接使用标准库可能已经足够; 但当服务规模继续增长时,这些容器和生命周期能力会逐渐体现出价值。

v1.3.0 发版公告

我深知 Go-Spring 仍然存在一些问题与不足,但我也清楚,不能因为一味追求完美而止步不前。

一个真正有生命力的项目,离不开更多开发者的参与与共建。它的成长,不只是一个人的坚持,也需要一群人的热爱与投入。

如果你对 Go-Spring 感兴趣,那就加入这个项目吧。让我们一起动手打磨,一起推动它不断进化,让它变得更强大。

相关推荐
zhouwy1131 天前
Golang 基础与实战笔记:从语法到微服务的全面指南
开发语言·go
日火2 天前
Go:实现基于mutex的环形缓冲区
go
审判长烧鸡3 天前
GO错误处理【7】层层递进,环环相扣
go·报错处理
审判长烧鸡4 天前
Go结构体与指针【3】自动解引用
go·指针·结构体·自动解引用
审判长烧鸡4 天前
【GO VS PHP】之 指针/引用传递
go·php·指针·引用传递
审判长烧鸡4 天前
GO错误处理【4】报错即链条
go·异常处理·错误处理
审判长烧鸡4 天前
GO时区【1】定义与使用
go·时区
审判长烧鸡4 天前
GO错误处理【5】显式错误处理
go·错误处理·报错链条
jeff聊企业数字化4 天前
私有化即时通讯选型指南:兼顾安全与高效
go·业界资讯·即时通讯