MSA Requirements for a Go App

  • Configuration management, graceful shutdown
  • Testable code
  • API specifications
  • Logging
  • Profiling, error monitoring, metrics, API tracing

Tiny Main Abstraction

  • Instead of using separate .env files, environment variables are read directly from the OS.
  • A question raised: isn’t setting every OS argument as a flag wasteful?

Graceful Shutdown


Testable Code

  • The speaker waits for the server startup with long polling before running tests—not the cleanest method.
  • Since the speaker is focused on vanilla Go, it seems they deliberately chose not to use the httptest package.

Health Check

  • Version is set using ldflags, which could be automatically tagged during release.
  • Storing server start time is preferred over tracking total uptime.

Documentation is a Must

As Gophers say, “GoDoc isn’t optional—it’s essential.”

  • While the speaker didn’t use godoc, they emphasized exposing OpenAPI specs.
  • go:embed embeds OpenAPI files into the binary at build time. Build fails if the file is missing.
  • Swagger endpoints expose the embedded documentation.

Maintaining Swagger alongside code updates may not be sustainable. Using swaggo might be more intuitive.


Logging is a Must

  • JSON logging with slog is adopted—a must for modern apps.

  • Following the 12-factor app philosophy, logs are written to stdout. This reduces File I/O costs.

  • Fluentbit handles post-processing with multiple outputs:

    • Sentry: Error tracking
    • Jaeger: API tracing
    • Elasticsearch: Log storage for Kibana

Decorate

  • Decorates the response writer to track HTTP status codes and byte sizes.

Reflections

  • This approach contrasts with rigid project structures. The minimal Go style without strict clean architecture was convincing.
  • Some trade-offs are inevitable. Even when using established frameworks, clean and concise code is still achievable.
  • Fluentbit should be adopted company-wide to reduce log coupling in the app.
  • Company-wide tracing adoption (e.g., Jaeger, OpenTelemetry) is a must, potentially mandated by CTOs.

Applying to Production

// HealthHandler : Server health status handler
type HealthHandler struct {
	version   string
	startTime time.Time
}

// NewHealthHandler : Creates a new HealthHandler
func NewHealthHandler(version string) HealthHandler {
	return HealthHandler{
		version:   version,
		startTime: time.Now(),
	}
}

// Check : Returns server status and build metadata
func (h HealthHandler) Check(ctx *gin.Context) {
	type responseBody struct {
		Version        string    `json:"version"`
		Uptime         string    `json:"up_time"`
		LastCommitHash string    `json:"last_commit_hash"`
		LastCommitTime time.Time `json:"last_commit_time"`
		DirtyBuild     bool      `json:"dirty_build"`
	}

	var (
		lastCommitHash string
		lastCommitTime time.Time
		dirtyBuild     bool
	)

	{
		buildInfo, _ := debug.ReadBuildInfo()
		for _, kv := range buildInfo.Settings {
			if kv.Value == "" {
				continue
			}
			switch kv.Key {
			case "vcs.revision":
				lastCommitHash = kv.Value
			case "vcs.time":
				lastCommitTime, _ = time.Parse(time.RFC3339, kv.Value)
			case "vcs.modified":
				dirtyBuild = kv.Value == "true"
			}
		}
	}

	up := time.Now()
	ctx.JSON(http.StatusOK, responseBody{
		Version:        h.version,
		Uptime:         up.Sub(h.startTime).String(),
		LastCommitHash: lastCommitHash,
		LastCommitTime: lastCommitTime,
		DirtyBuild:     dirtyBuild,
	})
}