Content: To learn Go, I ported my backend of a small site from Python to Go and got a fun and painless experience in the process.
I wanted to start learning Go for a while - I liked his philosophy: a small language, a nice learning curve and a very fast compilation (as for a statically-typed language). What finally made me step further and begin to teach him was the fact that I began to see more and more fast and serious programs written in Go - Docker and ngrok , in particular, from those that I recently used.
Go philosophy is not for everyone’s taste (there are no exceptions, you cannot create your own generics, etc.), but it fit well into my mental model. Simple, fast, making things in an obvious way. During porting, I was particularly impressed with how complete the standard library and toolbox was.
I started with a couple of 20 lower case scripts on Go, but this was not enough to understand the language and the ecosystem. So I decided to take a bigger project and chose a backend for my site GiftyWeddings.com to port .
On Python, this was about 1300 lines of code, using Flask, WTForms, Peewee, SQLite, and a few more libraries for S3, image resizing, etc.
For the Go version, I wanted to use as few external dependencies as possible in order to better master the language and work as much as possible with the standard library. In particular, Go has excellent libraries for working with HTTP, and I decided not to look at web frameworks at all. But I still used several third-party libraries for S3, Stripe, SQLite, work with passwords and resize images.
Because of the static typing of Go, and since I used fewer libraries, I expected the code to be more than twice the original number. But the result was 1900 lines (about 50% more than 1300 in Python).
The porting process itself went very smoothly, and most of the business logic was ported by almost mechanically translating the code line to line from Python to Go. I was surprised how well many concepts from Python translate to Go, down to things[:20]
slice syntax.
I also ported part of itsdangerous library that Flask uses, so that I could transparently decode session cookies from the Python service during the migration to the Go version. All the code for cryptography, compression and serialization was in the standard library and it was a simple process.
In general, between Go Tour , Effective Go, and glancing at various examples of code on the Internet, the language learning process went effortlessly. The documentation is fairly brief, but very well written.
Also very pleased with the language toolkit: everything you need to build, run, format, and test the code is available through the go
sub-commands. During development, I simply used go run *.go
to compile on the fly and run the server. Compilation and launch took about a second, which was a breath of fresh air after a battle with swords of 20 seconds incremental and 20 minutes of full compilation in Scala.
The standard Go library has a basic testing package and a ready test runner ( go test
), which finds, compiles and runs your tests. This package is very easy and simple (maybe even too much), but you can easily add your helpers if necessary.
In addition to unit tests, I wrote a test script (also with the use of the testing
package), which runs the HTTP tests set against the real Gifty Weddings server. I did this at the HTTP level, and not at the level of the Go code on purpose, to be able to set this test on the old Python server and make sure that the results are identical. This gave me enough confidence that everything works as it should before I changed the server.
I also did some white-box testing: the script checks the responses, but it also decodes cookies and checks that they contain valid data.
Here is one example of such a test that creates a registry and removes a gift:
func TestDeleteGift(t *testing.T) { client := NewClient(baseURL) response := client.PostJSONOK(t, "/api/create", nil) AssertMatches(t, response["slug"], `temp\d+`) slug := response["slug"].(string) html := client.GetOK(t, "/"+slug, "text/html") _, gifts := ParseRegistryAndGifts(t, html) AssertEqual(t, len(gifts), 3) gift := gifts[0].(map[string]interface{}) giftID := int(gift["id"].(float64)) response = client.PostJSONOK(t, fmt.Sprintf("/api/registries/%s/gifts/%d/delete", slug, giftID), nil) expected := map[string]interface{}{ "id": nil, } AssertDeepEqual(t, response, expected) html = client.GetOK(t, "/"+slug, "text/html") _, gifts = ParseRegistryAndGifts(t, html) AssertEqual(t, len(gifts), 2) }
I think it's just unreal cool - you can say on macOS:
$ GOOS=linux GOARCH=amd64 go build
and this will compile you with a ready-to-use Linux binaries directly on your Mac. And, of course, you can do it in the opposite direction, and cross-compile to and from Windows too. It just works.
Cross-compiling cgo modules (like SQLite) was a bit more complicated, as it required installing the correct version of GCC for compilation - which, unlike Go, was not too trivial. As a result, I just used Docker with the following command to build under Linux:
$ docker run --rm -it -v ~/go:/go -w /go/src/gifty golang:1.9.1 \ go build -o gifty_linux -v *.go
One of the coolest things in Go is that everything feels like solid, reliable : a standard library, tools (go sub-commands), and even third-party libraries. My inner gut shows that this is partly due to the fact that there are no exceptions in Go, and there is some kind of "error handling culture" imposed because of the way that errors are handled.
Network and HTTP libraries especially look cool. You can run a net / http web server (production-level and with HTTP / 2 support, please note) just a couple of lines of code.
The standard library contains most of the necessary things: html / template, ioutil.WriteFile, ioutil.TempFile, crypto / sha1, encoding / base64, smtp.SendMail, zlib, image / jpeg and image / png and you can go on and on. The library APIs are very understandable, and where there are low-level APIs, they are usually wrapped up in higher-level functions, for the most frequent uses.
As a result, writing a web backend without a framework on Go turned out to be quite easy.
I was pleasantly surprised how easy it was to work with JSON in a statically-typed language: you simply call json.Unmarshal
directly into the structure, and with the help of reflection, the correct fields are filled in automatically. It was very easy to load the server configuration from the json file:
err = json.Unmarshal(data, &config) if err != nil { log.Fatalf("error parsing config JSON: %v", err) }
By the way, about err! = Nil - it wasn’t as bad as some people come up with (verification occurs about 70 times on my 1900 lines of code). And it gives a very good feeling "this is really reliable, I correctly handle every error."
By the way, since each request handler works in its gorutin, I also used panic()
calls for things like database calls, which "should always work." And at the very top level, I caught these panic attacks using recover()
and correctly logged them and even added some code to send me a frame-mail to me.
After Python and Flask, my hands were very itchy to use a special value for panic for Not Found or Bad Request responses, but I restrained these urges and decided to go more idiomatic Go (correct return values).
I also like the single synchronous API for everything, plus the great go
keyword to run things in the background gorutin. This contrasts strongly with the Python / C # / JavaScript asynchronous APIs - which lead to new APIs for each I / O function, which doubles the surface of the API.
The format is in time. time.Parse()
bit quirky with the idea of a "reference date", but in practice it is very easy to read when you return to the code later (there is no this "again, what does% b mean here?")
The context
library took some time to enter it, but it turned out to be useful for transmitting various additional information (user session data, etc.) to all involved in processing the request.
Go definitely has fewer of them than Python (but, again, Go is not 26 years old, after all), but there are still a few. Here are some that I noticed during my porting process.
You cannot take the address of the result of a function or expression . You can take the address of a variable or, as a special case, a literal structure, like &Point{2, 3}
, but you cannot do &time.Now()
. This was a bit annoying because it made it create a temporary variable:
now := time.Now() thing.TimePtr = &now
It seems to me that the Go compiler could easily create it for me and allow writing a thing.TimePtr = &time.Now()
.
The HTTP handler accepts http.ResponseWriter instead of returning a response. The http.ResponseWriter API is a bit weird for basic cases, and you need to remember the correct order of calling Header().Set
, WriteHeader
and Write
. It would be easier if the handlers simply returned an object with a response.
It also makes it a bit awkward to get the HTTP response code after calling the handler (for example, for logging). You have to use the fake ResponseWriter, which stores the response code.
There was probably a good reason for this design (efficiency? Compatibility?), But I can't see it right away. I could easily make wrappers for my handlers to return an object, but I decided not to do so.
The template engine seems to be nothing, but there are also some quirks. The html / template package seemed pretty good to me, but it took me a while to understand what the "associated templates" are and what they are for. Slightly more examples, in particular for template inheritance, would be very useful. I liked that the template engine is expanding quite well (for example, it is easy to add your own functions).
Loading templates is a bit strange, so I wrapped the html/template
in my rendering package, which loaded the entire directory and the base template at once.
The pattern syntax is also ok, but the expression syntax is a bit strange. It seems to me that it would be better to use syntax more similar to Go itself. In fact, next time I’ll most likely use something like ego or quicktemplate , because they, in fact, use the Go syntax and do not force them to learn another syntax for expressions.
The database / sql package is too light. I am not the biggest fan of ORM, but it would be nice if database/sql
could use reflection and fill in the structure fields by analogy with encoding/json
. Scan()
is really quite low-level. There is an sqlx package though , which seems to do just that.
Testing is too simple Although I am a fan of go test
and ease of testing in Go as a whole, but it seems to me that it would be good to have at least AssertEqual
-style functions in the standard set. In the end, I just wrote my AssertEqual
and AssertMatches
functions. Although, again, it seems that there are third-party packages that do just that: stretchr / testify .
Package flag
fancy. Apparently, the design was based on the command line flagship package in Google, but the -
format still looks weird, considering that the GNU format with -s
and --long
is practically standard. Again, there are plenty of replacements for this package, including drop-in replacements in which even the code does not need to be changed.
The built-in URL router ( ServeMux ) is too simple because it allows you to do checks only on fixed prefixes, but creating a router based on regexp
was a trivial task (a dozen lines of code).
After Python, the code does look a bit more verbose in some places. Although, as I already wrote, most of the code translates the string into a string from Python, and it was quite natural.
But I really missed the inclusion of lists and dictionaries . It would be great if I could turn this:
gifts := []map[string]interface{}{} for _, g := range registryGifts { gifts = append(gifts, g.Map()) }
in it:
gifts := [g.Map() for _, g := range registryGifts]
Although, in fact, such places were much smaller than I expected.
Similarly, sort.Interface is too verbose . Adding sort.Slice () was the right step. But I still like how easy it is to sort by key in Python, without touching the slice indices at all. For example, to sort the list of strings case-insensitive, in Python it would be:
strs.sort(key=str.lower)
Go:
sort.Slice(strs, func(i, j int) bool { return strings.ToLower(strs[i]) < strings.ToLower(strs[j]) })
And this, in principle, is all I lacked. I expected that I would miss the exceptions, but it turned out not. And, contrary to popular opinion, the lack of generics has disturbed me only once in all time.
I'm not going to stop using Python for the foreseeable future. I continue to use it for my scripts, small projects and web backends. But I'm seriously looking at Go for more projects (static typing makes refactoring much easier) and for utilities or systems where language performance is important (although, frankly, with all the low-level libraries in Python that are written in C, it is often not important).
Summarizing, some of the reasons why I liked Go, and why you might like it:
go
, defer
:=
for concise type inference, type system on interfaces.go build
: build a program (no Makefile needed)go fmt
: automatically format code, no more style warsgo test
: run tests in all *_test.go
filesgo run
: compile and run the program instantly, it feels like scriptingdep
: package manager, go dep
soonSo go ahead, try writing something on Go (Write in Go) !
Source: https://habr.com/ru/post/342218/
All Articles