I am very new to go and have deployed a small service with an API endpoint.
I have heard/read that go doesn't use try/catch
so I am trying to figure out how I can "catch" any problems happening from my service call from my API and make sure that the resource server doesn't go down.
My code for my API looks like the following..
I have a routes.go
file with the following
package main
import (
"net/http"
"github.com/gorilla/mux"
)
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}
type Routes []Route
func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
for _, route := range routes {
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(route.HandlerFunc)
}
return router
}
var routes = Routes{
Route{
"CustomerLocationCreate",
"POST",
"/tracking/customer",
CustomerLocationCreate,
},
}
I have a handlers.go
package main
import (
"encoding/json"
"net/http"
"io"
"io/ioutil"
)
//curl -H "Content-Type: application/json" -d '{"userId":"1234"}' http://localhost:8181/tracking/customer
func CustomerLocationCreate(w http.ResponseWriter, r *http.Request) {
var location CustomerLocation
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
if err != nil {
panic(err)
}
if err := r.Body.Close(); err != nil {
panic(err)
}
if err := json.Unmarshal(body, &location); err != nil {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(422) // unprocessable entity
if err := json.NewEncoder(w).Encode(err); err != nil {
panic(err)
}
}
c := RepoCreateCustomerLocation(location)
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(c); err != nil {
panic(err)
}
HandleCustomerLocationChange(c);
}
and I have a bus.go
which has the HandleCustomerLocationChange(...)
function.
func HandleCustomerLocationChange(custLoc CustomerLocation) {
endpoint := og.Getenv("RABBIT_ENDPOINT")
conn, err := amqp.Dial("amqp://guest:guest@" + endpoint)
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
topic := "locationChange"
err = ch.ExchangeDeclare(
topic, // name
"topic", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare an exchange")
// Create JSON from the instance data.
body, _ := json.Marshal(custLoc)
// Convert bytes to string.
err = ch.Publish(
topic, // exchange
"", // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: body,
})
failOnError(err, "Failed to publish a message")
log.Printf(" [x] Sent %s", body)
}
My question is how should I modify both the HandleCustomerLocationChange(...) function and if necessary
CustomerLocationChange(..)` handler to handle errors properly so that if an error occurs, my entire API doesn't go down?
Go suggests a different approach, that errors are not exceptional, they're normal events, just less common.
Taking an example from the code above:
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
if err != nil {
panic(err)
}
Here, a panic (without recovery) terminates the process, shutting down the web server. Seems an overly severe response to not fully reading a request.
What do you want to do? It may be appropriate to tell the client who made the request:
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
You might want to return a json encoded response, or give a generic message to the client avoid exposing too much, and log the specific error details.
For general functions it's idiomatic to return the error as the last return parameter. In the specific example you mentioned:
func HandleCustomerLocationChange(custLoc CustomerLocation)
...
conn, err := amqp.Dial(...)
failOnError(err, "Failed to connect to RabbitMQ")
Instead, check if the connection failed, and return the error to the caller. Handle it in the calling function, or add information and propagate it up the call stack.
func HandleCustomerLocationChange(custLoc CustomerLocation) error
...
conn, err := amqp.Dial(...)
if err != nil {
return fmt.Errorf("failed to connect to RabbitMQ: %s", err)
}
Propagating the error in this way gives a concise explanation of the root cause, like the 5 whys technique, eg:
"did not update client location: did not connect to rabbitmq: network address 1.2.3 unreachable"
Another convention is to deal with errors first and return early. This helps to reduce nesting.
See also the many error handling resources, like error handling in a web application, Go by Example, Error Handling and Go, errors are values and Defer, Panic & Recover. The source code of the error package is interesting, as is Russ Cox's comment on error handling, and Nathan Youngman's To Err is Human.
Also interesting is Upspin's concept of an operational trace, rather than a stack trace.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With