Ett år senare: Agenterna kom och jag hamnade i golang

Det har gått snart ett år sedan jag skrev mitt förra inlägg som handlade om hur med AI börjat flytta oss från att söka på Stack Overflow till att i stället finna lösningar i en chattruta. Under det år som har gått så har rätt mycket hänt och det som nog förvånar mig mest är inte att verktygen blivit bättre utan i stället gjort med mina egna val som jag gör när jag utvecklar. Allt oftare blir GO programspråket jag föredrar, antalet färdiga ramverk och tredjepartsbibliotek blir allt färre och det är nog precis tvärtom mot vad jag trodde skulle hända. Här ska jag försöka förklara varför.

Innan vi börjar själva inlägget så vill jag här förklara att tonen på Internet har blivit en gemensam röst skriven av robotar och för att inte tappa bort människorna i yrket så har jag valt att återuppta bloggandet igen med mina reflektioner. Förändringen av rollen för oss som arbetar inom IT har aldrig gått snabbare och därför vill jag i skrift som jag själv skriver förmedla vardagen för en kodare som skrivit kod i princip varenda dag i över 40 år. Bilderna däremot har min AI-kompis fått rita.

Vi slutade prata om generativ AI

När jag skrev det förra inlägget var det fortfarande “generativ AI” som gällde och man använde chattar på ett sätt där man klistrade in en kodsnutt och fick ett svar baserat på kodsnutten utan sitt sammanhang. Man fick svar utifrån den frågeställningen och ibland fungerade den föreslagna lösningen men allt som oftast inte. Detta lyckades Cursor vara tidigt ute med genom att man kunde använda en agent som kunde förstå ett större sammanhang, men i ärlighetens namn, när man sett vad som skapades med Cursor och Github Copilot så var det ganska taffliga försök till maskingenererad och regelstyrd utveckling. Maskinen var självsäker och gjorde glatt och självsäkert fel och tackade oss för hur uppmärksamma vi var och hur rätt vi hade när det gick fel. Men det gick fort när det gick fel och vi satt där och var helt tagna över att det gick att göra så här.

Numera pratar väl ingen om generativ AI längre utan om olika former av agentdriven utveckling och förrsten nästan allt är agentdrivet och hur man orkestrerar sina agenter. Skillnaden är verkligen inte bara marknadsföringsmässig och fina ord, det är skillnad på riktigt. Skillnaden nu är att verktyget kan läsa hela kodbasen, göra en ändring på åtta ställen, köra testerna, se att bygget gick sönder, laga det innan jag ens hunnit titta efter och förstå vad som gått sönder. Det är en helt ny fördelning av arbetet mot att fråga AI om råd.

Min förväntan här var att jag skulle dra mig ännu högre upp i abstraktionerna, att jag skulle sitta och beskriva vad jag ville på en högre nivå och låta ramverken göra stora delar av jobbet åt mig. Så blev det inte. Det blev faktiskt tvärt om.

Abstraktionen fanns där av ett skäl

Innan vi tittar på varför jag börjat kapa bort abstraktionerna behöver vi titta på varför de fanns där från början. Abstraktioner, ramverk och tredjepartsbiblioteken är skapade för att förenkla för oss människor när det är vi som skriver koden och för att förenkla för oss att vi inte behöver förstå hur saker fungerar egentligen. Ett bra funktionsbiblotek hanterar sådant som människan kanske inte ens vet att det behöver hanteras och dessa sparar hundratals eller tusentals rader kod. Det är kodrader som man kanske ofta annars skulle behöva skriva och dessutom skriva samma sak i varje projekt.

Abstraktionen har ju alltid haft ett pris och det priset är att man byter ut kunskap om problemet som ska lösas mot kunskap om hur man använder biblioteket. Man sitter och lär sig hur man i LINQ eller i sitt ORM för att entitetsramverket ska generera den SQL-kod jag redan från början hoppats skulle efterfrågas i databasen. Man sitter sedan och försöker justera sin kod för att den ska fungera så som man vill det, men ramverkets egen optimerare gör något smart som säkert är smart för just det tillfället men inte det jag söker i det sammanhang frågan ska ställas. Har man kört några projekt där allt fungerat bra med små datavolymer har nog alla lyckats bygga in det här N+1-problemet där varje rad man får tillbaka i sin tur ställer en ny eller kanske ett helt gäng nya frågor till databasen och det syns inte i koden alls eftersom det ser ut som en vanlig loop över en lista.

Vad som händer när det tråkiga inte längre kostar tid

