Varför vi bytte från Python till Go

Uppdaterad den 14 maj 2019 för att bättre återspegla de förbättringar som Go har genomgått under de senaste två åren (pakethantering, bättre prestanda, snabbare kompilering och ett mognare ekosystem) Att byta till ett nytt språk är alltid ett stort steg, särskilt när bara en av dina teammedlemmar har tidigare erfarenhet av det språket. I början av året bytte vi Streams primära programmeringsspråk från Python till Go. I det här inlägget förklaras några av anledningarna till varför vi bestämde oss för att lämna Python bakom oss och byta till Go. Tack till Ren Sakamoto för översättningen av Why we switched from Python to Go till japanska, なぜ私達は Python から Go に移行したのか.

Reason 1 – Performance

Go är snabbt! Go är extremt snabb. Prestandan liknar den för Java eller C++. För vårt användningsfall är Go vanligtvis 40 gånger snabbare än Python. Här är ett litet benchmarkspel där Go jämförs med Python.

Rsak 2 – Språkets prestanda spelar roll

För många tillämpningar är programmeringsspråket helt enkelt limmet mellan appen och databasen. Språkets prestanda i sig spelar vanligtvis ingen större roll. Stream är dock en API-leverantör som driver en feeds- och chattplattform för 700 företag och mer än 500 miljoner slutanvändare. Vi har optimerat Cassandra, PostgreSQL, Redis osv. i åratal, men så småningom når du gränserna för det språk du använder. Python är ett fantastiskt språk men dess prestanda är ganska trög för användningsfall som serialisering/deserialisering, rangordning och aggregering. Vi stötte ofta på prestandaproblem där Cassandra tog 1 ms på sig att hämta data och Python spenderade de följande 10 ms på att omvandla dem till objekt.

Skäl 3 – Utvecklarens produktivitet & Att inte bli för kreativ

Ta en titt på det här lilla kodutsnittet av Go-kod från handledningen How I Start Go. (Detta är en bra handledning och en bra utgångspunkt för att lära sig lite om Go.)

Om du är ny i Go är det inte mycket som kommer att förvåna dig när du läser detta lilla kodutdrag. Det visar upp flera uppdrag, datastrukturer, pekare, formatering och ett inbyggt HTTP-bibliotek. När jag började programmera älskade jag alltid att använda Pythons mer avancerade funktioner. Python låter dig bli ganska kreativ med den kod du skriver. Du kan till exempel:

  • Använda MetaClasses för att själv registrera klasser vid initialisering av koden
  • Swap out True och False
  • Lägga till funktioner till listan över inbyggda funktioner
  • Overload-operatorer via magiska metoder
  • Använda funktioner som egenskaper via dekoratorn @property

De här funktionerna är roliga att leka med men, men de flesta programmerare håller med om att de ofta gör koden svårare att förstå när man läser någon annans arbete. Go tvingar dig att hålla dig till grunderna. Detta gör det mycket lätt att läsa någons kod och omedelbart förstå vad som händer. Observera: Hur ”lätt” det är beror förstås på ditt användningsområde. Om du vill skapa ett grundläggande CRUD API skulle jag fortfarande rekommendera Django + DRF, eller Rails.

Skäl 4 – Concurrency & Channels

Som språk försöker Go hålla saker och ting enkla. Det introducerar inte många nya begrepp. Fokus ligger på att skapa ett enkelt språk som är otroligt snabbt och lätt att arbeta med. Det enda område där det blir innovativt är goroutiner och kanaler. (För att vara 100 % korrekt började konceptet CSP 1977, så denna innovation är mer ett nytt tillvägagångssätt för en gammal idé). Goroutines är Go:s lättviktsstrategi för threading, och kanaler är det föredragna sättet att kommunicera mellan goroutines. Goroutiner är mycket billiga att skapa och tar bara några få KBs extra minne i anspråk. Eftersom goroutiner är så lätta är det möjligt att ha hundratals eller till och med tusentals av dem igång samtidigt. Du kan kommunicera mellan goroutiner med hjälp av kanaler. Go-körtiden hanterar all komplexitet. Goroutinerna och det kanalbaserade tillvägagångssättet för samtidighet gör det mycket enkelt att använda alla tillgängliga CPU-kärnor och hantera samtidiga IO – allt utan att komplicera utvecklingen. Jämfört med Python/Java kräver körning av en funktion på en goroutin minimal boilerplate-kod. Du sätter helt enkelt nyckelordet ”go” före funktionsanropet:

