::
FIG 2.1 // ENTRY
Back to Index

Learning Go part 2

Date:
Learning Go part 2 Hero Image

Update

I really like Go. It’s easy to pick up, the standard library is comprehensive, and the syntax is intentionally simple. I’ve been rewriting my network-mapper app, and Go is the right choice for the server.

Unfortunately, Go (and Rust) does not run natively on IBM i. Despite the AIX-based PASE environment, issues with mmap and the IFS prevent execution (see this GitHub issue). For now, I’ll use Go in Linux environments and stick to Node or Python on IBM i.

The App: “Gatherer”

I renamed the app to Gatherer. It’s designed for quick network and MSP client evaluations.

Architecture

  • Server: Echo framework (Go).
  • Background Tasks: Uses tools like nmap and whois.
  • Real-time: Event-driven architecture with WebSockets.
  • Database: GORM (ORM) for easy filtering.
  • Pub/Sub: Custom implementation using Go channels.

Frontend I initially tried Solid.js but reverted to React due to ecosystem maturity and IDE support.

  • Stack: React, React Query, React Router, React Hook Form.
  • UI: react-aria-components + daisyUI. This combination offers accessible components with clean CSS.

Event Bus Implementation

Here is the custom Event Bus I implemented, inspired by the Revel framework. It supports separate “rooms” for broadcasting and subscribing.

package events

import (
	"container/list"
	"time"
)

var rooms = make(map[string]Room)

type Room struct {
	Name        string `json:"name"`
	subscribe   chan (chan<- Subscription)
	unsubscribe chan (<-chan Event)
	publish     chan Event
	Subscription
}

func NewRoom(name string) Room {
	r, ok := rooms[name]

	if ok {
		return r
	}

	var (
		subscribe   = make(chan (chan<- Subscription), archiveSize)
		unsubscribe = make(chan (<-chan Event), archiveSize)
		publish     = make(chan Event, archiveSize)
	)
	room := Room{Name: name, subscribe: subscribe, unsubscribe: unsubscribe, publish: publish}
	room.init()
	rooms[name] = room
	return rooms[name]
}

func (r *Room) drain(ch <-chan Event) {
	for {
		select {
		case _, ok := <-ch:
			if !ok {
				return
			}
		default:
			return
		}
	}
}

func (r *Room) Cancel() {
	r.unsubscribe <- r.Subscription.New
	r.drain(r.Subscription.New)
}

func (r *Room) Broadcast(typ string, data interface{}) {
	r.publish <- Event{Type: typ, User: "go", Timestamp: int(time.Now().Unix()), Data: data}
}

func (r *Room) Subscribe() Subscription {
	resp := make(chan Subscription)
	r.subscribe <- resp
	return <-resp
}

func (r *Room) work() {
	archive := list.New()
	subscribers := list.New()

	for {
		select {
		case ch := <-r.subscribe:
			var events []Event
			for e := archive.Front(); e != nil; e = e.Next() {
				events = append(events, e.Value.(Event))
			}
			subscriber := make(chan Event, archiveSize)
			subscribers.PushBack(subscriber)
			ch <- Subscription{events, subscriber}

		case event := <-r.publish:
			for ch := subscribers.Front(); ch != nil; ch = ch.Next() {
				ch.Value.(chan Event) <- event
			}
			if archive.Len() >= archiveSize {
				archive.Remove(archive.Front())
			}
			archive.PushBack(event)

		case unsub := <-r.unsubscribe:
			for ch := subscribers.Front(); ch != nil; ch = ch.Next() {
				if ch.Value.(chan Event) == unsub {
					subscribers.Remove(ch)
					break
				}
			}
		}
	}
}

func (r *Room) init() {
	go r.work()
}

Any questions just hit me up!