Skip to content

Implement a web service with health checks using Golang + Gin ​

Golang Gin Health Check Service Diagram

In the previous two chapters, we discussed the architecture of the high-availability entry point and the principles of the VRRP protocol. Starting from this chapter, we move into the real hands-on phase.

We will use the Go language and the Gin framework to implement a Can report one's own identity Web service. This service has two core interfaces:

  • /health: Health check endpoint that tells Nginx whether it is alive

  • /api/v1/task: Business interface, simulate task dispatch

In subsequent chapters, we will deploy this service across three machines, and work with Nginx + Keepalived to build a complete high-availability entry point.

1. Why do we need “talking” Web services? ​

When building a high-availability entry point, we need a Can verify whether the architecture is working properly the backend service.

This service needs to have the following capabilities:

AbilityUse
Report identityTell us which machine handled the request (to verify VIP drift)
Health checkProvide a /health interface for Nginx/Keepalived probing
Simulated businessProvide a business interface to verify whether the reverse proxy is working properly.
Simulated faultProvide slow endpoints and error endpoints to test circuit breaking and removal logic.

This is what we mean by a “conversational” service—it can not only handle requests, but also tell the client “who I am.”

II. Project Initialization ​

2.1 Create Project Directory ​

bash
mkdir -p $GOPATH/src/github.com/fishfinal/go-gateway-demo
cd $GOPATH/src/github.com/fishfinal/go-gateway-demo
go mod init github.com/fishfinal/go-gateway-demo

2.2 Install Gin Dependencies ​

bash
go get -u github.com/gin-gonic/gin

2.3 Project Structure ​

go-gateway-demo/
├── go.mod
├── go.sum
├── main.go              # Main program
├── Dockerfile           # Containerized deployment (optional)
└── docker-compose.yml   # Three-node simulation (optional)

III. Complete Code Implementation ​

3.1 Main Program main.go ​

go
package main

import (
    "fmt"
    "net"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/gin-gonic/gin"
)

// Server encapsulates service information
type Server struct {
    hostname string
    localIP  string
    port     string
}

// NewServer creates a new server instance
func NewServer(port string) *Server {
    hostname, _ := os.Hostname()
    localIP := getLocalIP()
    return &Server{
        hostname: hostname,
        localIP:  localIP,
        port:     port,
    }
}

// getLocalIP retrieves the local non-loopback IPv4 address
func getLocalIP() string {
    addrs, err := net.InterfaceAddrs()
    if err != nil {
        return "unknown"
    }
    for _, addr := range addrs {
        if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
            if ipnet.IP.To4() != nil {
                return ipnet.IP.String()
            }
        }
    }
    return "unknown"
}

// setupRouter configures the routing
func (s *Server) setupRouter() *gin.Engine {
    r := gin.Default()

    // Health check endpoint (for Nginx/Keepalived)
    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "status":    "ok",
            "hostname":  s.hostname,
            "ip":        s.localIP,
            "timestamp": time.Now().Unix(),
        })
    })

    // Business endpoint: simulate task dispatching
    r.POST("/api/v1/task", func(c *gin.Context) {
        var req map[string]interface{}
        if err := c.BindJSON(&req); err != nil {
            c.JSON(400, gin.H{"error": "invalid request"})
            return
        }

        c.JSON(200, gin.H{
            "code":    0,
            "message": "task accepted",
            "processed_by": gin.H{
                "hostname": s.hostname,
                "ip":       s.localIP,
            },
            "task": req,
        })
    })

    // Simulated slow endpoint: for timeout testing
    r.GET("/api/slow", func(c *gin.Context) {
        time.Sleep(5 * time.Second)
        c.JSON(200, gin.H{
            "message":      "slow response",
            "processed_by": s.hostname,
        })
    })

    // Simulated error endpoint: for failure detection testing
    r.GET("/api/error", func(c *gin.Context) {
        c.JSON(500, gin.H{
            "error":        "internal server error",
            "processed_by": s.hostname,
        })
    })

    // Debug endpoint: view service information
    r.GET("/info", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "hostname": s.hostname,
            "ip":       s.localIP,
            "port":     s.port,
        })
    })

    return r
}

// Run starts the server
func (s *Server) Run() error {
    r := s.setupRouter()
    addr := ":" + s.port

    fmt.Printf("🚀 Server starting on %s\n", addr)
    fmt.Printf("📡 Hostname: %s\n", s.hostname)
    fmt.Printf("🌐 IP: %s\n", s.localIP)
    fmt.Printf("✅ Health check: http://localhost%s/health\n", addr)
    fmt.Printf("📋 Task API: POST http://localhost%s/api/v1/task\n", addr)

    return r.Run(addr)
}