https://tour.golang.org/concurrency/1 Gos tillvägagångssätt för samtidighet är mycket lätt att arbeta med. Det är ett intressant tillvägagångssätt jämfört med Node där utvecklaren måste ägna stor uppmärksamhet åt hur asynkron kod hanteras. En annan bra aspekt av samtidighet i Go är tävlingsdetektorn. Den gör det enkelt att ta reda på om det finns några kapplöpningstillstånd i din asynkrona kod.

Knock knock Race condition Who’s there?

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

Här är några bra resurser för att komma igång med Go och kanaler:

  • https://gobyexample.com/channels
  • http://guzalexander.com/2013/12/06/golang-channels-tutorial.htmlhttps://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
  • Goroutiner vs Gröna trådar

Rsak 5 – Snabb kompilering

Vår största mikrotjänst som skrivits i Go tar för närvarande 4 sekunder att kompilera. Go:s snabba kompileringstider är en stor produktivitetsvinst jämfört med språk som Java och C++ som är kända för sin tröga kompileringshastighet. Jag tycker om svärdskampen, men det är ännu trevligare att få saker gjorda medan jag fortfarande kommer ihåg vad koden ska göra:

Skäl 6 – Möjligheten att bygga ett team

För det första ska vi börja med det uppenbara: det finns inte lika många Go-utvecklare jämfört med äldre språk som C++ och Java. Enligt StackOverflow kan 38 % av utvecklarna Java, 19,3 % C++ och endast 4,6 % Go. GitHub-data visar en liknande trend: Go används i större utsträckning än språk som Erlang, Scala och Elixir, men är mindre populärt än Java och C++. Som tur är är Go ett mycket enkelt och lätt att lära sig. Det tillhandahåller de grundläggande funktioner du behöver och ingenting annat. De nya begrepp som introduceras är ”defer”-kommandot och inbyggd hantering av samtidighet med ”go-routiner” och kanaler. (För puristerna: Go är inte det första språket som implementerar dessa koncept, bara det första som gör dem populära). Alla Python-, Elixir-, C++-, Scala- eller Java-utvecklare som ansluter sig till ett team kan bli effektiva i Go inom en månad på grund av dess enkelhet. Vi har funnit att det är lättare att bygga upp ett team av Go-utvecklare jämfört med många andra språk. Om du anställer personer i konkurrenskraftiga ekosystem som Boulder och Amsterdam är detta en viktig fördel.

Rsak 7 – Starkt ekosystem

För ett team av vår storlek (~20 personer) spelar ekosystemet roll. Du kan helt enkelt inte skapa värde för dina kunder om du måste återuppfinna varje liten funktionalitet. Go har bra stöd för de verktyg som vi använder. Solida bibliotek fanns redan tillgängliga för Redis, RabbitMQ, PostgreSQL, Template parsing, Task scheduling, Expression parsing och RocksDB. Go:s ekosystem är en stor vinst jämfört med andra nyare språk som Rust eller Elixir. Det är naturligtvis inte lika bra som språk som Java, Python eller Node, men det är gediget och för många grundläggande behov hittar du högkvalitativa paket som redan finns tillgängliga.

Skäl 8 – Gofmt, Enforced Code Formatting

Låt oss börja med vad är Gofmt? Och nej, det är inte ett svordomarord. Gofmt är ett fantastiskt kommandoradsverktyg som är inbyggt i Go-kompilatorn för att formatera din kod. När det gäller funktionalitet är det mycket likt Pythons autopep8. Även om programmet Silicon Valley skildrar något annat gillar de flesta av oss egentligen inte att argumentera om tabuleringar kontra mellanslag. Det är viktigt att formateringen är konsekvent, men själva formateringsstandarden spelar egentligen inte så stor roll. Gofmt undviker all denna diskussion genom att ha ett officiellt sätt att formatera din kod.