Innan gick det flera timmar åt att skriva och underhålla de där enkla och repetitiva styckena kod. Mappning fram och tillbaka, felhantering på varje anrop, validering, samma scanningsloop för hundrade gången, saker som man gjorde framför en TV-serie och som egentligen är busenkelt men som tar tid. Här kunde ramverken spara mycket tid om man investerade tiden i att lära sig ramverk och bibliotek och här blir produktiviteten avsevärt mycket högre även om felsökningen kan bli mer komplicerad.

Med dagens AI-modeller stämmer inte längre den kalkylen lika bra för de ramverk som underlättar kodskrivning med abstraktion och framför allt inom det som kallas reflection och mappning av modeller. Repetitiv kod som är tydlig och lätt att läsa är numera det billiga alternativet och då finns det plötsligt inte lika stor anledning att dra in ännu en tredjepartskomponent med komplexitet i licenser, ytterligare beroenden till dess underkomponenter etc för att undvika att skriva repetitiv kod. Ännu viktigare är att man med rätt god tillförliglighet kan generera testfall som går sönder när man ändrat på ett ställe utan att ändringen nått ut i hela lösningen och det är väl just den tryggheten som gjorde att man innan valde ett ramverk. En annan stor vinst med tydligheten är att AI kommer att förstå din kod bättre genom att man kapar abstraktioner, vilket leder till färre missförstånd och vilseledande genererad kod.

Om jag ska göra något i en central del av en funktion där jag vet precis hur den SQL jag vill ställa en fråga till databasen om ser ut där jag från början vet hur jag ska hantera raderna, hur jag ska koppla samman flera tabeller och framför allt hur jag löser ev N+1-problem direkt med SQL så kan jag göra det utan att riskera att göra lösningen för komplicerad. Den blir i stället tydlig. Prestandavinsten med entitetsramverk, javisst förlorar jag den, men det finns en motsvarande optimerare i SQL-motorn som säkerställer att min fråga utförs på ett så effektivt sätt som möjligt och vill jag rätta den kan jag göra det direkt i stället för att gissa hur jag ska formuera den frågan som automatiskt skapas. Om jag behöver läsa mer data inne i en loop så gör jag såklart det, men då är det uppenbart och ett ärende för en uppföljning kan skapas för att fixa det senare och om så en test påvisar en brist finns det redan ett ärende i backloggen. Det viktiga här är att valet är mitt och inte entitetsramverkets som den hanterade åt mig tyst i bakgrunden.

Varför just GO

Jag har alltid varit nyfiken på golang ända sedan man på Google skapade det. Jag har haft svårt att göra det till mitt förstahandsval dels för att det innan kunde bli svårt att läsa, men också att det saknades något som kallas generics och en bra hantering av paket och moduler inom en kodbas för att strukturera sin kod. Med generics och moduler på plats samt att vem som helst kan använda AI för att ta reda på vad koden gör så är det för vissa typer av tillämpningar ett naturligt val, här är några av anledningarna:

  • Standardbiblioteket räcker förvänansvärt långt. HTTPO-server, JSON, kryptering, testning, hantering av minnet, det mesta finns redan där och inga tredjepartsfunktioner behövs.
  • Enkelheten. Datastrukturer och funktioner. Inga arvshierarkier att navigera. Det man tänker sig ska hända händer och inget annat.
  • Trådningsmodellen. Goroutines och channels är precis den sorts byggstenar som fanns i C när man utvecklade så för nu ganska många år sedan innan jag gick över till Java och C#.
  • Färre beroenden att förvalta. Det som du inte plockat in i din go.mod behöver heller inte uppdateras när nästa säkerhetshål dyker upp. Hela hanteringen med hur nya hål uppstår i gammal kod för att den analyseras för sårbarheter med AI gör att attackytan när man har en funktion exponerad mot Internet helt oförutsägbart stor.

Man behöver helt ärligt inte alls något ramverk ovanpå golang, men eftersom det inte är nödvändigt att skriva all kod som gör standardsaker varje gång man startar ett nytt projekt så är det bekvämt att ha en par väl valda tredjepartskomponenter och bibliotek som får följa med. Här använder jag Gin då det är väl beprovat och löser saker som routing, middlewares och bindning av modeller från JSON på ett förutsägbart sätt som inte ändras mellan versionerna. Jag använder också ett antal tredjepartskomponenter som gör det svårare enkelt som t.ex. sqlx för SQL-hantering eller om jag inte är beroende av prestandan GORM som entitetsramverk. Det är väl lite självmotsägande här då jag just skrivit om att man inte behöver använda tredjepartskomponenter i lika stor utsträckning, men skillnaden är att Gin inte står mellan mig och databasen, mellan mig och behörighetskontrollen utan den tar hand om just det som är likadant i varje projekt ändå.

Vertical Slice och kod som är tillräckligt bra

