Build a User CRUD Service from Scratch with Go‑Kratos

This step‑by‑step guide shows how to use the Go‑Kratos microservice framework to create a clean‑architecture user service, covering environment setup, project scaffolding, Protobuf API definition, code generation, layered implementation (biz, data, service), server registration, and testing with curl.

Golang Shines
Golang Shines
Golang Shines
Build a User CRUD Service from Scratch with Go‑Kratos

What is Kratos and why choose it?

Kratos is an open‑source Go microservice development framework created by Bilibili. It aims to let developers focus on business logic without being burdened by infrastructure.

Four Core Advantages

Microservices : independent deployment and evolution via modular layout and service registration/discovery.

Specified API : unified API definition using Protobuf with automatic HTTP/gRPC generation.

Plug‑able : optional tracing, metrics, logging, rate‑limit components.

Environment Preparation (including domestic acceleration)

go version
# Recommended: 1.24+
# Install Kratos CLI (foreign source may be slow)
go install github.com/go-kratos/kratos/cmd/kratos/v2@latest

# ✅ Domestic recommendation: Gitee source (faster)
go install -v github.com/go-kratos/kratos/cmd/kratos/v2@latest

# If pulling fails, set GOPROXY
export GOPROXY=https://goproxy.cn,direct
# Install Protobuf compiler & plugins
brew install protobuf               # macOS
sudo apt install -y protobuf-compiler  # Ubuntu

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v2@latest
go install github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v2@latest
kratos -v
# Example output: kratos version v2.7.2

Create Project Skeleton

# Use Gitee source (domestic friendly)
kratos new user-service -r https://gitee.com/go-kratos/kratos-layout.git

cd user-service
go mod tidy

Generated directory (key parts):

user-service/
├── api/               # ← Protobuf API definitions
├── cmd/
│   └── user-service/  # ← entry point
├── internal/
│   ├── biz/           # ← business logic (UseCase)
│   ├── data/          # ← data access (Repo/DB)
│   ├── service/       # ← service implementation (API binding)
│   └── server/        # ← HTTP/gRPC server registration
├── go.mod
└── Makefile
💡 Key point: google.api.http automatically generates RESTful routes.

Define User API (Protobuf)

syntax = "proto3";

package api.user.v1;
option go_package = "user-service/api/user/v1;v1";

import "google/api/annotations.proto";
import "errors/errors.proto"; // ← custom errors

message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
}

message CreateUserRequest { string name = 1; string email = 2; int32 age = 3; }
message CreateUserReply { User user = 1; }

message GetUserRequest { int64 id = 1; }
message GetUserReply { User user = 1; }

message UpdateUserRequest { int64 id = 1; string name = 2; string email = 3; int32 age = 4; }
message UpdateUserReply { User user = 1; }

message DeleteUserRequest { int64 id = 1; }
message DeleteUserReply { bool ok = 1; }

message ListUserRequest { int32 page = 1; int32 size = 2; }
message ListUserReply { repeated User users = 1; }

extend google.protobuf.MethodOptions {
  kratos.api.Errors errors = 1111; // type‑safe error constructor
}

service UserService {
  rpc CreateUser(CreateUserRequest) returns (CreateUserReply) {
    option (google.api.http) = { post: "/v1/users" body: "*" };
  }
  rpc GetUser(GetUserRequest) returns (GetUserReply) {
    option (google.api.http) = { get: "/v1/users/{id}" };
    option (errors) = { rpc_error: [{ code: 404 reason: "USER_NOT_FOUND" message: "user not found" }] };
  }
  rpc UpdateUser(UpdateUserRequest) returns (UpdateUserReply) {
    option (google.api.http) = { put: "/v1/users/{id}" body: "*" };
  }
  rpc DeleteUser(DeleteUserRequest) returns (DeleteUserReply) {
    option (google.api.http) = { delete: "/v1/users/{id}" };
  }
  rpc ListUser(ListUserRequest) returns (ListUserReply) {
    option (google.api.http) = { get: "/v1/users" };
  }
}
🔍 Key point: errors/errors.proto + option (errors) generates type‑safe error constructors, e.g., v1.ErrorUserNotFound().Err() .

Generate Code (Protobuf → Go)

# Or run manually
kratos proto client api/user/v1/user.proto

Generated files:

api/user/v1/
├── user.pb.go          # Protobuf serialization
├── user_grpc.pb.go     # gRPC stub
└── user_http.pb.go     # HTTP route binding (with parameter parsing)
✅ At this point HTTP/gRPC dual‑protocol interfaces are generated automatically – no manual routing needed!

Layered Implementation: CRUD Logic

1. Business Model & Interface (internal/biz/user.go)

package biz

import "context"

type User struct {
    ID    int64
    Name  string
    Email string
    Age   int32
}

// Repository interface
type UserRepo interface {
    Save(context.Context, *User) (*User, error)
    Get(context.Context, int64) (*User, error)
    Update(context.Context, *User) (*User, error)
    Delete(context.Context, int64) error
    List(context.Context, int32, int32) ([]*User, error)
}

