Go Web Frameworks and RPC Notes
A study guide to the mainstream Go frameworks for beginners and interview preparation, covering the core concepts, usage, and comparisons of Gin, Eino, GoZero, gRPC, and Protobuf
Go Web Frameworks and RPC Notes
This article compiles the most commonly used web frameworks and RPC technologies in the Go ecosystem, suitable for readers with some Go experience who want systematic learning and interview preparation.
I. Gin — The Most Popular Go Web Framework
1.1 What Is Gin
Gin is an HTTP web framework written in Go, characterized by its high performance and minimalist API. Its API design was inspired by Martini (another Go framework), but it is about 40 times faster than Martini. Gin uses httprouter as its routing engine and optimizes the routing tree heavily under the hood.
In a nutshell: Gin is the “Spring Boot” or “Express” of the Go world — it lets you quickly build RESTful API services.
1.2 Why Choose Gin
- Fast routing: Based on a Radix Tree (compressed prefix tree); route lookup has time complexity O(k), where k is the URL path length
- Mature middleware mechanism: Logging, authentication, rate limiting, CORS, and more can all be implemented with middleware
- Active community: 75k+ stars on GitHub, with a rich ecosystem of plugins
- Complete documentation: Many official examples and fewer pitfalls
- JSON validation: Built-in data binding and validation, saving you the trouble of manual parsing
- Error recovery: Built-in Recovery middleware so a panic won’t crash the entire process
1.3 Quick Start
Install:
go get -u github.com/gin-gonic/ginThe simplest Hello World:
package main
import ( "net/http" "github.com/gin-gonic/gin")
func main() { // Create the default engine (comes with Logger and Recovery middleware) r := gin.Default()
// Define a GET route r.GET("/hello", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "Hello, Gin!", }) })
// Start the server, listening on :8080 by default r.Run(":8080")}After running, visit http://localhost:8080/hello and you will see the JSON response {"message": "Hello, Gin!"}.
gin.Default() vs gin.New():
// gin.Default() = gin.New() + Logger() + Recovery()r := gin.Default()
// gin.New() has no middleware at all; you have full controlr := gin.New()r.Use(gin.Logger()) // Add logging manuallyr.Use(gin.Recovery()) // Add panic recovery manually1.4 Routing and Request Handling
Gin supports all HTTP methods and also supports route grouping:
func main() { r := gin.Default()
// Basic routes r.GET("/users", getUsers) r.POST("/users", createUser) r.PUT("/users/:id", updateUser) // Path parameter r.DELETE("/users/:id", deleteUser)
// Route grouping — very convenient for managing API versions v1 := r.Group("/api/v1") { v1.GET("/articles", getArticles) v1.POST("/articles", createArticle) }
v2 := r.Group("/api/v2") { v2.GET("/articles", getArticlesV2) }
r.Run()}Path parameters vs query parameters:
// Path parameter: /user/123r.GET("/user/:id", func(c *gin.Context) { id := c.Param("id") // "123"})
// Query parameter: /search?keyword=go&page=1r.GET("/search", func(c *gin.Context) { keyword := c.DefaultQuery("keyword", "") // Has a default value page := c.Query("page") // No default value})Wildcard routes:
// *filepath matches all paths under /assets// e.g. /assets/css/style.css → filepath = "/css/style.css"r.Static("/assets", "./public")
// Manual wildcardr.GET("/files/*filepath", func(c *gin.Context) { filepath := c.Param("filepath") // "/docs/readme.md" c.File("./public" + filepath)})Route priority: Gin’s route matching follows a strict priority — static routes > parameter routes > wildcard routes. For example, /user/list takes priority over /user/:id.
1.5 Parameter Binding and Validation
This is one of Gin’s most powerful features — it can automatically bind request data to structs:
type LoginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required,min=6"` Email string `json:"email" binding:"omitempty,email"`}
r.POST("/login", func(c *gin.Context) { var req LoginRequest // ShouldBindJSON automatically parses the JSON request body and validates it if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Binding succeeded; use req.Username, req.Password directly c.JSON(http.StatusOK, gin.H{"message": "Login successful"})})Common validation tags (binding tag):
| Tag | Meaning |
|---|---|
required | Required |
min=N / max=N | Minimum / maximum length or value |
email | Email format |
oneof=a b c | Enum value, must be one of them |
gte=0 / lte=100 | Greater than or equal to / less than or equal to |
datetime=2006-01-02 | Date format (Go’s time format must use this reference time) |
len=N | Exact length |
contains=xxx | Must contain the substring |
startswith=xxx / endswith=xxx | Prefix / suffix |
Binding methods for different data sources:
// JSON request bodyc.ShouldBindJSON(&req)
// URL query parametersc.ShouldBindQuery(&req)
// Form data (application/x-www-form-urlencoded)c.ShouldBind(&req)
// Path parameters must be retrieved manually; automatic binding to struct is not supportedid := c.Param("id")
// Multi-source binding (query first, then form)c.ShouldBindWith(&req, binding.Query)Custom validators:
// Register a custom validation: check whether age is within a reasonable rangevalidate := validator.New()validate.RegisterValidation("age", func(fl validator.FieldLevel) bool { age := fl.Field().Int() return age >= 0 && age <= 150})
type User struct { Age int `json:"age" binding:"required,age"` // Use the custom validation}1.6 Middleware
Middleware is code that runs before (or after) a request reaches the handler function, commonly used for logging, authentication, rate limiting, and so on.
// Custom middlewarefunc AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") if token == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Not logged in"}) c.Abort() // Terminate the request chain return } // Token verification logic... c.Set("userId", 123) // Put the parsed user ID into the context c.Next() // Continue to the next handler }}
func main() { r := gin.Default()
// Global middleware r.Use(corsMiddleware())
// Middleware on a route group auth := r.Group("/api", AuthMiddleware()) { auth.GET("/profile", getProfile) }}Middleware execution order: Request → Middleware1 (c.Next()) → Middleware2 (c.Next()) → Handler → Middleware2 remainder → Middleware1 remainder. Similar to an onion model.
CORS middleware in practice:
func CORSMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return }
c.Next() }}Rate limiting middleware (token bucket):
import "golang.org/x/time/rate"
func RateLimitMiddleware(r rate.Limit, b int) gin.HandlerFunc { limiter := rate.NewLimiter(r, b) return func(c *gin.Context) { if !limiter.Allow() { c.JSON(http.StatusTooManyRequests, gin.H{"error": "Too many requests"}) c.Abort() return } c.Next() }}
// Usage: 10 requests per second, max burst of 20r.Use(RateLimitMiddleware(10, 20))1.7 Gin’s Context
gin.Context is Gin’s most central object — it encapsulates all operations on the request and response:
func handler(c *gin.Context) { // Get request information method := c.Request.Method path := c.Request.URL.Path ip := c.ClientIP() ua := c.GetHeader("User-Agent")
// Set / get context values (commonly used to pass values from middleware to handlers) c.Set("key", "value") val, exists := c.Get("key")
// Response c.JSON(200, gin.H{"status": "ok"}) // JSON c.String(200, "hello") // Plain text c.HTML(200, "index.html", data) // HTML template c.Redirect(301, "/new-path") // Redirect
// File upload file, _ := c.FormFile("file") c.SaveUploadedFile(file, "./uploads/"+file.Filename)}Unified response format wrapper:
type Response struct { Code int `json:"code"` Message string `json:"message"` Data interface{} `json:"data,omitempty"`}
func OK(c *gin.Context, data interface{}) { c.JSON(http.StatusOK, Response{Code: 0, Message: "success", Data: data})}
func Fail(c *gin.Context, code int, msg string) { c.JSON(http.StatusOK, Response{Code: code, Message: msg})}
// Usager.GET("/user/:id", func(c *gin.Context) { // Business logic... OK(c, user)})1.8 Graceful Shutdown
In production, you cannot simply call r.Run(); you need to handle graceful shutdown, waiting for in-flight requests to finish:
import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time")
func main() { r := gin.Default() r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) })
srv := &http.Server{ Addr: ":8080", Handler: r, }
// Start the server in a goroutine go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Failed to start server: %v", err) } }()
// Wait for interrupt signal quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down server...")
// Allow 5 seconds to handle remaining requests ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()
if err := srv.Shutdown(ctx); err != nil { log.Fatalf("Server shutdown error: %v", err) } log.Println("Server shut down safely")}1.9 File Upload and Download
// Single file uploadr.POST("/upload", func(c *gin.Context) { file, err := c.FormFile("file") if err != nil { c.JSON(400, gin.H{"error": "File upload failed"}) return }
// Limit file size (10MB) if file.Size > 10<<20 { c.JSON(400, gin.H{"error": "File cannot exceed 10MB"}) return }
// Save the file dst := fmt.Sprintf("./uploads/%d_%s", time.Now().Unix(), file.Filename) if err := c.SaveUploadedFile(file, dst); err != nil { c.JSON(500, gin.H{"error": "Save failed"}) return } c.JSON(200, gin.H{"path": dst})})
// Multiple file uploadr.POST("/upload/multiple", func(c *gin.Context) { form, _ := c.MultipartForm() files := form.File["files"] // Form field name
for _, file := range files { dst := fmt.Sprintf("./uploads/%s", file.Filename) c.SaveUploadedFile(file, dst) } c.JSON(200, gin.H{"count": len(files)})})
// File downloadr.GET("/download/:filename", func(c *gin.Context) { filename := c.Param("filename") filepath := "./uploads/" + filename c.Header("Content-Disposition", "attachment; filename="+filename) c.File(filepath)})1.10 Gin Testing
Gin provides testing support without needing to actually start an HTTP server:
import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert")
func setupRouter() *gin.Engine { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) }) return r}
func TestPing(t *testing.T) { router := setupRouter()
// Create a test request req, _ := http.NewRequest("GET", "/ping", nil) w := httptest.NewRecorder()
// Execute the request router.ServeHTTP(w, req)
// Assertions assert.Equal(t, 200, w.Code) assert.Contains(t, w.Body.String(), "pong")}1.11 Common Interview Questions
Q: Why is Gin’s routing fast? A: Gin uses httprouter, which under the hood is a compressed Radix Tree (compressed prefix tree). Compared to the standard library’s net/http linear matching, the Radix Tree merges nodes by common prefixes, reducing lookup efficiency from O(n) to O(k), where k is the path depth.
Q: What’s the difference between Gin’s middleware and Go’s native http.Handler? A: Gin middleware is a gin.HandlerFunc that controls the flow via c.Next() and c.Abort(), which is more elegant than the native chained Handler. The native approach requires manual nesting or closures.
Q: What’s the difference between ShouldBind and Bind? A: Bind automatically returns a 400 response and calls c.Abort() on validation failure; ShouldBind only returns an error and does not auto-respond. ShouldBind is recommended because it lets you customize the error response format.
Q: Is gin.Context thread-safe? A: No. Gin’s Context is reused (via the object pool sync.Pool) and reclaimed after the request ends. So you cannot use c directly inside a goroutine; you must call c.Copy() first or extract the values you need.
Q: How does Gin handle panics? A: Gin’s built-in Recovery() middleware catches panics and returns a 500 error without crashing the entire process. It is implemented using the defer + recover mechanism.
II. Protobuf — An Efficient Data Serialization Protocol
2.1 Why Protobuf Is Needed
In a microservices architecture, services need to communicate with each other. Common approaches include:
- JSON: Human-readable, but bulky and slow to parse
- XML: Even more verbose, basically unused nowadays
- Protobuf: Binary format, small size and fast parsing; the default serialization for gRPC
Comparison:
| Feature | JSON | Protobuf |
|---|---|---|
| Readability | Human-readable | Binary, not directly readable |
| Size | Larger | 3-10x smaller |
| Parsing speed | Slow | 5-100x faster |
| Strong typing | No | Yes |
| Requires IDL | No | Yes (.proto file) |
| Forward compatibility | Naturally supported | Supported via field numbers |
In a nutshell: Protobuf is “binary JSON” — you define data structures in a .proto file and then automatically generate code for various languages.
2.2 Installation and Configuration
First install the protoc compiler:
# macOSbrew install protobuf
# Linuxsudo apt install protobuf-compiler
# Windows (scoop)scoop install protobuf
# Verify installationprotoc --versionThen install Go’s protoc plugins:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latestgo install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestMake sure $GOPATH/bin is in your PATH.
2.3 .proto File Syntax
Create a user.proto file:
// Specify the protobuf versionsyntax = "proto3";
// Go package path for the generated codeoption go_package = "./pb";
// Package name (prevents message name conflicts across projects)package user;
// Define a message (similar to a Go struct)message User { int64 id = 1; // Field number 1, not a default value string name = 2; string email = 3; int32 age = 4; Gender gender = 5; // Use an enum type repeated string hobbies = 6; // repeated = list / slice map<string, string> metadata = 7; // map type}
// Enumenum Gender { GENDER_UNSPECIFIED = 0; // proto3 requires the first value to be 0 GENDER_MALE = 1; GENDER_FEMALE = 2;}
// Request and response messages (typically used for RPC definitions)message GetUserRequest { int64 id = 1;}
message GetUserResponse { User user = 1;}
message ListUsersRequest { int32 page = 1; int32 page_size = 2;}
message ListUsersResponse { repeated User users = 1; int32 total = 2;}
// Define a service (for gRPC)service UserService { rpc GetUser(GetUserRequest) returns (GetUserResponse); rpc ListUsers(ListUsersRequest) returns (ListUsersResponse); rpc CreateUser(User) returns (User);}2.4 Generate Go Code
protoc --go_out=. --go-grpc_out=. user.protoThis generates two files in the ./pb/ directory:
user.pb.go— message structs and serialization methodsuser_grpc.pb.go— gRPC service interfaces and client code
2.5 Core Concepts Explained
Field numbers: The = 1, = 2 after each field are not default values but field numbers, used for binary encoding. Once published, they must not be changed, otherwise existing data will fail to deserialize. When deleting a field, use reserved to keep the number:
message Example { reserved 2, 15, 9 to 11; reserved "old_field_name"; string new_field = 1;}Field number encoding rules:
- Numbers 1-15: occupy only 1 byte after encoding (give these to high-frequency fields first)
- Numbers 16-2047: occupy 2 bytes
- Numbers 19000-19999 cannot be used (reserved for internal Proto use)
proto3 vs proto2:
- proto3 is more concise and does not need
required/optionalkeywords (all fields are optional) - proto3’s default values are all zero values (0, "", false, nil), making it impossible to distinguish “not set” from ” set to zero value”
- If you need to distinguish, use the
optionalkeyword or Google’s wrapper types - proto2 supports
required,defaultvalue settings, andgrouptypes
oneof: a mutually exclusive field, only one of which can be set at a time:
message Payment { oneof payment_method { CreditCard credit_card = 1; Alipay alipay = 2; WechatPay wechat = 3; }}Nested messages:
message Outer { message Inner { string value = 1; } Inner inner = 1;}
// In Go: outer.Inner.Valueimport — importing other proto files:
import "google/protobuf/timestamp.proto";
message Event { string name = 1; google.protobuf.Timestamp created_at = 2;}Well-Known Types (common built-in types):
import "google/protobuf/wrappers.proto";import "google/protobuf/timestamp.proto";import "google/protobuf/duration.proto";import "google/protobuf/empty.proto";
message Example { // Wrapper types: can distinguish "not set" from "zero value" google.protobuf.StringValue name = 1; // nil means not set, "" means empty string google.protobuf.Int32Value count = 2; // nil means not set, 0 means zero
// Time and duration google.protobuf.Timestamp created_at = 3; google.protobuf.Duration timeout = 4;
// Empty message (for RPC methods with no parameters) // rpc HealthCheck(google.protobuf.Empty) returns (google.protobuf.Empty);}2.6 Using Protobuf in Go
package main
import ( "fmt" "log"
pb "your-module/pb" "google.golang.org/protobuf/proto")
func main() { // Create a message user := &pb.User{ Id: 1, Name: "张三", Email: "zhangsan@example.com", Age: 25, Gender: pb.Gender_GENDER_MALE, Hobbies: []string{"编程", "游戏"}, Metadata: map[string]string{ "city": "北京", }, }
// Serialize to binary data, err := proto.Marshal(user) if err != nil { log.Fatal(err) } fmt.Printf("Serialized size: %d bytes\n", len(data))
// Deserialize newUser := &pb.User{} if err := proto.Unmarshal(data, newUser); err != nil { log.Fatal(err) } fmt.Printf("User: %+v\n", newUser)
// JSON serialization (for debugging) jsonData, _ := protojson.Marshal(user) fmt.Println(string(jsonData))}2.7 Common Interview Questions
Q: How does Protobuf achieve efficient encoding? A: The core is variable-length encoding (Varint) and field numbers. Integer types use variable-length encoding, so small numbers occupy only 1 byte; fields are identified only by number, not by name (JSON sends the full key every time), greatly reducing transmission size.
Q: Why can’t field numbers be changed? A: Because in Protobuf’s binary format, data is identified by field number. If the number changes, old data will be deserialized into the wrong field. So new fields use new numbers, and deprecated fields should be reserved.
Q: Why doesn’t proto3 have required? A: Google’s experience showed that required fields cause serious compatibility issues during version evolution. Once marked as required, it cannot later be changed to optional without breaking existing code. So proto3 removed it uniformly, and all fields are optional.
Q: How to solve Protobuf’s zero-value problem? A: In proto3, all fields have zero-value defaults, making it impossible to distinguish “not set” from “set to zero value”. Solutions: ① use the optional keyword (proto3.15+); ② use the Well-Known Types wrapper types (e.g. google.protobuf.StringValue); ③ use oneof wrapping.
III. gRPC — A High-Performance RPC Framework
3.1 What Is RPC
RPC (Remote Procedure Call) lets you call a remote service as if it were a local function. You write a GetUser(id) function, and gRPC handles all the low-level details — network communication, serialization, deserialization, and so on.
In a nutshell: gRPC is an RPC framework open-sourced by Google, based on HTTP/2 and Protobuf, faster and more standardized than RESTful APIs.
3.2 Why gRPC Instead of REST
| Feature | RESTful API | gRPC |
|---|---|---|
| Protocol | HTTP/1.1 (mostly) | HTTP/2 |
| Data format | JSON (text) | Protobuf (binary) |
| Performance | Average | 2-10x faster |
| Interface definition | No enforced spec (Swagger optional) | Must define with .proto |
| Streaming | Not supported (needs WebSocket) | Native support |
| Browser support | Native | Needs a gRPC-Web proxy |
| Code generation | Optional | Automatic |
When to use gRPC:
- Internal microservice communication (inter-service calls)
- Need high performance and low latency
- Need streaming (real-time push, large file transfer)
- Polyglot architecture (Go, Java, Python services interoperating)
When to use REST:
- Public APIs exposed externally
- Need direct browser calls
- Simple CRUD scenarios
3.3 gRPC’s Four Communication Modes
Mode 1: Unary RPC (most common)
The client sends one request and the server returns one response. Same as a normal function call.
rpc GetUser(GetUserRequest) returns (GetUserResponse);Mode 2: Server Streaming RPC
The client sends one request and the server returns a stream of data (multiple messages).
// e.g.: the client requests logs for a time range, and the server keeps pushingrpc WatchLogs(WatchLogsRequest) returns (stream LogEntry);Mode 3: Client Streaming RPC
The client sends a stream of data and the server returns one response.
// e.g.: the client uploads a large filerpc UploadFile(stream FileChunk) returns (UploadResponse);Mode 4: Bidirectional Streaming RPC
Both client and server can send streams of data, and the two streams operate independently.
// e.g.: real-time chatrpc Chat(stream ChatMessage) returns (stream ChatMessage);3.4 In Practice: Writing a User Service with gRPC
Step 1: Define the proto file (already written above, reused here)
Step 2: Generate code
protoc --go_out=. --go-grpc_out=. user.protoStep 3: Implement the server
package main
import ( "context" "log" "net"
pb "your-module/pb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status")
// Implement the UserService interfacetype server struct { pb.UnimplementedUserServiceServer // Embed this for forward compatibility users map[int64]*pb.User}
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) { user, ok := s.users[req.Id] if !ok { return nil, status.Errorf(codes.NotFound, "User %d does not exist", req.Id) } return &pb.GetUserResponse{User: user}, nil}
func (s *server) CreateUser(ctx context.Context, user *pb.User) (*pb.User, error) { s.users[user.Id] = user return user, nil}
func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("Failed to listen: %v", err) }
grpcServer := grpc.NewServer() pb.RegisterUserServiceServer(grpcServer, &server{ users: make(map[int64]*pb.User), })
log.Println("gRPC service started, listening on :50051") if err := grpcServer.Serve(lis); err != nil { log.Fatalf("Service failed: %v", err) }}Step 4: Implement the client
package main
import ( "context" "log" "time"
pb "your-module/pb" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure")
func main() { // Establish a connection conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { log.Fatalf("Connection failed: %v", err) } defer conn.Close()
// Create a client client := pb.NewUserServiceClient(conn)
// Set a timeout ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel()
// Call the remote method — just like calling a local function resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 1}) if err != nil { log.Printf("Call failed: %v", err) return } log.Printf("User: %v", resp.User)}3.5 gRPC Interceptors (Middleware)
gRPC implements middleware via Interceptors, with the same idea as Gin’s middleware:
// Unary interceptorfunc loggingInterceptor( ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,) (interface{}, error) { start := time.Now() log.Printf("Calling method: %s", info.FullMethod)
resp, err := handler(ctx, req)
log.Printf("Method %s took: %v", info.FullMethod, time.Since(start)) return resp, err}
// Register the interceptorgrpcServer := grpc.NewServer( grpc.UnaryInterceptor(loggingInterceptor),)Chained interceptors (combining multiple interceptors):
import "google.golang.org/grpc/middleware"
grpcServer := grpc.NewServer( grpc.ChainUnaryInterceptor( loggingInterceptor, // Logging authInterceptor, // Authentication recoveryInterceptor, // panic recovery ),)Client interceptor:
func clientInterceptor( ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption,) error { start := time.Now() log.Printf("Client calling: %s", method)
err := invoker(ctx, method, req, reply, cc, opts...)
log.Printf("Method %s took: %v", method, time.Since(start)) return err}
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(clientInterceptor),)3.6 gRPC Error Handling
gRPC has a standardized error code system:
import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status")
// Server returns an errorfunc (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) { if req.Id <= 0 { return nil, status.Errorf(codes.InvalidArgument, "ID must be greater than 0, received: %d", req.Id) } user, ok := s.users[req.Id] if !ok { return nil, status.Errorf(codes.NotFound, "User %d does not exist", req.Id) } return &pb.GetUserResponse{User: user}, nil}
// Client parses the errorresp, err := client.GetUser(ctx, req)if err != nil { st, ok := status.FromError(err) if ok { switch st.Code() { case codes.NotFound: log.Println("User does not exist") case codes.InvalidArgument: log.Println("Invalid argument:", st.Message()) case codes.Unavailable: log.Println("Service unavailable") default: log.Printf("Unknown error: %v", st.Err()) } }}Common error codes:
| Error code | Meaning | HTTP equivalent |
|---|---|---|
OK | Success | 200 |
InvalidArgument | Invalid argument | 400 |
NotFound | Resource does not exist | 404 |
AlreadyExists | Resource already exists | 409 |
PermissionDenied | No permission | 403 |
Unauthenticated | Not authenticated | 401 |
Internal | Internal server error | 500 |
Unavailable | Service unavailable | 503 |
DeadlineExceeded | Timeout | 504 |
3.7 Streaming RPC in Practice
Server streaming:
service UserService { rpc WatchUsers(WatchUsersRequest) returns (stream User);}// Server implementationfunc (s *server) WatchUsers(req *pb.WatchUsersRequest, stream pb.UserService_WatchUsersServer) error { for _, user := range s.users { if err := stream.Send(user); err != nil { return err } time.Sleep(time.Second) // Simulate real-time push } return nil}
// Client callstream, err := client.WatchUsers(ctx, &pb.WatchUsersRequest{})for { user, err := stream.Recv() if err == io.EOF { break // Stream ended } if err != nil { log.Fatal(err) } fmt.Printf("Received user: %s\n", user.Name)}Bidirectional streaming (chat example):
// Serverfunc (s *server) Chat(stream pb.UserService_ChatServer) error { for { msg, err := stream.Recv() if err == io.EOF { return nil } if err != nil { return err } // Reply to the message reply := &pb.ChatMessage{ From: "server", Content: "Received: " + msg.Content, } if err := stream.Send(reply); err != nil { return err } }}
// Clientstream, _ := client.Chat(ctx)
// Goroutine that sends messagesgo func() { for _, msg := range messages { stream.Send(msg) } stream.CloseSend()}()
// Goroutine that receives messagesfor { reply, err := stream.Recv() if err == io.EOF { break } fmt.Println(reply.Content)}3.8 Running gRPC Alongside REST (gRPC-Gateway)
In real projects, you often need to support both gRPC and REST. gRPC-Gateway can automatically proxy a gRPC service into a REST API:
import "google/api/annotations.proto";
service UserService { rpc GetUser(GetUserRequest) returns (GetUserResponse) { option (google.api.http) = { get: "/api/v1/users/{id}" }; } rpc CreateUser(User) returns (User) { option (google.api.http) = { post: "/api/v1/users" body: "*" }; }}// Start both gRPC and HTTPfunc main() { // gRPC service grpcServer := grpc.NewServer() pb.RegisterUserServiceServer(grpcServer, &server{})
// HTTP proxy gwMux := runtime.NewServeMux() pb.RegisterUserServiceHandlerFromEndpoint(ctx, gwMux, ":50051", opts)
// HTTP server (forwards REST requests to gRPC) httpServer := &http.Server{ Addr: ":8080", Handler: gwMux, }
go grpcServer.Serve(lis) httpServer.ListenAndServe()}3.9 Common Interview Questions
Q: What protocol does gRPC use? A: It is based on HTTP/2, so it natively supports multiplexing (multiple requests in parallel over one connection), header compression, and server push.
Q: Why is gRPC faster than REST? A: Three reasons — ① Protobuf binary encoding is smaller and faster to parse than JSON text encoding; ② HTTP/2 multiplexing reduces connection setup overhead; ③ strongly-typed interface definitions mean no runtime parsing and validation is needed.
Q: What is UnimplementedXXXServer for? A: This is the default implementation auto-generated by protoc-gen-go-grpc. By embedding it, if your struct does not implement all the methods defined in the proto, it will not cause a compile error but instead return a “method not implemented” error. This way, when a proto adds new methods, old server code will not fail to compile, ensuring forward compatibility.
Q: How is gRPC error handling done? A: gRPC has a standard error code system (the codes package), including 16 types such as OK, NotFound, InvalidArgument, Internal, etc. Errors are returned via status.Errorf(codes.NotFound, "msg"), and the client parses them via status.FromError(err).
Q: Is the gRPC connection long-lived or short-lived? A: Long-lived. gRPC is based on HTTP/2, maintaining a single TCP long connection between client and server; all RPC calls reuse this connection (HTTP/2 multiplexing). This is far more efficient than REST establishing a new connection on every request.
Q: How does gRPC achieve load balancing? A: Two approaches — ① Client-side load balancing: the client knows all server addresses and chooses which to connect to itself (gRPC has built-in strategies such as round-robin); ② Proxy load balancing: requests are forwarded through a proxy such as Envoy or Nginx.
IV. GoZero — Microservice Framework
4.1 What Is GoZero
GoZero (go-zero) is a microservice framework open-sourced by TAL Education (the open-source team at ByteDance), positioned as a one-stop microservice solution. It is not just a web framework, but integrates:
- Web services (similar to Gin)
- RPC services (based on gRPC)
- Service registration and discovery
- Load balancing
- Circuit breaking and rate limiting
- Distributed tracing
- Configuration management
- Code generation tool (goctl)
In a nutshell: If Gin is a “wheel”, GoZero is a “complete car” — you don’t need to assemble various middleware yourself; it works out of the box.
4.2 GoZero vs Gin
| Feature | Gin | GoZero |
|---|---|---|
| Positioning | Web framework | Microservice framework |
| Scope | Routing + middleware | Web + RPC + full suite of service governance |
| Learning curve | Low | Medium |
| Suitable scenarios | Simple API services | Large microservice architectures |
| Code generation | None | goctl auto-generates |
| Service governance | Integrate yourself | Built-in circuit breaking, rate limiting, discovery |
4.3 Quick Start
Install:
go install github.com/zeromicro/go-zero/tools/goctl@latestGenerate the project structure with goctl:
# Generate an API servicegoctl api new user-apicd user-apigo mod tidygo run user-api.go -f etc/user-api.yaml
# Generate an RPC servicegoctl rpc new user-rpc4.4 API Service Definition (.api File)
GoZero uses .api files to define HTTP interfaces (similar to how .proto defines gRPC interfaces):
syntax = "v1"
type GetUserRequest { Id int64 `path:"id"`}
type GetUserResponse { Id int64 `json:"id"` Name string `json:"name"` Email string `json:"email"`}
type CreateUserRequest { Name string `json:"name"` Email string `json:"email" validate:"email"`}
@server( prefix: /api/v1 group: user)service user-api { @handler GetUser get /user/:id (GetUserRequest) returns (GetUserResponse)
@handler CreateUser post /user (CreateUserRequest) returns (GetUserResponse)}Then use goctl to auto-generate the complete code:
goctl api go -api user.api -dir .The generated directory structure:
user-api/├── etc/│ └── user-api.yaml # Configuration file├── internal/│ ├── config/│ │ └── config.go # Config struct│ ├── handler/ # HTTP handler functions│ │ ├── getuserhandler.go│ │ └── createuserhandler.go│ ├── logic/ # Business logic layer│ │ ├── getuserlogic.go│ │ └── createuserlogic.go│ ├── svc/ # Service context (dependency injection)│ │ └── servicecontext.go│ └── types/ # Type definitions│ └── types.go└── user-api.go # Entry fileLayered architecture:
- handler layer: only responsible for parameter parsing and responses, no business logic
- logic layer: core business logic
- svc layer: dependency injection; database connections, caches, etc. are managed here
4.5 GoZero’s Core Mechanisms
Circuit Breaker
When a downstream service has problems, it automatically “trips” and stops sending requests to it, avoiding the avalanche effect. GoZero uses a sliding window to count the error rate:
Normal state → error rate exceeds threshold → Open state (fail fast) → after cool-down → Half-Open state (probe) → recover on success / keep open on failureRate Limiter
GoZero uses adaptive rate limiting, dynamically adjusting the allowed number of requests based on CPU usage:
- CPU < 80%: pass normally
- CPU > 80%: reject requests proportionally
- CPU = 100%: reject almost all
Service Discovery
etcd is supported as the registry by default:
Name: user-apiHost: 0.0.0.0Port: 8080
UserRpc: Etcd: Hosts: - localhost:2379 Key: user.rpc4.6 RPC Service (Based on gRPC)
GoZero’s RPC service is gRPC under the hood, but with enhancements:
// Define protosyntax = "proto3";package user;
option go_package = "./user";
message GetUserRequest { int64 id = 1;}
message GetUserResponse { int64 id = 1; string name = 2;}
service User { rpc GetUser(GetUserRequest) returns (GetUserResponse);}# Generate RPC codegoctl rpc protoc user.proto --go_out=./types --go-grpc_out=./server --zrpc_out=.API service calling the RPC service:
type ServiceContext struct { Config config.Config UserRpc user.UserZrpcClient}
func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, UserRpc: user.NewUserZrpcClient(zrpc.MustNewClient(c.UserRpc)), }}
// internal/logic/getuserlogic.gofunc (l *GetUserLogic) GetUser(req *types.GetUserRequest) (*types.GetUserResponse, error) { // Call the RPC service as if calling a local function resp, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.GetUserRequest{ Id: req.Id, }) if err != nil { return nil, err } return &types.GetUserResponse{ Id: resp.Id, Name: resp.Name, }, nil}4.7 GoZero Middleware
GoZero supports custom middleware for unified handling of authentication, logging, etc.:
// Custom middlewarefunc AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token == "" { http.Error(w, "Not logged in", http.StatusUnauthorized) return } // Verify token... next(w, r) }}
// Register in routes.gofunc RegisterHandlers(engine *rest.Server, serverCtx *svc.ServiceContext) { engine.AddRoutes( rest.WithMiddlewares( []rest.Middleware{AuthMiddleware}, rest.Route{ Method: http.MethodGet, Path: "/api/v1/user/:id", Handler: GetUserHandler(serverCtx), }, ), )}4.8 GoZero Integration with Databases
GoZero has built-in ORM support (based on sqlx):
# Generate Model from databasegoctl model mysql datasource -url "user:password@tcp(localhost:3306)/dbname" -table "users" -dir ./modelThe generated Model code:
type UserModel struct { db sqlx.SqlConn}
func (m *UserModel) FindOne(ctx context.Context, id int64) (*User, error) { query := "SELECT id, name, email FROM users WHERE id = ?" var user User err := m.db.QueryRowCtx(ctx, &user, query, id) return &user, err}
func (m *UserModel) Insert(ctx context.Context, data *User) (sql.Result, error) { query := "INSERT INTO users (name, email) VALUES (?, ?)" return m.db.ExecCtx(ctx, query, data.Name, data.Email)}Use it in Logic:
func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (*types.GetUserResponse, error) { user, err := l.svcCtx.UserModel.FindOne(l.ctx, req.Id) if err != nil { return nil, err } return &types.GetUserResponse{ Id: user.Id, Name: user.Name, }, nil}4.9 GoZero Integration with Caching
GoZero has built-in cache support, automatically handling cache penetration, breakdown, and avalanche:
Cache: - Host: localhost:6379 Pass: ""// Auto-generated Model with cachingtype UserModel struct { sqlc.CachedConn // Built-in cache connection}
func (m *UserModel) FindOne(ctx context.Context, id int64) (*User, error) { // Automatically goes through cache; queries DB on cache miss var user User err := m.QueryRowCtx(ctx, &user, fmt.Sprintf("user:%d", id), func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { query := "SELECT id, name, email FROM users WHERE id = ?" return conn.QueryRowCtx(ctx, v, query, id) }) return &user, err}4.10 Common Interview Questions
Q: What is GoZero’s layered architecture? A: Three layers — handler (entry point, parameter validation and responses) → logic (business logic) → svc ( dependency management, DB/cache/RPC clients). This layering keeps code responsibilities clear and makes it easy to test.
Q: How is GoZero’s circuit breaking implemented? A: It uses a sliding window algorithm (Google’s sbreaker), counting request count and failure rate within a time window. When the failure rate exceeds the threshold (by default, more than 60% failures among 5 consecutive requests), it trips; subsequent requests return an error directly instead of being sent downstream. After a cool-down period it enters a half-open state, letting one request through as a probe — if it succeeds, it recovers.
Q: How to choose between GoZero and Kratos (Bilibili’s open-source microservice framework)? A: Both are excellent Go microservice frameworks. GoZero focuses more on “out-of-the-box” and code generation, suitable for quickly building businesses; Kratos focuses more on flexibility and extensibility, with a design more oriented toward DDD (Domain-Driven Design). The choice mainly depends on the team’s tech stack and preferences.
Q: How does GoZero prevent cache penetration? A: GoZero uses several strategies: ① empty-value caching (cache empty values when data is not found, with a short TTL); ② singleflight (concurrent queries for the same key hit the database only once); ③ bloom filter (optional). All of these are automatically implemented in CachedConn.
V. Eino — ByteDance’s AI Application Development Framework
5.1 What Is Eino
Eino (pronounced “Ayno”, derived from the Chinese word “效能” / efficiency) is an **AI application development framework ** open-sourced by ByteDance, designed specifically for the Go ecosystem. If Gin is a web framework and GoZero is a microservice framework, then Eino is an LLM application framework — used to build AI Agents, RAG pipelines, chatbots, and similar applications.
Positioning: similar to LangChain in the Python ecosystem, but implemented in Go for Go developers.
5.2 Why Eino Is Needed
Python dominates the AI/ML field, but many companies’ backend services are written in Go. When integrating AI capabilities into a Go service, there are a few options:
| Approach | Problem |
|---|---|
| Call OpenAI SDK directly | Locked to one provider, no abstraction layer |
| Build your own wrapper | Heavy workload, plus you must handle streaming, concurrency, tool calling, etc. |
| Eino | Unified abstraction, out of the box |
5.3 Core Concepts
Eino’s design philosophy is componentization + orchestration:
Components:
ChatModel— LLM invocation (OpenAI, Claude, Doubao, etc.)Embedding— text vectorizationRetriever— knowledge retrieval (document retrieval in RAG)Tool— tool calling (used by Agents)Lambda— custom processing functions
Orchestration: combine components like Lego bricks to form a processing pipeline (Chain / Graph).
5.4 Basic Usage
package main
import ( "context" "fmt" "log"
"github.com/cloudwego/eino/components/model" "github.com/cloudwego/eino/schema")
func main() { ctx := context.Background()
// Create a ChatModel (using OpenAI as an example) chatModel, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{ APIKey: "your-api-key", Model: "gpt-4", }) if err != nil { log.Fatal(err) }
// Call the model messages := []*schema.Message{ schema.SystemMessage("You are a Go expert"), schema.UserMessage("Explain what a Go goroutine is"), }
resp, err := chatModel.Generate(ctx, messages) if err != nil { log.Fatal(err) }
fmt.Println(resp.Content)}5.5 Streaming Output
Eino natively supports streaming output, which is very important in AI applications (users don’t want to wait for the full reply to finish generating):
// Streaming callstream, err := chatModel.Stream(ctx, messages)if err != nil { log.Fatal(err)}
for { chunk, err := stream.Recv() if err != nil { break // Stream ended } fmt.Print(chunk.Content) // Output chunk by chunk}5.6 Agent and Tool Calling
Eino supports building AI Agents — letting the LLM call external tools:
// Define a tooltype WeatherTool struct{}
func (t *WeatherTool) Info() *schema.ToolInfo { return &schema.ToolInfo{ Name: "get_weather", Desc: "Get weather information for a specified city", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "city": { Type: "string", Desc: "City name", Required: true, }, }), }}
func (t *WeatherTool) Run(ctx context.Context, params map[string]any) (any, error) { city := params["city"].(string) // In practice this would call a weather API return fmt.Sprintf("%s is sunny today, temperature 25°C", city), nil}
// Create an Agent and register the toolagent, _ := react.NewAgent(ctx, &react.AgentConfig{ Model: chatModel, ToolsConfig: react.ToolsConfig{ Tools: []schema.Tool{&WeatherTool{}}, },})When the user asks “What’s the weather like in Beijing today”, the Agent will automatically call the get_weather tool, get the result, and then formulate a reply to the user.
5.7 Multi-Tool Agent in Practice
In real projects, an Agent usually needs to register multiple tools:
// Define multiple toolstype SearchTool struct{}type CalculatorTool struct{}type DatabaseTool struct{}
func main() { // Register multiple tools tools := []schema.Tool{ &SearchTool{}, &CalculatorTool{}, &DatabaseTool{}, }
agent, _ := react.NewAgent(ctx, &react.AgentConfig{ Model: chatModel, ToolsConfig: react.ToolsConfig{ Tools: tools, }, // Maximum reasoning rounds, to prevent infinite loops MaxIterations: 10, })
// The Agent will automatically think → call tool → observe result → continue reasoning resp, _ := agent.Generate(ctx, []*schema.Message{ schema.UserMessage("Help me check the weather in Beijing, then calculate the Fahrenheit temperature"), }) fmt.Println(resp.Content)}5.8 RAG (Retrieval-Augmented Generation)
RAG is the most popular AI application pattern today. The core flow: user asks a question → retrieve relevant documents → feed the documents in as context to the LLM → the LLM generates an answer based on the documents.
Building a RAG pipeline in Eino:
// 1. Document loading and splittingloader := text.NewFileLoader("docs/")docs, _ := loader.Load(ctx)
splitter := text.NewRecursiveTextSplitter(&text.SplitterConfig{ ChunkSize: 500, ChunkOverlap: 50,})chunks, _ := splitter.Split(ctx, docs)
// 2. Vectorizationembedder, _ := openai.NewEmbedder(ctx, &openai.EmbeddingConfig{ APIKey: "your-api-key", Model: "text-embedding-3-small",})vectors, _ := embedder.EmbedDocuments(ctx, chunks)
// 3. Store into a vector databasestore, _ := chroma.NewVectorStore(ctx, &chroma.Config{ Collection: "my_docs",})store.AddDocuments(ctx, chunks, vectors)
// 4. Retrieve + generatequery := "What is a Go interface?"queryVector, _ := embedder.EmbedQuery(ctx, query)relevantDocs, _ := store.Search(ctx, queryVector, 3) // Take the 3 most relevant
// 5. Assemble the prompt and call the LLMmessages := []*schema.Message{ schema.SystemMessage("Answer the question based on the following documents: " + formatDocs(relevantDocs)), schema.UserMessage(query),}resp, _ := chatModel.Generate(ctx, messages)fmt.Println(resp.Content)5.9 Chain Orchestration (Chained Calls)
Eino supports chaining multiple components together with Chain:
// Create a simple RAG Chainchain, _ := compose.NewChain[*schema.Message, *schema.Message]()
// Add a retriever nodechain.AppendRetriever(retriever, nil)
// Add an LLM nodechain.AppendChatModel(chatModel, nil)
// Compile and runcompiled, _ := chain.Compile(ctx)resp, _ := compiled.Invoke(ctx, []*schema.Message{ schema.UserMessage("What's the difference between a Go goroutine and a thread?"),})5.10 Common Interview Questions
Q: What’s the difference between Eino and LangChain? A: Eino is implemented in Go, LangChain in Python. Eino’s design is more lightweight, with simpler component interfaces; LangChain’s ecosystem is more mature, supporting more providers and tools. If your backend is Go, using Eino avoids the cross-language call overhead between Python and Go.
Q: How to choose between RAG and Fine-tuning? A: RAG suits knowledge-intensive tasks (enterprise knowledge bases, document Q&A) — no retraining needed, and updating knowledge only requires updating the document store; fine-tuning suits style / capability adjustment tasks (making the model speak in a specific tone, learning reasoning patterns in a specific domain). The two can be combined.
Q: What is the Agent’s ReAct pattern? A: ReAct (Reasoning + Acting) is the classic Agent paradigm. Each step is: ① Thought → ② Action → ③ Observation → back to ①, until a final answer is reached. Eino’s react.NewAgent is an implementation of this pattern.
Q: What is the role of a vector database? A: A vector database stores the vector representation (embedding) of text and supports efficient similarity search. When a user asks a question, the question is also converted into a vector, and then the most similar document fragments are found in the vector database to serve as context for the LLM. Common vector databases include Chroma, Milvus, Pinecone, and others.
Q: What is Embedding? A: Embedding is the process of converting text into fixed-dimensional numeric vectors. Texts with similar semantics have closer vector distances. For example, the vector distance between “cat” and “dog” is closer than that between “cat” and “car”. This is the foundation of RAG and semantic search.
VI. Comprehensive Comparison and Selection Advice
| Scenario | Recommended solution |
|---|---|
| Simple REST API | Gin |
| Microservice project | GoZero (or Kratos) |
| Inter-Go-service communication | gRPC + Protobuf |
| AI application integration | Eino |
| Quick prototype | Gin (simplest) |
| Large production system | GoZero (most complete) |
Suggested learning path:
- Learn Gin first — understand the basics of Go web development
- Then learn Protobuf + gRPC — understand microservice communication
- Then learn GoZero — understand the complete microservice architecture
- Finally learn Eino — if you need to do AI-related development
VII. Summary of High-Frequency Interview Questions
- What data structure does Gin’s routing engine use? Why is it fast? → Radix Tree, merging common prefixes, O(k) lookup
- What is Gin middleware’s execution order? → Onion model: request → middleware → Handler → middleware remainder
- What is Protobuf better at than JSON? → Binary encoding, smaller size, faster parsing, strong typing, field numbers guarantee compatibility
- What are gRPC’s four communication modes? → Unary, Server Streaming, Client Streaming, Bidirectional Streaming
- What protocol is gRPC based on? → HTTP/2, supports multiplexing and header compression
- What is GoZero’s circuit breaking principle? → Sliding window counts error rate; trips when exceeding threshold; half-open probe after cool-down
- What is GoZero’s layered architecture? → handler (entry) → logic (business) → svc (dependencies)
- What is RAG? → Retrieval-augmented generation: retrieve relevant documents first, then have the LLM generate an answer based on the documents
- What is the role and considerations of Protobuf field numbers? → Used in binary encoding to identify fields; once published they cannot be changed; deprecated ones must use reserved
- Should inter-microservice communication use REST or gRPC? → Use gRPC internally (good performance, strong typing); use REST for external exposure (browser-friendly)
- Can Gin’s Context be used in a goroutine? → No, Context is reused and reclaimed; use c.Copy() in goroutines
- How do gRPC error codes map to HTTP status codes? → gRPC has a standard codes package (NotFound=404, InvalidArgument=400, etc.)
- How does GoZero prevent cache penetration? → Empty-value caching + singleflight + bloom filter
- What is Eino’s Agent ReAct pattern? → Thought→Action→Observation loop until an answer is reached
- How to solve Protobuf’s zero-value problem? → optional keyword, Wrapper types, oneof wrapping
The above content is compiled from personal study and practice. Corrections and feedback are welcome. In interviews, beyond memorizing concepts, it is more important to be able to clearly explain, with real projects, “why we chose this” and “what problems we encountered”. Wishing everyone success in their interviews!