Den arkitektur som visat sig passa mig väldigt bra i golang-sammanhang är den som brukar kallas för Vertical Slice, alltså att allt som hör till en funktion ligger tillsammans i stället för att spridas ut över lager där funktionerna är grupperade per struktur. Jag använder i större projekt en intern Clean Architecture inom varje slice för att undvika att det blir för många filer i samma katalog och har sedan en namnstandard på att i varje katalog finns vissa filer som innehåller de funktioner jag förväntar mig. Tillsammans med den defactostandard för projektullägg som finns i golang-världen, ett inslag av ovan nämnda Clean Architecture och de gamla hederliga mönstren från GIF i namngivning landar det i ett mycket förutsättningsbart mönster där det är lätta att hitta rätt när man ska titta på koden vid ett senare tillfälle.

Poängen är just att hitta tillbaka. När jag efter ett halvår sedan ska in i ett av de projekt som jag och mina AI-assistenter skrivit tillsammans så vet jag hur jag ska hitta tillbaka och hitta rätt. Jag behöver inte jaga anrop i olika mappstrukturer utan varje slice innehåller allt det den behöver och det är väl definierat hur man delar funktioner mellan slices och när det är gemensam infrastruktur som behövs.

En lärdom jag tagit med mig av är att eftersom AI av sin natur kommer ge dig olika svar varje gång du frågar inom den ram du har gett den friheter, så lät jag i begynnelsen av agentisk kodning agenterna skapa en struktur som var väldigt snygg och som bländade vid första anblicken, men så här när jag 10-12 månader senare ska in och titta i koden så är det så tydligt att varje lösning är elegant utförd, men också att min startsträcka är mycket lägre för att hitta rätt och att jag rent behöver använda AI för att förstå något. Tillräckligt bra kod som är förutsägbar slår perfekt kod som är unik, åtminstone i förvaltarens perspektiv.

När jag inte väljer GO

Jag tänker verkligen inte sluta med ramverk, högnivåspråk eller beroenden till tredjepartsbibliotek. Ska jag bygga ett gränssnitt, integrationer som ligger i Microsoftmiljön, en frontendlösning med ett interaktivt gränssnitt som man bygger med React, där teamet som ska förvalta lösningen är experter på C# och .NET eller Java med Spring, eller en lösning där man kör Symfony och PHP så kommer jag såklart välja dessa språk och miljöer. Jag kommer också föredra att luta sig tillbaka mot ett lagom stort och förvaltat ramverk före att bygga ihop min egen mix av tredjepartsbibliotek. Vid dessa tillfällen, då är helt enkelt inte go ett lämpligt val, för att bygga koden från början det är det lilla jobbat och början på förvaltningen som är det långsiktiga arbetet som man behöver optimera för när man vill att system ska leva över en längre tid och skapa nytta.

Behöver jag ett enkelt API, en funktion som ska fungera som en liten tjänst i en större helhet, eller i en lösning där jag vet att man inte kommer ha en budget till förvaltning som räcker långt som ska fungera år efter år, eller där man exponerar något mot Internet och riskerar ha en beroendekedja mot tredjepartskomponenter t.ex. i NodeJS-världen, då kommer jag allt oftare välja golang än jag gjort innan. Särskilt ofta för de komponenter som ligger närmast kärnlogiken eller de ställen där det gör mest ont om något går fel.

Om du sitter i ett större bolag med gemensamma plattformsval är det förstås värt att stämma av innan man tar in ett nytt språk i utvecklarstacken, det är så mycket mer än den första implementationen som kommer bero på att organisationen är redo att ta in ett språk som GO. Ska också vara ärlig och säga att jag själv inte kört något riktigt stort projekt på GO ännu som har flera år på nacken, däremot har jag några mellanstora projekt som har några år på nacken varav ett blivit ordentligt refaktorerat under sommaren med hjälp av de nya agentiska verktygen, vilket har visat sig vara en mycket trevlig upplevelse, särskilt med tanke på att den viktiga logiken inte döljs i abstraktioner utan de senaste modellerna har direkt förstått koden. Därför är det här inlägget mer en riktning än en slutsats.

Om du nu tagit dig hela vägen genom detta inlägg och kanske gått åt ett annat håll och blivit mer förtjust i ramverk och tredjepartskomponenterna sedan agenterna kom för att hjälpa till, så lämna gärna en kommentar. Jag är rätt nyfiken på om det bara är jag som sitter här nu.

Om Fredrik
Fredrik Gustavsson

IT-konsult som är konstant nyfiken. Intresserad av många saker. Jobbar på Nexer på dagarna, men driver nätbutik som hobbyprojekt sedan 15 år. Har nog programmerat det mesta genom åren och skriver nu rätt mycket programvara i Microsoftmiljön C# eller PHP.

Leave a Comment