// UseCase
type UserUsecase struct{ repo UserRepo }

func NewUserUsecase(repo UserRepo) *UserUsecase { return &UserUsecase{repo: repo} }

func (uc *UserUsecase) Create(ctx context.Context, u *User) (*User, error) { return uc.repo.Save(ctx, u) }
func (uc *UserUsecase) Get(ctx context.Context, id int64) (*User, error) { return uc.repo.Get(ctx, id) }
// Update, Delete, List omitted for brevity

2. Data Layer (in‑memory) – internal/data/user.go

package data

import (
    "context"
    "sync/atomic"
    "user-service/internal/biz"
)

type userRepo struct{ data map[int64]*biz.User; seq int64 }

func NewUserRepo() biz.UserRepo { return &userRepo{data: make(map[int64]*biz.User)} }

func (r *userRepo) Save(_ context.Context, u *biz.User) (*biz.User, error) {
    id := atomic.AddInt64(&r.seq, 1)
    u.ID = id
    r.data[id] = u
    return u, nil
}

func (r *userRepo) Get(_ context.Context, id int64) (*biz.User, error) {
    u, ok := r.data[id]
    if !ok { return nil, nil }
    return u, nil
}

func (r *userRepo) Update(_ context.Context, u *biz.User) (*biz.User, error) {
    if _, ok := r.data[u.ID]; !ok { return nil, nil }
    r.data[u.ID] = u
    return u, nil
}

func (r *userRepo) Delete(_ context.Context, id int64) error { delete(r.data, id); return nil }

func (r *userRepo) List(_ context.Context, page, size int32) ([]*biz.User, error) {
    var users []*biz.User
    for _, u := range r.data { users = append(users, u) }
    return users, nil
}

3. Service Layer – internal/service/user.go

package service

import (
    "context"
    v1 "user-service/api/user/v1"
    "user-service/internal/biz"
)

type UserService struct{ v1.UnimplementedUserServiceServer; uc *biz.UserUsecase }

func NewUserService(uc *biz.UserUsecase) *UserService { return &UserService{uc: uc} }

func (s *UserService) CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.CreateUserReply, error) {
    u := &biz.User{Name: req.Name, Email: req.Email, Age: req.Age}
    created, err := s.uc.Create(ctx, u)
    if err != nil { return nil, err }
    return &v1.CreateUserReply{User: convertUser(created)}, nil
}

func (s *UserService) GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.GetUserReply, error) {
    u, err := s.uc.Get(ctx, req.Id)
    if err != nil { return nil, err }
    if u == nil { return nil, v1.ErrorUserNotFound("user not found").Err() }
    return &v1.GetUserReply{User: convertUser(u)}, nil
}

// UpdateUser, DeleteUser, ListUser omitted for brevity

func convertUser(u *biz.User) *v1.User {
    return &v1.User{Id: u.ID, Name: u.Name, Email: u.Email, Age: u.Age}
}

Register Service & Start

1. Modify internal/server/http.go

// In NewHTTPServer add:
v1.RegisterUserServiceHTTPServer(srv, service.NewUserService(uc))

2. Modify internal/server/grpc.go

// In NewGRPCServer add:
v1.RegisterUserServiceServer(srv, service.NewUserService(uc))

3. Initialise dependencies in main()

repo := data.NewUserRepo()
uc   := biz.NewUserUsecase(repo)
svc  := service.NewUserService(uc)

app := kratos.New(
    kratos.Name("user-service"),
    kratos.Server(httpSrv, grpcSrv),
)
✅ Recommended: use Wire for dependency injection (wire.go generated).

Run & Test

# Start service
go run ./cmd/user-service

# Default ports:
#   HTTP: :8000
#   gRPC: :9000

Test with curl (HTTP)

# Create
curl -X POST http://localhost:8000/v1/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Tom","email":"[email protected]","age":30}'
# → {"user":{"id":1,"name":"Tom","email":"[email protected]","age":30}}

# Get
curl http://localhost:8000/v1/users/1

# Update
curl -X PUT http://localhost:8000/v1/users/1 \
  -d '{"name":"Tom Lee","age":31}'

# List
curl "http://localhost:8000/v1/users?page=1&size=10"

# Delete
curl -X DELETE http://localhost:8000/v1/users/1

Conclusion

The article walks through the complete lifecycle of building a Go‑Kratos user service: from environment setup, project scaffolding, Protobuf‑first API design, automatic code generation, clean‑architecture layered implementation, server registration, to practical testing. It also highlights the use of type‑safe error handling and suggests Wire for dependency injection.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

microservicesgogRPCProtobufClean ArchitectureHTTPCRUDKratos
Golang Shines
Written by

Golang Shines

We share daily the latest Golang technical articles, practical resources, language news, tutorials, and real-world projects to help everyone learn and improve.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.