func main() {
    // Read port from environment variable, default to 8080
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    server := NewServer(port)

    // Graceful shutdown handling
    go func() {
        if err := server.Run(); err != nil {
            fmt.Printf("Server error: %v\n", err)
        }
    }()

    // Wait for shutdown signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    fmt.Println("\n🛑 Shutting down server...")
}

3.2 Code Structure Description ​

3.3 Core Function Verification ​

Start service:

bash
go run main.go

# Output:
# 🚀 Server starting on :8080
# 📡 Hostname: ubuntu-node1
# 🌐 IP: 192.168.0.201
# ✅ Health check: http://localhost:8080/health
# 📋 Task API: POST http://localhost:8080/api/v1/task

Verify health check:

bash
curl http://localhost:8080/health | jq .

# Response:
{
  "status": "ok",
  "hostname": "ubuntu-node1",
  "ip": "192.168.0.201",
  "timestamp": 1749876543
}

Verify business interface:

bash
curl -X POST http://localhost:8080/api/v1/task \
  -H "Content-Type: application/json" \
  -d '{"task_id": "task-001", "target": "192.168.0.0/24"}' | jq .

# Response:
{
  "code": 0,
  "message": "task accepted",
  "processed_by": {
    "hostname": "ubuntu-node1",
    "ip": "192.168.0.201"
  },
  "task": {
    "task_id": "task-001",
    "target": "192.168.0.0/24"
  }
}

Verify slow API:

bash
time curl http://localhost:8080/api/slow

# real    0m5.012s

IV. Three-Node Simulation Environment (Docker Compose) ​

To simulate a three-machine cluster on a single computer, we use Docker Compose + a custom bridge network.

4.1 docker-compose.yml ​

yaml
services:
  node1:
    build: .
    container_name: gateway-node1
    environment:
      - PORT=8080
    networks:
      ha_net:
        ipv4_address: 192.168.0.201
    ports:
      - "8081:8080"   # Map to the host machine for convenient standalone debugging.
    restart: unless-stopped

  node2:
    build: .
    container_name: gateway-node2
    environment:
      - PORT=8080
    networks:
      ha_net:
        ipv4_address: 192.168.0.202
    ports:
      - "8082:8080"
    restart: unless-stopped

  node3:
    build: .
    container_name: gateway-node3
    environment:
      - PORT=8080
    networks:
      ha_net:
        ipv4_address: 192.168.0.203
    ports:
      - "8083:8080"
    restart: unless-stopped

networks:
  ha_net:
    driver: bridge
    ipam:
      config:
        - subnet: 192.168.0.0/24
          gateway: 192.168.0.1

4.2 Dockerfile ​

dockerfile
FROM golang:1.26-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o gateway main.go

FROM alpine:3.24
RUN apk --no-cache add ca-certificates curl
WORKDIR /root/
COPY --from=builder /app/gateway .

EXPOSE 8080
CMD ["./gateway"]

4.3 Start the three-node cluster ​

4.3.1 Build and Start ​

bash
docker-compose up -d --build

The specific build process and startup logs are similar to the following:

Compose can now delegate builds to bake for better performance.
 To do so, set COMPOSE_BAKE=true.
[+] Building 81.1s (27/37)                                                                      docker:desktop-linux
 => [node2 internal] load build definition from Dockerfile                                                      0.1s
 => => transferring dockerfile: 314B                                                                            0.0s
 => [node3 internal] load build definition from Dockerfile                                                      0.1s
 => => transferring dockerfile: 314B                                                                            0.0s
 => [node1 internal] load build definition from Dockerfile                                                      0.1s
 => => transferring dockerfile: 314B                                                                            0.0s
 => [node3 internal] load metadata for docker.io/library/alpine:3.24                                            0.1s
 => [node3 internal] load metadata for docker.io/library/golang:1.21-alpine                                     0.1s
 => [node2 internal] load .dockerignore                                                                         0.1s
...
[+] Running 4/4
 ✔ Network go-gateway-demo_ha_net  Created                                                                      0.1s
 ✔ Container gateway-node1         Started                                                                      1.3s
 ✔ Container gateway-node2         Started                                                                      1.5s
 ✔ Container gateway-node3         Started                                                                      1.2s

4.3.2 View Running Status ​

bash
docker-compose ps

Will output information similar to the following:

NAME            IMAGE                   COMMAND       SERVICE   CREATED         STATUS         PORTS
gateway-node1   go-gateway-demo-node1   "./gateway"   node1     6 seconds ago   Up 4 seconds   0.0.0.0:8081->8080/tcp
gateway-node2   go-gateway-demo-node2   "./gateway"   node2     6 seconds ago   Up 4 seconds   0.0.0.0:8082->8080/tcp
gateway-node3   go-gateway-demo-node3   "./gateway"   node3     6 seconds ago   Up 4 seconds   0.0.0.0:8083->8080/tcp

