Go Web Frameworks and RPC Notes - MuxiaoWFSkip to main content
This page was machine-translated and may contain errors or omissions. / 本页面为机器翻译,可能存在错漏。

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

Tue Jun 28 2005
6387 words · 55 minutes

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.


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:

Terminal window
go get -u github.com/gin-gonic/gin

The 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 control
r := gin.New()
r.Use(gin.Logger()) // Add logging manually
r.Use(gin.Recovery()) // Add panic recovery manually

1.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/123
r.GET("/user/:id", func(c *gin.Context) {
id := c.Param("id") // "123"
})
// Query parameter: /search?keyword=go&page=1
r.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 wildcard
r.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):

TagMeaning
requiredRequired
min=N / max=NMinimum / maximum length or value
emailEmail format
oneof=a b cEnum value, must be one of them
gte=0 / lte=100Greater than or equal to / less than or equal to
datetime=2006-01-02Date format (Go’s time format must use this reference time)
len=NExact length
contains=xxxMust contain the substring
startswith=xxx / endswith=xxxPrefix / suffix

Binding methods for different data sources:

// JSON request body
c.ShouldBindJSON(&req)
// URL query parameters
c.ShouldBindQuery(&req)
// Form data (application/x-www-form-urlencoded)
c.ShouldBind(&req)
// Path parameters must be retrieved manually; automatic binding to struct is not supported
id := 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 range
validate := 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 middleware
func 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 20
r.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})
}
// Usage
r.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 upload
r.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 upload
r.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 download
r.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:

FeatureJSONProtobuf
ReadabilityHuman-readableBinary, not directly readable
SizeLarger3-10x smaller
Parsing speedSlow5-100x faster
Strong typingNoYes
Requires IDLNoYes (.proto file)
Forward compatibilityNaturally supportedSupported 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:

Terminal window
# macOS
brew install protobuf
# Linux
sudo apt install protobuf-compiler
# Windows (scoop)
scoop install protobuf
# Verify installation
protoc --version

Then install Go’s protoc plugins:

Terminal window
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Make sure $GOPATH/bin is in your PATH.

2.3 .proto File Syntax

Create a user.proto file:

// Specify the protobuf version
syntax = "proto3";
// Go package path for the generated code
option 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
}
// Enum
enum 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

Terminal window
protoc --go_out=. --go-grpc_out=. user.proto

This generates two files in the ./pb/ directory:

  • user.pb.go — message structs and serialization methods
  • user_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/optional keywords (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 optional keyword or Google’s wrapper types
  • proto2 supports required, default value settings, and group types

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.Value

import — 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

FeatureRESTful APIgRPC
ProtocolHTTP/1.1 (mostly)HTTP/2
Data formatJSON (text)Protobuf (binary)
PerformanceAverage2-10x faster
Interface definitionNo enforced spec (Swagger optional)Must define with .proto
StreamingNot supported (needs WebSocket)Native support
Browser supportNativeNeeds a gRPC-Web proxy
Code generationOptionalAutomatic

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 pushing
rpc 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 file
rpc 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 chat
rpc 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

Terminal window
protoc --go_out=. --go-grpc_out=. user.proto

Step 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 interface
type 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 interceptor
func 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 interceptor
grpcServer := 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 error
func (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 error
resp, 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 codeMeaningHTTP equivalent
OKSuccess200
InvalidArgumentInvalid argument400
NotFoundResource does not exist404
AlreadyExistsResource already exists409
PermissionDeniedNo permission403
UnauthenticatedNot authenticated401
InternalInternal server error500
UnavailableService unavailable503
DeadlineExceededTimeout504

3.7 Streaming RPC in Practice

Server streaming:

user.proto
service UserService {
rpc WatchUsers(WatchUsersRequest) returns (stream User);
}
// Server implementation
func (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 call
stream, 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):

// Server
func (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
}
}
}
// Client
stream, _ := client.Chat(ctx)
// Goroutine that sends messages
go func() {
for _, msg := range messages {
stream.Send(msg)
}
stream.CloseSend()
}()
// Goroutine that receives messages
for {
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 HTTP
func 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

FeatureGinGoZero
PositioningWeb frameworkMicroservice framework
ScopeRouting + middlewareWeb + RPC + full suite of service governance
Learning curveLowMedium
Suitable scenariosSimple API servicesLarge microservice architectures
Code generationNonegoctl auto-generates
Service governanceIntegrate yourselfBuilt-in circuit breaking, rate limiting, discovery

4.3 Quick Start

Install:

Terminal window
go install github.com/zeromicro/go-zero/tools/goctl@latest

Generate the project structure with goctl:

Terminal window
# Generate an API service
goctl api new user-api
cd user-api
go mod tidy
go run user-api.go -f etc/user-api.yaml
# Generate an RPC service
goctl rpc new user-rpc

4.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:

Terminal window
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 file

Layered 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 failure

Rate 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:

etc/user-api.yaml
Name: user-api
Host: 0.0.0.0
Port: 8080
UserRpc:
Etcd:
Hosts:
- localhost:2379
Key: user.rpc

4.6 RPC Service (Based on gRPC)

GoZero’s RPC service is gRPC under the hood, but with enhancements:

// Define proto
syntax = "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);
}
Terminal window
# Generate RPC code
goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./server --zrpc_out=.

