Golang really tries to allow you to do the most common programming tasks in the standard library. The Go team put a lot of effort into designing the API for the standard library to maximise generality without compromising efficiency (with exceptions).
When starting new projects, I now try to see how far I go with importing external dependencies if I know the project most likely won't scale beyond its capabilities.
Here is a list of snippets I've collected over time which I've used to replace third-party dependencies in real production code. Though I've tried to write coherently, this is primarily documentation for myself.
I've been able to replace the test suites functions from github.com/stretchr/testify/require
the following function; often without the rest of my team even noticing it.
func assert[T comparable](t *testing.T, got, wanted T) {
t.Helper()
if wanted != got {
t.Fatalf(`
-- wanted --
%v
-- got --
%v`, expected, got)
}
}
The call to Helper()
tells the testing library that this is just a helper function and that if we fail here, it should report the line of failure as being the place where this function was called.
If you don't have generics, then just use reflect.DeepEqual
to test for equality.
Now simply make the following replacements
require.Equal(t, a, b) -> assert(t, a, b)
require.Nil(t, a) -> assert(t, a, nil)
import "slices" // Since Go 1.21
require.Contains(t, a, b) -> assert(t, slices.Contains(a, b), true)
The errgroup is like salt, you can sprinkle it everywhere to makes everything better.
import "golang.org/x/sync/errgroup"
...
grp, ctx := errgroup.WithContext(ctx)
grp.SetLimit(10)
for _, job := range jobs {
grp.Go(job)
}
if err := grp.Wait(); err != nil {
t.Fatal(err)
}
This is useful for when you want to run multiple goroutinues in parallel, and you want to limit the number of goroutinues active at any one time, and to shutdown all goroutinues if one of them fails.
Go 1.21 introduced structured logging directly into standard library. This just almost a drop in replacement for other structured logging modules like zap, zerolog, and logrus, I just created a regular expression to search and replace all the logrus entries with this and it worked out of the box.
import "log/slog"
slog.Info("Incoming request", "method", req.Method, "url", req.URL)
slog.Error(err.Error())
which produces the following output
Output:
2024/04/10 12:59:36 INFO Incoming request method=GET url=/ping
Go's builtin HTTP router has method routing since Go 1.22 and basic path routing. So you tell it to match get or post requests, and get the path parameters from it.
http.HandleFunc("GET /posts/{id}", handleGet)
http.HandleFunc("POST /posts/{id}", handlePost)
http.HandleFunc("DELETE /posts/{id}", handleDelete)
id := req.PathValue("id")
Implement middleware by simply wrapping all of your handlers.
wrap := func(f httpHandler) httpHandler {
return auth(log(f))
}
http.HandleFunc("GET /ping", wrap(handlePing))
...
func log(f httpHandler) httpHandler {
return func(w http.ResponseWriter, req *http.Request) {
slog.Info("Incoming request", "method", req.Method, "url", req.URL)
f(w, req)
}
}
Here I have added a line to log the request when it comes in, but my team's have used it for authentication, benchmarking response times, recoding metrics. Here, it is also easy to control which middleware is applied to which route, something which is cumbersome to do in other routers.
I also like to convert the signature of the HTTP to return a JSON using the following sendJSON
middleware to standardise the way the HTTP server responds to requests.
type Response struct {
Data any
Error string
}
type myHttpHandler func(*http.Request) (any, int, error)
func sendJSON(f myHttpHandler) httpHandler {
return func(w http.ResponseWriter, req *http.Request) {
var res Response
data, code, err := f(req)
if err != nil {
res.Error = err.Error()
} else {
res.Data = data
}
w.WriteHeader(code)
json.NewEncoder(w).Encode(res)
}
}
// For example
func handlePing(r *http.Request) (data any, code int, err error) {
return "pong", 200, nil
}
Now my handlers return:
Go has a lightweight testing framework which unfortunately I’ve rarely seen used.
It simply generates a mock request and a recording device to directly call HTTP handlers without needing to spin up the HTTP server.
This also allows you to choose which middleware to apply to the handler, for instance if you don't want this route to authenticate (maybe because the test environment doesn't have a token) then you can just not include the authentication middleware here.
import "net/http/httptest"
req := httptest.NewRequest("GET", "http://127.0.0.1/ping", nil)
w := httptest.NewRecorder()
handler(w, req)
var resp Response
json.NewDecoder(w.Body).Decode(&resp)
assert(t, w.Code, 200)
assert(t, resp.Error, "")
assert(t, resp.Data, "pong")
Trace outgoing HTTP request. If the "connection failed" error message isn't enough, then you can trace the request by adding handlers to log messages at each stage of the procedure.
Did the request fail because your machine isn’t networked, is it because DNS failed, or it got to their machine but got no response, maybe its not running on their side.
import "net/http/httptrace"
req, _ := http.NewRequest("GET", "http://google.com", nil)
trace := &httptrace.ClientTrace{
DNSDone: func(dnsInfo httptrace.DNSDoneInfo) {
fmt.Printf("DNS Info: %+v\n", dnsInfo)
},
GotConn: func(connInfo httptrace.GotConnInfo) {
fmt.Printf("Got Conn: %+v\n", connInfo)
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
Output:
DNS Info: {Addrs:[{IP:142.250.187.206 Zone:} {IP:2a00:1450:4009:815::200e Zone:}]...
Got Conn: {Conn:0x14000052008 Reused:false WasIdle:false IdleTime:0s}
The main thing is just see how far you can go
"A little copying is better than a little dependency"
-- Rob Pike
It is "the Golang way".