Backend Development 12 min read

Build a Simple Go Load Balancer with Round‑Robin and Health Checks

This article walks through building a simple Go load balancer using round‑robin distribution, reverse proxy, atomic indexing, health checks, and concurrency‑safe data structures, providing complete code examples and explanations for each component.

360 Zhihui Cloud Developer
360 Zhihui Cloud Developer
360 Zhihui Cloud Developer
Build a Simple Go Load Balancer with Round‑Robin and Health Checks

Working Principle

Load balancing distributes requests among backend services, increasing scalability and availability. If a backend fails, the balancer should route only to healthy nodes.

Round‑Robin Strategy

We implement the simplest strategy, round‑robin, which cycles through backends equally.

Data Structures

<code>type Backend struct {
    URL          *url.URL
    Alive        bool
    mux          sync.RWMutex
    ReverseProxy *httputil.ReverseProxy
}</code>
<code>type ServerPool struct {
    backends []*Backend
    current  uint64
}</code>

ReverseProxy

Go's httputil.ReverseProxy forwards requests to a backend and returns the response.

<code>u, _ := url.Parse("http://localhost:8080")
rp := httputil.NewSingleHostReverseProxy(u)
http.HandlerFunc(rp.ServeHTTP)</code>

Selection Process

NextIndex uses an atomic increment to obtain the next backend index modulo the pool size.

<code>func (s *ServerPool) NextIndex() int {
    return int(atomic.AddUint64(&s.current, 1) % uint64(len(s.backends)))
}</code>

Get Alive Backend

GetNextPeer loops through the backends starting from the next index to find an alive one.

<code>// GetNextPeer returns next active peer to take a connection
func (s *ServerPool) GetNextPeer() *Backend {
    // loop entire backends to find an Alive backend
    next := s.NextIndex()
    l := len(s.backends) + next // start from next and move a full cycle
    for i := next; i < l; i++ {
        idx := i % len(s.backends)
        if s.backends[idx].IsAlive() {
            if i != next {
                atomic.StoreUint64(&s.current, uint64(idx)) // mark the current one
            }
            return s.backends[idx]
        }
    }
    return nil
}</code>

Concurrency Handling

The Alive field of Backend is protected by a RWMutex to allow safe concurrent reads and writes.

<code>// SetAlive for this backend
func (b *Backend) SetAlive(alive bool) {
    b.mux.Lock()
    b.Alive = alive
    b.mux.Unlock()
}

// IsAlive returns true when backend is alive
func (b *Backend) IsAlive() (alive bool) {
    b.mux.RLock()
    alive = b.Alive
    b.mux.RUnlock()
    return
}</code>

Request Handling

The lb function selects a peer and forwards the request via its reverse proxy, returning an error if no healthy backend is available.

<code>func lb(w http.ResponseWriter, r *http.Request) {
    peer := serverPool.GetNextPeer()
    if peer != nil {
        peer.ReverseProxy.ServeHTTP(w, r)
        return
    }
    http.Error(w, "Service not available", http.StatusServiceUnavailable)
}</code>

Health Checks

A passive health check periodically pings each backend to determine its status and updates the Alive flag.

<code>// isBackendAlive checks whether a backend is alive by establishing a TCP connection
func isBackendAlive(u *url.URL) bool {
    timeout := 2 * time.Second
    conn, err := net.DialTimeout("tcp", u.Host, timeout)
    if err != nil {
        log.Println("Site unreachable, error: ", err)
        return false
    }
    _ = conn.Close()
    return true
}</code>
<code>// HealthCheck pings the backends and updates the status
func (s *ServerPool) HealthCheck() {
    for _, b := range s.backends {
        status := "up"
        alive := isBackendAlive(b.URL)
        b.SetAlive(alive)
        if !alive {
            status = "down"
        }
        log.Printf("%s [%s]\n", b.URL, status)
    }
}</code>
<code>// healthCheck runs a routine for checking status of the backends every 20 secs
func healthCheck() {
    t := time.NewTicker(time.Second * 20)
    for {
        select {
        case <-t.C:
            log.Println("Starting health check...")
            serverPool.HealthCheck()
            log.Println("Health check completed")
        }
    }
}</code>

Start the health‑check routine in a separate goroutine: go healthCheck() .

Conclusion

The implementation provides a basic load balancer; further enhancements could include weighted round‑robin or least‑connection algorithms, heap‑based alive node tracking, statistics collection, configuration file support, and more.

backend developmentGoReverse Proxyload balancerRound Robinhealth check
360 Zhihui Cloud Developer
Written by

360 Zhihui Cloud Developer

360 Zhihui Cloud is an enterprise open service platform that aims to "aggregate data value and empower an intelligent future," leveraging 360's extensive product and technology resources to deliver platform services to customers.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

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