API service calling the RPC service:

internal/svc/servicecontext.go
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.go
func (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 middleware
func 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.go
func 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):

Terminal window
# Generate Model from database
goctl model mysql datasource -url "user:password@tcp(localhost:3306)/dbname" -table "users" -dir ./model

The generated Model code:

model/usermodel.go
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:

etc/user-api.yaml
Cache:
- Host: localhost:6379
Pass: ""
// Auto-generated Model with caching
type 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:

ApproachProblem
Call OpenAI SDK directlyLocked to one provider, no abstraction layer
Build your own wrapperHeavy workload, plus you must handle streaming, concurrency, tool calling, etc.
EinoUnified 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 vectorization
  • Retriever — 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 call
stream, 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 tool
type 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 tool
agent, _ := 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 tools
type 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 splitting
loader := text.NewFileLoader("docs/")
docs, _ := loader.Load(ctx)
splitter := text.NewRecursiveTextSplitter(&text.SplitterConfig{
ChunkSize: 500,
ChunkOverlap: 50,
})
chunks, _ := splitter.Split(ctx, docs)
// 2. Vectorization
embedder, _ := openai.NewEmbedder(ctx, &openai.EmbeddingConfig{
APIKey: "your-api-key",
Model: "text-embedding-3-small",
})
vectors, _ := embedder.EmbedDocuments(ctx, chunks)
// 3. Store into a vector database
store, _ := chroma.NewVectorStore(ctx, &chroma.Config{
Collection: "my_docs",
})
store.AddDocuments(ctx, chunks, vectors)
// 4. Retrieve + generate
query := "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 LLM
messages := []*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 Chain
chain, _ := compose.NewChain[*schema.Message, *schema.Message]()
// Add a retriever node
chain.AppendRetriever(retriever, nil)
// Add an LLM node
chain.AppendChatModel(chatModel, nil)
// Compile and run
compiled, _ := 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

ScenarioRecommended solution
Simple REST APIGin
Microservice projectGoZero (or Kratos)
Inter-Go-service communicationgRPC + Protobuf
AI application integrationEino
Quick prototypeGin (simplest)
Large production systemGoZero (most complete)

Suggested learning path:

  1. Learn Gin first — understand the basics of Go web development
  2. Then learn Protobuf + gRPC — understand microservice communication
  3. Then learn GoZero — understand the complete microservice architecture
  4. Finally learn Eino — if you need to do AI-related development

VII. Summary of High-Frequency Interview Questions

  1. What data structure does Gin’s routing engine use? Why is it fast? → Radix Tree, merging common prefixes, O(k) lookup
  2. What is Gin middleware’s execution order? → Onion model: request → middleware → Handler → middleware remainder
  3. What is Protobuf better at than JSON? → Binary encoding, smaller size, faster parsing, strong typing, field numbers guarantee compatibility
  4. What are gRPC’s four communication modes? → Unary, Server Streaming, Client Streaming, Bidirectional Streaming
  5. What protocol is gRPC based on? → HTTP/2, supports multiplexing and header compression
  6. What is GoZero’s circuit breaking principle? → Sliding window counts error rate; trips when exceeding threshold; half-open probe after cool-down
  7. What is GoZero’s layered architecture? → handler (entry) → logic (business) → svc (dependencies)
  8. What is RAG? → Retrieval-augmented generation: retrieve relevant documents first, then have the LLM generate an answer based on the documents
  9. 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
  10. Should inter-microservice communication use REST or gRPC? → Use gRPC internally (good performance, strong typing); use REST for external exposure (browser-friendly)
  11. Can Gin’s Context be used in a goroutine? → No, Context is reused and reclaimed; use c.Copy() in goroutines
  12. How do gRPC error codes map to HTTP status codes? → gRPC has a standard codes package (NotFound=404, InvalidArgument=400, etc.)
  13. How does GoZero prevent cache penetration? → Empty-value caching + singleflight + bloom filter
  14. What is Eino’s Agent ReAct pattern? → Thought→Action→Observation loop until an answer is reached
  15. 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!


Thanks for reading! Follow me if you'd like~

Go Web Frameworks and RPC Notes

Tue Jun 28 2005
6387 words · 55 minutes
Cover
Sample track
Sample artist
Cover
Sample track
Sample artist
0:00 / 0:00