Hvorfor vi skiftede fra Python til Go

Opdateret 14. maj 2019 for bedre at afspejle forbedringer i Go i de sidste 2 år (pakkehåndtering, bedre ydeevne, hurtigere kompileringstider og et mere modent økosystem) At skifte til et nyt sprog er altid et stort skridt, især når kun én af dine teammedlemmer har tidligere erfaring med sproget. I begyndelsen af året skiftede vi Stream’s primære programmeringssprog fra Python til Go. Dette indlæg vil forklare nogle af grundene til, at vi besluttede at forlade Python og foretage skiftet til Go. Tak til Ren Sakamoto for at oversætte Hvorfor vi skiftede fra Python til Go til japansk, なぜ私達は Python から Go に移行したのか.

Ræsonnement 1 – Ydelse

Go er hurtigt! Go er ekstremt hurtig. Ydelsen svarer til den samme som i Java eller C++. I vores brugssituation er Go typisk 40 gange hurtigere end Python. Her er et lille benchmarkspil, der sammenligner Go vs. Python.

Ræsonnement 2 – Sprogets ydeevne er vigtig

For mange applikationer er programmeringssproget simpelthen limen mellem appen og databasen. Selve sprogets ydeevne betyder normalt ikke meget for selve sproget. Stream er imidlertid en API-udbyder, der driver en feeds- og chatplatform for 700 virksomheder og mere end 500 millioner slutbrugere. Vi har optimeret Cassandra, PostgreSQL, Redis osv. i årevis, men til sidst når du grænserne for det sprog, du bruger. Python er et fantastisk sprog, men dets ydeevne er ret træg i forbindelse med use cases som serialisering/deserialisering, rangering og aggregering. Vi løb ofte ind i ydelsesproblemer, hvor Cassandra ville tage 1 ms at hente dataene, og Python ville bruge de næste 10 ms på at omdanne dem til objekter.

Ræsonnement 3 – Udviklerproduktivitet & Ikke at blive for kreativ

Kig på dette lille uddrag af Go-kode fra How I Start Go tutorial. (Det er en fantastisk tutorial og et godt udgangspunkt for at lære lidt om Go.)

Hvis du er nybegynder i Go, er der ikke meget, der vil overraske dig, når du læser dette lille kodestykke. Det viser flere opgaver, datastrukturer, pointers, formatering og et indbygget HTTP-bibliotek. Da jeg først begyndte at programmere, elskede jeg altid at bruge Pythons mere avancerede funktioner. Python giver dig mulighed for at blive ret kreativ med den kode, du skriver. Du kan f.eks:

  • Brug MetaClasses til at selvregistrere klasser ved kodeinitialisering
  • Swap out True og False
  • Tilføj funktioner til listen over indbyggede funktioner
  • Overload operatorer via magiske metoder
  • Brug funktioner som egenskaber via @property decorator

Disse funktioner er sjove at lege med, men, som de fleste programmører vil være enige i, gør de ofte koden sværere at forstå, når man læser en andens arbejde. Go tvinger dig til at holde dig til det grundlæggende. Det gør det meget nemt at læse andres kode og straks forstå, hvad der foregår. Bemærk: Hvor “nemt” det er, afhænger selvfølgelig af dit brugsscenarie. Hvis du vil lave et grundlæggende CRUD API vil jeg stadig anbefale Django + DRF, eller Rails.

Ræsonnement 4 – Concurrency & Channels

Som sprog forsøger Go at holde tingene simple. Det introducerer ikke mange nye begreber. Fokus er på at skabe et simpelt sprog, der er utrolig hurtigt og nemt at arbejde med. Det eneste område, hvor det bliver innovativt, er goroutiner og kanaler. (For at være 100 % korrekt startede CSP-konceptet i 1977, så denne innovation er mere en ny tilgang til en gammel idé). Goroutiner er Go’s letvægtstilgang til threading, og kanaler er den foretrukne måde at kommunikere mellem goroutiner på. Goroutiner er meget billige at oprette og tager kun et par KBs ekstra hukommelse i brug. Fordi goroutiner er så lette, er det muligt at have hundredvis eller endda tusindvis af dem kørende på samme tid. Du kan kommunikere mellem goroutiner ved hjælp af kanaler. Go-køringstiden håndterer al kompleksiteten. Goroutinerne og den kanalbaserede tilgang til samtidighed gør det meget nemt at bruge alle tilgængelige CPU-kerner og håndtere samtidige IO – alt sammen uden at komplicere udviklingen. Sammenlignet med Python/Java kræver det at køre en funktion på en goroutine minimal boilerplate-kode. Du sætter blot nøgleordet “go” foran funktionskaldet:

