e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
115 lines
4.3 KiB
Markdown
115 lines
4.3 KiB
Markdown
---
|
|
layout: blog
|
|
title: "Developer Experience Cleanup: One Way to Do Things"
|
|
permalink: /blog/5
|
|
description: "Unified service creation, cleaner handler registration, and modular monolith support — the Go Micro DX overhaul"
|
|
---
|
|
|
|
# Developer Experience Cleanup: One Way to Do Things
|
|
|
|
<img src="/images/generated/blog-dx.jpg" alt="Developer Experience Cleanup: One Way to Do Things" style="width: 100%; border-radius: 8px; margin: 1rem 0 1.5rem;" />
|
|
|
|
*March 4, 2026 — By the Go Micro Team*
|
|
|
|
Go Micro has always prioritized getting out of your way. But over time, the API accumulated multiple ways to do the same thing — `micro.New(name)`, `micro.NewService(micro.Name(...))`, `service.New()`, three different handler registration patterns. If you're building something for AI agents or running a modular monolith, you shouldn't have to choose between equivalent APIs.
|
|
|
|
We've cleaned it up. Here's what changed and why.
|
|
|
|
## One Way to Create a Service
|
|
|
|
Before, there were three ways to create a service:
|
|
|
|
```go
|
|
// Old: three equivalent patterns
|
|
service := micro.NewService("greeter") // name only
|
|
service := micro.NewService(micro.Name("greeter")) // options only
|
|
service := service.New(service.Name("greeter")) // internal package
|
|
```
|
|
|
|
Now there's one canonical pattern:
|
|
|
|
```go
|
|
service := micro.NewService("greeter")
|
|
service := micro.NewService("greeter", micro.Address(":8080"))
|
|
```
|
|
|
|
Name is always the first argument. Options follow. `micro.New` still works as a deprecated alias, but every example, doc, and guide now uses `micro.NewService("name")`.
|
|
|
|
## Clean Handler Registration
|
|
|
|
Registering handlers used to require reaching through to the server:
|
|
|
|
```go
|
|
// Old: verbose, leaks abstraction
|
|
handler := service.Server().NewHandler(
|
|
&TaskService{tasks: make(map[string]*Task)},
|
|
server.WithEndpointScopes("TaskService.Create", "tasks:write"),
|
|
)
|
|
service.Server().Handle(handler)
|
|
```
|
|
|
|
Now `service.Handle()` accepts handler options directly:
|
|
|
|
```go
|
|
// New: clean, one call
|
|
service.Handle(
|
|
&TaskService{tasks: make(map[string]*Task)},
|
|
server.WithEndpointScopes("TaskService.Create", "tasks:write"),
|
|
)
|
|
```
|
|
|
|
For the common case with no options, it's just:
|
|
|
|
```go
|
|
service.Handle(new(Greeter))
|
|
```
|
|
|
|
## Modular Monoliths with Service Groups
|
|
|
|
Run multiple services in a single binary. Each service gets isolated state (server, client, store, cache) while sharing infrastructure (registry, broker, transport):
|
|
|
|
```go
|
|
users := micro.NewService("users", micro.Address(":9001"))
|
|
orders := micro.NewService("orders", micro.Address(":9002"))
|
|
|
|
users.Handle(new(Users))
|
|
orders.Handle(new(Orders))
|
|
|
|
g := micro.NewGroup(users, orders)
|
|
g.Run()
|
|
```
|
|
|
|
Start as a monolith, split into separate binaries when you need independent scaling. The Group handles signals and coordinated shutdown — all services start together and stop together.
|
|
|
|
## MCP Integration in One Line
|
|
|
|
Every service is automatically an MCP tool. Add a gateway alongside your service with one option:
|
|
|
|
```go
|
|
service := micro.NewService("greeter",
|
|
micro.Address(":9090"),
|
|
mcp.WithMCP(":3000"),
|
|
)
|
|
|
|
service.Handle(new(Greeter))
|
|
service.Run()
|
|
```
|
|
|
|
Your Go comments become tool descriptions. Your struct tags become parameter schemas. No glue code.
|
|
|
|
## Bug Fixes
|
|
|
|
- **Stop() error handling**: Previously, `Stop()` would silently swallow errors from `BeforeStop` hooks. Now all errors are properly propagated.
|
|
- **Store initialization**: Fatal-level log on store init failure changed to error-level — a store init failure shouldn't crash your service.
|
|
- **Service interface**: The internal implementation is now properly unexported. Users interact through the `service.Service` interface, not a concrete type.
|
|
|
|
## What This Means for You
|
|
|
|
If you're building new services, use `micro.NewService("name", opts...)` and `service.Handle()`. That's it.
|
|
|
|
If you have existing code using `micro.New("name")` or `service.Server().Handle()`, it still works. For v6, update name-less `micro.NewService(opts...)` calls to `micro.NewService("name", opts...)`. The docs, examples, and guides all point to the new patterns now.
|
|
|
|
The goal is simple: when someone asks "how do I create a service?", there should be exactly one answer.
|
|
|
|
See the updated [Getting Started guide](https://go-micro.dev/docs/getting-started.html) and the [agent demo](https://github.com/micro/go-micro/tree/master/examples/agent-demo) for working examples.
|