Live data from Hacker News

REST Servers in Go: Part 1 – standard library

eli.thegreenplace.net

1–10 of 149 posts

Re: REST Servers in Go: Part 1 – standard library

#2
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!)

Re: REST Servers in Go: Part 1 – standard library

#3
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 := 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.

Re: REST Servers in Go: Part 1 – standard library

#4

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

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 }.

Re: REST Servers in Go: Part 1 – standard library

#5
post #3

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 re…

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.

Re: REST Servers in Go: Part 1 – standard library

#6
post #5
post #3

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 re…

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]

Re: REST Servers in Go: Part 1 – standard library

#7
post #5
post #3

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 re…

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.

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.

Re: REST Servers in Go: Part 1 – standard library

#8
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?"

Re: REST Servers in Go: Part 1 – standard library

#9
post #3

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 re…

> 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?

Re: REST Servers in Go: Part 1 – standard library

#10
post #3

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 re…

> 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?

Not necessarily. Panics can be recovered and the stdlib http server recovers panics from handlers.
Post reply on HN