https://tour.golang.org/concurrency/1 Go’s tilgang til samtidighed er meget nem at arbejde med. Det er en interessant tilgang sammenlignet med Node, hvor udvikleren skal være meget opmærksom på, hvordan asynkron kode håndteres. Et andet godt aspekt af samtidighed i Go er race-detektoren. Det gør det nemt at finde ud af, om der er nogen racebetingelser i din asynkrone kode.

Knock knock Racebetingelse Hvem er der?

– I Am Devloper (@iamdevloper) November 11, 2013

Her er et par gode ressourcer til at komme i gang med Go og kanaler:

  • https://gobyexample.com/channels
  • https://tour.golang.org/concurrency/2
  • http://guzalexander.com/2013/12/06/golang-channels-tutorial.html
  • https://www.golang-book.com/books/intro/10
  • https://www.goinggo.net/2014/02/the-nature-of-channels-in-go.html
  • Goroutines vs Green threads

Ræsonnement 5 – Hurtig kompileringstid

Vores største mikroservice skrevet i Go tager i øjeblikket 4 sekunder at kompilere. Go’s hurtige kompileringstider er en stor produktivitetsgevinst sammenlignet med sprog som Java og C++, der er berømte for deres langsomme kompileringshastighed. Jeg kan godt lide sværdkamp, men det er endnu rarere at få tingene gjort, mens jeg stadig kan huske, hvad koden skal gøre:

Ræsonnement 6 – Muligheden for at opbygge et team

Først og fremmest skal vi starte med det indlysende: Der er ikke så mange Go-udviklere sammenlignet med ældre sprog som C++ og Java. Ifølge StackOverflow kender 38 % af udviklerne Java, 19,3 % kender C++ og kun 4,6 % kender Go. GitHub-data viser en lignende tendens: Go er mere udbredt end sprog som Erlang, Scala og Elixir, men mindre populært end Java og C++. Heldigvis er Go et meget enkelt og let at lære sprog. Det giver de grundlæggende funktioner, du har brug for, og intet andet. De nye koncepter, som det introducerer, er “defer”-anvisningen og indbygget styring af samtidighed med “go-routiner” og kanaler. (Til puristerne: Go er ikke det første sprog, der implementerer disse koncepter, men blot det første, der gør dem populære). Enhver Python-, Elixir-, C++-, Scala- eller Java-udvikler, der slutter sig til et team, kan blive effektiv i Go inden for en måned på grund af dets enkelhed. Vi har fundet det lettere at opbygge et team af Go-udviklere sammenlignet med mange andre sprog. Hvis du ansætter folk i konkurrencedygtige økosystemer som Boulder og Amsterdam, er dette en vigtig fordel.

Ræsonnement 7 – Stærkt økosystem

For et team af vores størrelse (~20 personer) betyder økosystemet noget. Du kan simpelthen ikke skabe værdi for dine kunder, hvis du er nødt til at genopfinde hver eneste lille del af funktionaliteten. Go har stor støtte til de værktøjer, vi bruger. Der var allerede solide biblioteker til rådighed for Redis, RabbitMQ, PostgreSQL, Template parsing, Task scheduling, Expression parsing og RocksDB. Go’s økosystem er en stor gevinst i forhold til andre nyere sprog som Rust eller Elixir. Det er selvfølgelig ikke så godt som sprog som Java, Python eller Node, men det er solidt, og til mange grundlæggende behov finder du allerede tilgængelige pakker af høj kvalitet.

Ræsonnement 8 – Gofmt, Enforced Code Formatting

Lad os starte med, hvad er Gofmt? Og nej, det er ikke et bandeord. Gofmt er et fantastisk kommandolinjeværktøj, der er indbygget i Go-compileren til at formatere din kode. Med hensyn til funktionalitet ligner det meget Pythons autopep8. Selv om showet Silicon Valley fremstiller det modsatte, kan de fleste af os ikke rigtig lide at diskutere tabs vs. mellemrum. Det er vigtigt at formateringen er konsistent, men selve formateringsstandarden er egentlig ikke så vigtig. Gofmt undgår alle disse diskussioner ved at have én officiel måde at formatere din kode på.

Ræsonnement 9 – gRPC og protokolbuffere