4.3.3 Test the three nodes separately ​

bash
# Test the three nodes separately.
curl -s http://localhost:8081/health | jq .hostname
# d3a2b1c4e5f6 (The actual output is the first 12 characters of the container ID, which differs each time it is created)

curl -s http://localhost:8082/health | jq .hostname
# "a7b8c9d0e1f2"

curl -s http://localhost:8083/health | jq .hostname
# "f6e5d4c3b2a1"

💡Why isn't it "gateway-node1"? The default hostname of a Docker container is the first 12 characters of the container ID (e.g.c1d70aac6b5f), rather than container_name. If you need to customize the hostname, you can docker-compose.yml add to hostname Configuration:

yaml
services:
  node1:
    hostname: gateway-node1
    # ... Other configuration

After adding, restart the container,/health The returned hostname will become a custom value. However, for functional verification, as long as it can distinguish that the three nodes are different containers, a random ID is completely sufficient.

V. Compile and Deploy to Physical Machines ​

5.1 Cross-compiling Linux binaries ​

bash
# Compile the Linux amd64 version
GOOS=linux GOARCH=amd64 go build -o gateway main.go

# View file information
file gateway
# gateway: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked

5.2 Deploy to Server ​

bash
# Upload to the server (using node1 as an example)
scp gateway [email protected]:/usr/local/bin/

# Run on the server
ssh [email protected]
chmod +x /usr/local/bin/gateway
./gateway &

# Verify
curl http://localhost:8080/health

💡 Note

It needs to be deployed separately on the three servers 192.168.0.201/202/203. It is recommended to use the following systemd method for deployment!

5.3.1 Create a Service Definition Configuration File ​

bash
# /etc/systemd/system/gateway.service
cat > /etc/systemd/system/gateway.service << 'EOF'
[Unit]
Description=Gateway Health Check Service
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/lib/gateway/
ExecStart=/usr/local/bin/gateway
Restart=always
RestartSec=5
Environment="PORT=8080"

[Install]
WantedBy=multi-user.target
EOF

5.3.2 Create necessary directories ​

bash
mkdir -p /var/lib/gateway

5.3.3 Start and set auto-start on boot ​

bash
systemctl daemon-reload
systemctl enable gateway
systemctl start gateway

5.3.4 View Status ​

bash
systemctl status gateway

VI. Description of the Practice Environment ​

The hands-on environment for this series uses the following home network configuration:

Configuration itemvalueExplanation
network segment192.168.0.0/24Standard home local area network
gateway192.168.0.1Router address
VIP192.168.0.200Virtual IP, used in subsequent chapters
node1192.168.0.201Node 1, priority 100
node2192.168.0.202Node 2, priority 90
node3192.168.0.203Node 3, priority 80

💡 If your home network subnet is different (e.g.192.168.1.0/24), just need to take all 192.168.0.x Just replace it with your own network segment.

VII. Integration with Master Election (Preview) ​

In the subsequent chapter on distributed scanning engine integration, we will link this service with etcd election:

go
// Pseudocode example
func (m *Master) Start() {
    if m.election.IsLeader() {
        // Become Master, create identification file
        os.Create("/tmp/is_master")
        // Start scheduling logic
        go m.schedule()
    } else {
        os.Remove("/tmp/is_master")
    }
}

Then the Nginx health check script can determine it like this:

bash
#!/bin/bash
# Return 200 only when the /tmp/is_master file exists.
if [ -f /tmp/is_master ]; then
    exit 0
else
    exit 1
fi

This way, Nginx will only forward requests to nodes that are truly the Master, achieving "product-transparent" high-availability scheduling.

VIII. Summary ​

In this chapter, we completed a complete Golang + Gin health check service with the following capabilities:

FunctioninterfacePurpose
Health checkGET /healthNginx/Keepalived probing
Business interfacePOST /api/v1/taskSimulate task dispatch and verify identity
slow endpointGET /api/slowTest timeout and circuit breaking
Error interfaceGET /api/errorTest fault removal
Debug informationGET /infoView service identity

and provides two deployment methods:

  • Docker Compose: Local three-node simulation, suitable for development and debugging

  • Systemd service: Production environment deployment, supports startup on boot and automatic recovery

In the next chapter, we will configure Nginx health checks and active circuit breaking so that Nginx can dynamically remove faulty nodes.

💡 This article is the 3rd installment of the "Distributed High-Availability Ingress Architecture in Practice Series"

最后更新2026/11/07 14:16
如果你觉得这篇文章有帮助,或者想聊聊技术、工作,欢迎通过下面方式联系我:
contact fishfinal