Da jeg startet arbeidet med backend-arkitekturen for Tuli Technologies, stod valget mellom flere språk. Node.js var det trygge valget — jeg kjente økosystemet godt, teamet hadde erfaring med det, og det finnes et npm-pakke for alt. Men jeg endte med Go. Ikke fordi det var trendy, men fordi det løste de faktiske problemene vi hadde.
Dette innlegget handler om de reelle trade-offs jeg vurderte, hvordan Go fungerer i produksjon for oss, og når du definitivt bør velge noe annet.
Hvorfor Go og ikke Node.js?
La meg være ærlig: Node.js er et utmerket valg for mange prosjekter. Men for Tuli hadde vi noen spesifikke krav som tippet vektskålen.
Type-sikkerhet uten seremoni. TypeScript gir deg typesikkerhet i Node-verdenen, men det er et lag oppå JavaScript — med sin egen build-pipeline, sine egne quirks, og et typesystem som noen ganger føles som det jobber mot deg. Go har typesikkerhet innebygd fra dag én. Ingen tsconfig, ingen type gymnastics. Du skriver koden, kompilatoren fanger feilene, ferdig.
Concurrency-modellen. Tuli håndterer mange samtidige forespørsler — kursdata, brukersesjoner, sanntidsoppdateringer. Goroutines og channels gir en concurrency-modell som er fundamentalt enklere å resonnere rundt enn Node sin event loop med callbacks og promises. Når du trenger å gjøre tre API-kall parallelt og samle resultatene, er det i Go bare å skrive det — ingen Promise.all eller asynkron magi.
Én binærfil. Deployment i Go betyr én kompilert binærfil. Ingen node_modules på 500 MB, ingen runtime-avhengigheter, ingen "fungerer på min maskin"-problemer. Docker-imaget vårt er under 20 MB. Det forenkler CI/CD enormt og gjør at vi kan spinne opp nye instanser på sekunder.
Minnefotavtrykk. En typisk Go-tjeneste hos oss bruker 30-50 MB RAM. En tilsvarende Node-tjeneste ville brukt 150-300 MB. Når du kjører flere mikrotjenester, summerer dette seg raskt.
Men det er ikke bare solskinn. Go sin feilhåndtering er verbose. Du skriver if err != nil hundrevis av ganger om dagen. Økosystemet er mindre enn Node sitt — det finnes ikke alltid en ferdig pakke for alt. Og generics kom sent til festen, noe som betyr at mye eksisterende kode bruker interface{} der du ville ønsket deg sterkere typing.
Slik bruker jeg Go i produksjon
Prosjektstruktur
Vi følger en struktur inspirert av Standard Go Project Layout, men tilpasset våre behov:
tuli-api/
├── cmd/
│ └── server/ # Entrypoint — main.go
├── internal/
│ ├── course/ # Domenelogikk per bounded context
│ ├── auth/
│ └── platform/ # Delt infrastruktur (database, logging)
└── pkg/ # Eksporterte hjelpepakkercmd/ inneholder applikasjonens entrypoints. internal/ er der all forretningslogikk lever — Go håndhever at denne koden ikke kan importeres av eksterne pakker. pkg/ er for ting som faktisk bør deles.
API-design med ren arkitektur
Vi bruker interfaces tungt for å holde lagene separert. HTTP-handlere kjenner ikke til databasen. Service-laget kjenner ikke til HTTP. Dette gjør testing enkelt og refaktorering trygt.
func NewRouter(svc *service.Service) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/courses", handleListCourses(svc))
mux.HandleFunc("POST /api/v1/courses", requireAuth(handleCreateCourse(svc)))
return withLogging(withCORS(mux))
}
func handleListCourses(svc *service.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
courses, err := svc.ListCourses(r.Context())
if err != nil {
respondError(w, err)
return
}
respondJSON(w, http.StatusOK, courses)
}
}Legg merke til at vi bruker Go 1.22 sin nye routing-syntaks med metode-prefiks. Middleware er bare funksjoner som wrapper en http.Handler — ingen rammeverk nødvendig.
Dependency injection via interfaces
Dette er kanskje det viktigste mønsteret i Go-koden vår. Ved å definere interfaces der de brukes (ikke der de implementeres), får vi løs kobling og enkel testing:
type CourseRepository interface {
List(ctx context.Context) ([]Course, error)
GetByID(ctx context.Context, id string) (*Course, error)
Create(ctx context.Context, c *Course) error
}
type PostgresCourseRepo struct {
db *sql.DB
}
func (r *PostgresCourseRepo) List(ctx context.Context) ([]Course, error) {
rows, err := r.db.QueryContext(ctx, "SELECT id, title, created_at FROM courses ORDER BY created_at DESC")
if err != nil {
return nil, fmt.Errorf("list courses: %w", err)
}
defer rows.Close()
// ...
}I tester bytter vi ut PostgresCourseRepo med en in-memory-implementasjon. Service-laget merker ingen forskjell.
Feilhåndtering som filosofi
Go tvinger deg til å håndtere feil eksplisitt. Det er verbose, ja — men det betyr også at du aldri har en uventet throw som bobler opp gjennom fem lag med kode. Hver feil er synlig der den oppstår. Vi wrapper alle feil med kontekst via fmt.Errorf("operation: %w", err) slik at feilmeldingene forteller en historie når de dukker opp i loggene.
Graceful shutdown
En detalj som skiller produksjonskode fra hobbyprosjekter — riktig nedstengning:
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":8080", Handler: router}
go func() { srv.ListenAndServe() }()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)
}Når Kubernetes sender SIGTERM, fullfører serveren pågående forespørsler før den stenger ned. Ingen tapte requests, ingen korrupte transaksjoner.
Når du IKKE bør bruke Go
Jeg er ikke en Go-evangelist. Det finnes klare situasjoner der andre språk er bedre valg:
Prototyping og MVP. Hvis du trenger å validere en idé på to uker, er Go for tregt — ikke runtime-ytelsen, men utviklerhastigheten. Node.js eller Python lar deg iterere raskere. Du kan alltid skrive om i Go senere når du vet hva du bygger.
Tunge datatransformasjoner. Python med pandas og numpy er overlegen for datapipelines, analyse og ML-workloads. Go kan gjøre det, men du ender opp med å skrive mye kode som allerede finnes som en one-liner i Python.
Fullstack-applikasjoner. Hvis teamet ditt bygger både frontend og backend, er det en reell fordel å holde seg i TypeScript-økosystemet. Delt validering, delte typer mellom klient og server, og ett språk å vedlikeholde. For Tuli fungerer det fordi vi har dedikerte backend-utviklere.
Tung strengmanipulering. Go sin strenghåndtering er funksjonell men ikke elegant. Hvis applikasjonen din primært handler om å parse, transformere og generere tekst, vil du ha det bedre med Python eller til og med Perl.
Oppsummering
Go er ikke det beste språket. Det er det beste språket for visse problemer. For Tuli Technologies — med behov for høy samtidighet, lav latens, enkel deployment og et lite team som trenger å vedlikeholde koden over tid — var det riktig valg.
Det viktigste er ikke hvilket språk du velger, men at du velger det for de riktige grunnene. Forstå trade-offs, vær ærlig om teamets styrker, og optimaliser for vedlikeholdbarhet over cleverness.