Go har førsteklasses understøttelse for protokolbuffere og gRPC. Disse to værktøjer fungerer meget godt sammen til opbygning af mikroservices, der skal kommunikere via RPC. Du behøver blot at skrive et manifest, hvor du definerer de RPC-opkald, der kan foretages, og hvilke argumenter de tager. Både server- og klientkode genereres derefter automatisk ud fra dette manifest. Den resulterende kode er både hurtig, har et meget lille netværksfodaftryk og er nem at bruge. Ud fra det samme manifest kan du generere klientkode til mange forskellige sprog, som f.eks. C++, Java, Python og Ruby. Så ikke flere tvetydige REST-endpoints til intern trafik, som du skal skrive næsten den samme klient- og serverkode til hver gang. .

Ulempe 1 – Manglende rammeværk

Go har ikke et enkelt dominerende rammeværk som Rails til Ruby, Django til Python eller Laravel til PHP. Dette er et emne for ophedet debat inden for Go-fællesskabet, da mange går ind for, at man ikke bør bruge en ramme til at begynde med. Jeg er helt enig i, at dette er sandt for nogle brugssituationer. Men hvis nogen ønsker at bygge et simpelt CRUD API vil de have det meget nemmere med Django/DJRF, Rails Laravel eller Phoenix. Opdatering: Som det blev påpeget i kommentarerne er der flere projekter, der leverer en ramme til Go. Revel, Iris, Echo, Macaron og Buffalo synes at være de førende kandidater. I forbindelse med Stream’s use case foretrækker vi ikke at bruge en ramme. Men for mange nye projekter, der ønsker at levere et simpelt CRUD API, vil manglen på en dominerende ramme være en alvorlig ulempe.

Ulempe 2 – Fejlhåndtering

Go håndterer fejl ved simpelthen at returnere en fejl fra en funktion og forvente, at din kaldende kode håndterer fejlen (eller returnerer den op ad den kaldende stak). Selv om denne fremgangsmåde fungerer, er det let at miste omfanget af, hvad der gik galt, for at sikre, at du kan give en meningsfuld fejl til dine brugere. Pakken errors løser dette problem ved at give dig mulighed for at tilføje kontekst og en stack trace til dine fejl. Et andet problem er, at det er let at glemme at håndtere en fejl ved et uheld. Statiske analyseværktøjer som errcheck og megacheck er praktiske til at undgå at begå disse fejl. Selvom disse workarounds fungerer godt, føles det ikke helt rigtigt. Man ville forvente, at korrekt fejlhåndtering blev understøttet af sproget.

Ulempe 3 – Pakkehåndtering

Ajourføring: Go’s pakkehåndtering er kommet langt siden dette indlæg blev skrevet. Go-moduler er en effektiv løsning, det eneste problem jeg har set med dem er, at de bryder nogle statiske analyseværktøjer som errcheck. Her er en vejledning til at lære at bruge Go ved hjælp af Go-moduler. Go’s pakkehåndtering er på ingen måde perfekt. Som standard har den ikke en måde at angive en specifik version af en afhængighed på, og der er ingen måde at skabe reproducerbare builds på. Python, Node og Ruby har alle bedre systemer til pakkehåndtering. Med de rigtige værktøjer fungerer Go’s pakkehåndtering dog ganske godt. Du kan bruge Dep til at administrere dine afhængigheder, så du kan angive og fastgøre versioner. Udover det har vi bidraget med et open source-værktøj kaldet VirtualGo, som gør det nemmere at arbejde på flere projekter skrevet i Go.

Python vs Go

Opdatering: Ydelsesforskellen mellem Python og Go er blevet større, siden dette indlæg blev skrevet. (Go blev hurtigere og Python blev ikke) Et interessant eksperiment, vi udførte, var at tage vores rangordnede feed-funktionalitet i Python og omskrive den i Go. Tag et kig på dette eksempel på en rangordningsmetode:

Både Python- og Go-koden skal gøre følgende for at understøtte denne rangordningsmetode:

  1. Parse udtrykket for scoren. I dette tilfælde ønsker vi at omdanne denne streng “simple_gauss(time)*popularity” til en funktion, der tager en aktivitet som input og returnerer en score som output.
  2. Skab delfunktioner baseret på JSON-konfigurationen. For eksempel ønsker vi, at “simple_gauss” kalder “decay_gauss” med en skala på 5 dage, en forskydning på 1 dag og en henfaldsfaktor på 0,3.
  3. Parse “defaults”-konfigurationen, så du har en fallback, hvis et bestemt felt ikke er defineret på en aktivitet.
  4. Brug funktionen fra trin 1 til at score alle aktiviteter i feedet.

