[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)