终于,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()
}
使用下面的命令运行示例:
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()
}
与上一章相比,这次的代码有两个实质性变化:
- 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!
这里的 Hello 和 Go-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
使用下面的命令运行示例:
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,而是写 greeting 和 audience, 因为注册构造函数时会指定整体前缀 ${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()
}
使用下面的命令运行示例:
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)
}
使用下面的命令运行示例:
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}")表示构造函数参数RedisConfig从spring.go-redis前缀读取配置;Destroy(CloseRedis)表示容器关闭时调用销毁函数。
咱们还需要在配置文件中增加一个配置项,用于指定 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:"$ != ''"`
}
使用下面的命令运行示例:
bash
cd examples/08-conditional-multi-redis
go run .
然后会看到启动时控制台上打印了 __default__ 创建的日志, 但是并没有 cache 和 queue 创建的日志。 这是因为 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 注册的对象进行装配。 它接受一个回调函数,回调函数的参数是一个结构体,用来注入要检查的对象, 可以使用 autowire 和 value 标签来注入对象或者配置。
使用下面的命令运行测试:
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 感兴趣,那就加入这个项目吧。让我们一起动手打磨,一起推动它不断进化,让它变得更强大。