[HN Gopher] REST Servers in Go: Part 1 - standard library ___________________________________________________________________ REST Servers in Go: Part 1 - standard library Author : tutfbhuf Score : 62 points Date : 2021-01-16 20:21 UTC (2 hours ago) (HTM) web link (eli.thegreenplace.net) (TXT) w3m dump (eli.thegreenplace.net) | jrockway wrote: | Good introduction. A few thoughts: | | 1) Be careful with locks in the form "x.Lock(); x.DoSomething(); | x.Unlock()". If DoSomething panics, you will still be holding the | lock, and that's pretty much the end of your program. ("x.Lock(); | defer x.Unlock(); x.DoSomething()" avoids this problem, but | obviously in the non-panic case, the lock is released at a | different time than in this implementation. Additional tweaking | is required.) | | Generally I don't like locks in the request critical path because | waiting for a lock is uncancelable, but in this very simple case | it doesn't matter. For more complicated concurrency requirements, | consider the difference between x.Lock()/x.Do()/x.Unlock vs. | select { case x := <-ch: doSomethingWithX(x); case | <-request.Context().Done(): error(request.Context().Err()) }. The | channel wait can be cancelled when the user disconnects, or hits | the stop button in the error, or the request timeout is reached. | | 2) Long if/else statements are harder to read than a switch | statement. Instead of: if(foo == "bar") { | // Bar } else if (foo == "baz") { // Baz | } else { // Error } | | You might like: switch(foo) { case "bar": | // Bar case "baz": // Baz default: | // Error } | | These are exactly semantically equivalent, and neither protect | you at compile-time from forgetting a case, but there is slightly | less visual noise. Worth considering. | | 3) I have always found that error handling in http.HandlerFunc- | tions cumbersome. The author runs into this, with code like: | foo, err := Foo() if err != nil { http.Error(w, | ...) return } bar, err := Bar() if | err != nil { http.Error(w, ...) return | } | | Basically, you end up writing the error handling code a number of | times, and you have to do two things in the "err != nil" block, | which is annoying. I prefer: func | DoTheActualThing() ([]byte, error) { if | everythingIsFine { return []byte(`{"result":"it | worked and you are cool"}`), nil } return | nil, errors.New("not everything is okay, feels sad") } | | Then in your handler function: func ServeHTTP(w | http.ResponseWriter, req *http.Request) { result, err | := DoTheActualThing() if err != nil { | http.Error(w, ...) return } | w.Header().Set("content-type", "application/json") | w.WriteHeader(http.StatusOK) w.Write(result) } | | In this simple example, it doesn't matter, but when you do more | than one thing that can cause an error, you'll like it better. | makeworld wrote: | > _Be careful with locks in the form "x.Lock(); | x.DoSomething(); x.Unlock()". If DoSomething panics, you will | still be holding the lock, and that's pretty much the end of | your program._ | | Interesting, thanks. But isn't panicking the end of your | program anyway? Could you provide another example where no | using defer causes problems? | acrispino wrote: | Not necessarily. Panics can be recovered and the stdlib http | server recovers panics from handlers. | klohto wrote: | Would you please expand more on your first point regarding | using channels instead of Locks? It's hard for me to wrap a | head around it without practical example. | [deleted] | Philip-J-Fry wrote: | Not the OP but basically imagine that instead of locking a | mutex to handle synchronised writes, you spawn a goroutine | which just reads from a channel and writes the data. | | If that goroutine hasn't finished processing then the channel | will be blocked, just like a mutex. | | So in your handler you can use a select statement to either | write to the channel OR read from the | request.Context().Done(). The request context only lives as | long as the request. So if the connection drops or times out | then the context gets cancelled and a value is pushed onto | the done channel and your read is unblocked. | | Because you use a select statement then which ever operation | unblocks first is what happens. If the write channel unblocks | then you get to write your value. If your request context | gets cancelled then you can report an error. The request | context will always get cancelled eventually, unlike a mutex | which will wait forever. | anderspitman wrote: | I think I've managed to get by with less dependencies in Go than | any other language. It somehow walks the line between JavaScript | leftpad and Python "stdlib is where modules go to die". | | I don't think there's been a single instance where I've thought | "why can't stdlib do this?" nor "why the heck is this in stdlib?" | samuelroth wrote: | Nice article! This is an interesting approach, much less likely | to make Go devs' blood boil over unnecessary libraries. | | My only question is why the server / HTTP handlers have to deal | with the Mutex. That seems like a "leak" from the `TaskStore` | abstraction, which otherwise I really like. (Thank you for not | using channels in that interface!) | jrockway wrote: | I think it's necessary to leak the details of the mutex until | you have some sort of transaction object to abstract that away. | In a concurrent workload, these two things are different: | store.Lock() store.WriteKey("foo", "bar") x := | store.ReadKey("foo") store.Unlock() // x is | always "bar" | | And: store.Lock() store.WriteKey("foo", | "bar") store.Unlock() store.Lock() x | := store.ReadKey("foo") store.Unlock() // x could | be whatever another goroutine set "foo" to, not the "bar" that | you just wrote. | | In a more complicated app, you'll have library that acts as the | datastore, with transaction objects that abstract away the | actual mutex (which will be something more complicated): | var x string err := db.DoTx(func(tx *Tx) { | tx.Write("foo", "bar") x = tx.Read("foo") }) | if err != nil { ... } // what x is depends on the | details of your database; maybe you're running at "read | uncommitted", maybe you're running at "serializable". | | But, even in the simple examples, it's worth thinking about the | difference between lock { write; read } and lock { write }; | lock { read }. | tptacek wrote: | With respect to DRY'ing the JSON code, isn't something like this | workable: err = | json.NewEncoder(w).Encode(&task) | | I know there used to be a reason why this was disfavored but | thought it had been addressed in the stdlib. ___________________________________________________________________ (page generated 2021-01-16 23:00 UTC)