Rsak 9 – gRPC och protokollbuffertar

Go har förstklassigt stöd för protokollbuffertar och gRPC. Dessa två verktyg fungerar mycket bra tillsammans för att bygga mikrotjänster som behöver kommunicera via RPC. Du behöver bara skriva ett manifest där du definierar de RPC-anrop som kan göras och vilka argument de tar. Både server- och klientkod genereras sedan automatiskt från detta manifest. Den resulterande koden är både snabb, har ett mycket litet nätverksavtryck och är lätt att använda. Från samma manifest kan du generera klientkod till och med för många olika språk, t.ex. C++, Java, Python och Ruby. Så, inga fler tvetydiga REST-slutpunkter för intern trafik, som du måste skriva nästan samma klient- och serverkod för varje gång. .

Nackdel 1 – Brist på ramverk

Go har inte ett enda dominerande ramverk som Rails för Ruby, Django för Python eller Laravel för PHP. Detta är ett ämne som debatteras livligt inom Go-communityt, eftersom många förespråkar att man inte bör använda ett ramverk till att börja med. Jag håller helt med om att detta är sant för vissa användningsfall. Men om någon vill bygga ett enkelt CRUD API kommer de att ha det mycket lättare med Django/DJRF, Rails Laravel eller Phoenix. Uppdatering: Som det påpekades i kommentarerna finns det flera projekt som tillhandahåller ett ramverk för Go. Revel, Iris, Echo, Macaron och Buffalo verkar vara de främsta utmanarna. För Streams användningsfall föredrar vi att inte använda ett ramverk. Men för många nya projekt som vill tillhandahålla ett enkelt CRUD API kommer bristen på ett dominerande ramverk att vara en allvarlig nackdel.

Nackdel 2 – Felhantering

Go hanterar fel genom att helt enkelt returnera ett fel från en funktion och förvänta sig att din anropande kod hanterar felet (eller att den returnerar det uppåt i den anropande stapeln). Även om det här tillvägagångssättet fungerar är det lätt att förlora räckvidden för vad som gick fel för att säkerställa att du kan ge ett meningsfullt fel till dina användare. Paketet errors löser detta problem genom att låta dig lägga till kontext och en stapelspårning till dina fel. Ett annat problem är att det är lätt att glömma bort att hantera ett fel av misstag. Statiska analysverktyg som errcheck och megacheck är praktiska för att undvika att göra dessa misstag. Även om dessa lösningar fungerar bra känns det inte riktigt rätt. Man förväntar sig att korrekt felhantering ska stödjas av språket.

Nackdel 3 – Pakethantering

Uppdatering: Go:s pakethantering har kommit en bra bit på väg sedan det här inlägget skrevs. Go-moduler är en effektiv lösning, det enda problemet som jag har sett med dem är att de bryter vissa statiska analysverktyg som errcheck. Här finns en handledning för att lära sig använda Go med hjälp av Go-moduler. Go:s pakethantering är på intet sätt perfekt. Som standard har den inget sätt att ange en specifik version av ett beroende och det finns inget sätt att skapa reproducerbara byggen. Python, Node och Ruby har alla bättre system för pakethantering. Med rätt verktyg fungerar dock Go:s pakethantering ganska bra. Du kan använda Dep för att hantera dina beroenden så att det blir möjligt att specificera och pinna versioner. Utöver det har vi bidragit med ett verktyg med öppen källkod som heter VirtualGo som gör det lättare att arbeta med flera projekt som är skrivna i Go.

Python vs Go

Uppdatering: Prestandaskillnaden mellan Python och Go har ökat sedan det här inlägget skrevs. (Go blev snabbare och Python inte) Ett intressant experiment som vi genomförde var att ta vår funktionalitet för rankade flöden i Python och skriva om den i Go. Ta en titt på det här exemplet på en rangordningsmetod:

Både Python- och Go-koden måste göra följande för att stödja den här rangordningsmetoden:

  1. Parse uttrycket för poängen. I det här fallet vill vi omvandla den här strängen ”simple_gauss(time)*popularity” till en funktion som tar en aktivitet som indata och returnerar en poäng som utdata.
  2. Skapa delfunktioner baserat på JSON-konfigurationen. Vi vill till exempel att ”simple_gauss” ska anropa ”decay_gauss” med en skala på 5 dagar, en förskjutning på 1 dag och en avklingningsfaktor på 0,3.
  3. Parse ”defaults”-konfigurationen så att du har en reservfunktion om ett visst fält inte är definierat för en aktivitet.
  4. Använd funktionen från steg 1 för att poängsätta alla aktiviteter i flödet.

Utvecklingen av Python-versionen av rangordningskoden tog ungefär 3 dagar. Det inkluderar att skriva koden, enhetstester och dokumentation. Därefter har vi ägnat ungefär 2 veckor åt att optimera koden. En av optimeringarna var att översätta poänguttrycket (simple_gauss(time)*popularity) till ett abstrakt syntaxträd. Vi implementerade också cachinglogik som förberäknade poängen för vissa tider i framtiden. Att utveckla Go-versionen av den här koden tog däremot ungefär fyra dagar. Prestandan krävde ingen ytterligare optimering. Så även om den inledande delen av utvecklingen var snabbare i Python, krävde den Go-baserade versionen i slutändan betydligt mindre arbete av vårt team. Som en extra fördel presterade Go-koden ungefär 40 gånger snabbare än vår högt optimerade Python-kod. Detta är bara ett enda exempel på de prestandaförbättringar som vi har upplevt genom att byta till Go. Det är naturligtvis att jämföra äpplen med päron:

  • Rankningskoden var mitt första projekt i Go
  • Go-koden byggdes efter Pythonkoden, så användningsfallet förstods bättre
  • Go-biblioteket för analysering av uttryck var av exceptionell kvalitet

Din körning kommer att variera. Vissa andra komponenter i vårt system tog betydligt längre tid att bygga i Go jämfört med Python. Som en allmän trend ser vi att det tar något mer ansträngning att utveckla Go-kod. Vi lägger dock mycket mindre tid på att optimera koden för prestanda.

Elixir vs Go – The Runner Up

Ett annat språk vi utvärderade är Elixir. Elixir bygger på den virtuella maskinen Erlang. Det är ett fascinerande språk och vi övervägde det eftersom en av våra lagmedlemmar har massor av erfarenhet av Erlang. För våra användningsfall märkte vi att Go:s råprestanda är mycket bättre. Både Go och Elixir gör ett bra jobb när det gäller att betjäna tusentals samtidiga förfrågningar. Om man tittar på prestanda för enskilda förfrågningar är Go dock betydligt snabbare för vårt användningsfall. En annan anledning till att vi valde Go framför Elixir var ekosystemet. För de komponenter vi behövde hade Go mer mogna bibliotek medan Elixir-biblioteken i många fall inte var redo för produktionsanvändning. Det är också svårare att utbilda/finna utvecklare för att arbeta med Elixir. Dessa skäl fick balansen att tippa över till förmån för Go. Phoenix-ramverket för Elixir ser dock fantastiskt ut och är definitivt värt en titt.

Slutsats

Go är ett mycket prestandaspråk med bra stöd för samtidighet. Det är nästan lika snabbt som språk som C++ och Java. Även om det tar lite mer tid att bygga saker med Go jämfört med Python eller Ruby, sparar du massor av tid på att optimera koden. Vi har ett litet utvecklingsteam på Stream som driver flöden och chatt för över 500 miljoner slutanvändare. Go har ett bra ekosystem, är lätt att komma igång med för nya utvecklare, har snabb prestanda, har bra stöd för samtidighet och är en produktiv programmeringsmiljö, vilket gör Go till ett utmärkt val. Stream använder fortfarande Python för vår instrumentpanel, webbplats och maskininlärning för personliga flöden. Vi kommer inte att säga adjö till Python inom kort, men framöver kommer all prestandaintensiv kod att skrivas i Go. Vårt nya chatt-API är också skrivet helt och hållet i Go. Om du vill lära dig mer om Go kan du kolla in blogginläggen nedan. Om du vill lära dig mer om Stream är den här interaktiva handledningen en bra utgångspunkt.

Mer läsning om att byta till 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är dig 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

Lämna ett svar

Din e-postadress kommer inte publiceras.