Go Is the Right Choice for Network Tools. Here Is the Proof

Six Months In: Lessons Learned
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
nmapandwhois. - 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()
}
The event bus works well enough for my use case, though I would reach for a proper message queue like NATS if the traffic grew significantly. If you spot a bug or have a cleaner pattern for Go channel-based pub/sub, I am interested.
Surveyor: A Field-Tested Network Discovery Tool for MSPs
From Raspberry Pi prototype to Wails desktop app: how Surveyor replaces manual nmap commands with stealth scanning and RFC 1918 auto-discovery.
Why I Picked Up Go After 20 Years of Web Development
After two decades of JavaScript, PHP, and Python, I am picking up Go to solve a real performance problem—and the early impressions are strong.
Welcome to the Atomic Drafting Room
Why I tore down the Renaissance Technical design and rebuilt the site in Raygun Gothic — atomic starbursts, chrome typography, and a turquoise-coral-gold palette that finally feels like me.