Udviklingen af Python-versionen af rangeringskoden tog ca. 3 dage. Det omfatter skrivning af koden, enhedstests og dokumentation. Dernæst har vi brugt ca. 2 uger på at optimere koden. En af optimeringerne bestod i at oversætte scoreudtrykket (simple_gauss(time)*popularity) til et abstrakt syntaksetræ. Vi implementerede også caching-logik, som forudberegnede scoren for bestemte tidspunkter i fremtiden. I modsætning hertil tog det ca. 4 dage at udvikle Go-versionen af denne kode. Ydelsen krævede ikke nogen yderligere optimering. Så selv om den indledende del af udviklingen var hurtigere i Python, krævede den Go-baserede version i sidste ende betydeligt mindre arbejde fra vores team. Som en ekstra fordel ydede Go-koden ca. 40 gange hurtigere end vores stærkt optimerede Python-kode. Dette er blot et enkelt eksempel på de ydelsesforbedringer, vi har oplevet ved at skifte til Go. Det er naturligvis at sammenligne æbler med pærer:

  • Rangeringskoden var mit første projekt i Go
  • Go-koden blev bygget efter Python-koden, så use case blev bedre forstået
  • Go-biblioteket til analyse af udtryk var af exceptionel kvalitet

Din kilometervis vil variere. Nogle andre komponenter i vores system tog væsentligt mere tid at bygge i Go sammenlignet med Python. Som en generel tendens kan vi se, at det kræver lidt mere arbejde at udvikle Go-kode. Vi bruger dog meget mindre tid på at optimere koden med henblik på ydeevne.

Elixir vs Go – The Runner Up

Et andet sprog, som vi evaluerede, er Elixir. Elixir er bygget på toppen af den virtuelle Erlang-maskine. Det er et fascinerende sprog, og vi overvejede det, da et af vores teammedlemmer har masser af erfaring med Erlang. For vores brugssager bemærkede vi, at Go’s rå ydeevne er meget bedre. Både Go og Elixir vil gøre et godt stykke arbejde med at betjene tusindvis af samtidige forespørgsler. Men hvis man ser på den individuelle anmodningspræstation, er Go betydeligt hurtigere til vores use case. En anden grund til, at vi valgte Go frem for Elixir, var økosystemet. For de komponenter, vi havde brug for, havde Go mere modne biblioteker, mens Elixir-bibliotekerne i mange tilfælde ikke var klar til produktionsbrug. Det er også sværere at uddanne/finde udviklere til at arbejde med Elixir. Disse grunde fik balancen til at tippe til fordel for Go. Phoenix-rammen til Elixir ser dog fantastisk ud og er bestemt et kig værd.

Konklusion

Go er et meget performant sprog med god understøttelse for samtidighed. Det er næsten lige så hurtigt som sprog som C++ og Java. Selv om det tager en smule mere tid at bygge ting med Go sammenlignet med Python eller Ruby, sparer du en masse tid, der bruges på at optimere koden. Vi har et lille udviklingsteam hos Stream, der driver feeds og chat for over 500 millioner slutbrugere. Go’s kombination af et godt økosystem, nem indføring for nye udviklere, hurtig ydeevne, solid understøttelse af samtidighed og et produktivt programmeringsmiljø gør det til et godt valg. Stream anvender stadig Python til vores dashboard, websted og maskinlæring til personliggjorte feeds. Vi vil ikke sige farvel til Python lige foreløbig, men fremover vil al ydelsesintensiv kode blive skrevet i Go. Vores nye Chat API er også skrevet udelukkende i Go. Hvis du vil lære mere om Go, kan du tjekke de nedenstående blogindlæg. Hvis du vil lære mere om Stream, er denne interaktive vejledning et godt udgangspunkt.

Mere læsning om at skifte til Golang

  • https://movio.co/en/blog/migrate-Scala-to-Go/
  • https://hackernoon.com/why-i-love-golang-90085898b4f7
  • https://sendgrid.com/blog/convince-company-go-golang/
  • https://dave.cheney.net/2017/03/20/why-go

Lære Go

  • https://learnxinyminutes.com/docs/go/
  • https://tour.golang.org/
  • http://howistart.org/posts/go/1/
  • https://getstream.io/blog/building-a-performant-api-using-go-and-cassandra/
  • https://www.amazon.com/gp/product/0134190440
  • Go Rocket Tutorial

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.