A linter I really recommend you to use if you're doing SQL in Golang

This is a final article of my mini series about the topic of leaking resources when using SQL databases in Golang. The whole series consists of three parts

Cover image for A linter I really recommend you to use if you're doing SQL in Golang

This is a final article of my mini series about the topic of leaking resources when using SQL databases in Golang. The whole series consists of three parts:

  1. How to get your database access code deadlocked in Golang
  2. Something you probably want to know about if you're using SQLite in Golang
  3. A linter I really recommend you to use if you're doing SQL in Golang (this article)

I didn't plan to write a that many texts but each article led to interesting discussions on the internet that pointed me to more topics worth exploring. Previously, I described how forgetting to close sql.Rows could lead to a deadlock or to your database using disk in a very inefficient way. It turns out that we can use sqlclosecheck linter to automatically catch errors related to not closing database resources correctly and avoid all those problems I wrote about before.

#Running sqlclosecheck locally

To use sqlclosecheck on your computer you need to first install it by executing:

go install github.com/ryanrolds/sqlclosecheck@latest

Let's try the linter on the following program that forgets to close sql.Rows :

package main

import (
 "database/sql"
 _ "modernc.org/sqlite"
)

func main() {
 db, err := sql.Open("sqlite", ":memory:")
 if err != nil {
  panic(err)
 }
 defer db.Close()

 _, err = db.Exec("CREATE TABLE IF NOT EXISTS users (email TEXT)")
 if err != nil {
  panic(err)
 }

 _, err = db.Exec("INSERT INTO users (email, count) VALUES ('test')")
 if err != nil {
  panic(err)
 }

 rows, err := db.Query("SELECT email FROM users") // This is line no. 25
 if err != nil {
  panic(err)
 }

 for rows.Next() {
  // do stuff
 }
}

If the following code lives in a file called main.go then we can run sqlclosecheck against it with the following command:

>sqlclosecheck main.go
main.go:25:23: Rows/Stmt was not closed

As we can see, the linter correctly identifies that we're not closing the rowsobject created in line 25. Once we add defer rows.Close() before for rows.Next() { the linter no longer complains:

>sqlclosecheck main.go
<no output>

#Adding sqlclosecheck to Github Actions

Running linter locally is great but we really should make it automatic on every pull request so that no one has to remember about using the linter.

I did that for all the Turso database projects that use Golang. Some of them like turso-cli are open-source so you can see the PR that adds the check here.

It is really easy to start using sqlclosecheck in your Github Actions. You just need to add the following two steps to the definition of your workflow:

- name: Install sqlclosecheck
      run: go install github.com/ryanrolds/sqlclosecheck@latest

- name: sqlclosecheck
  run: go vet -vettool=${HOME}/go/bin/sqlclosecheck ./...

and with that you can enjoy the peace of mind knowing that your database resources won't be leaked.

If you're using golangci-lint, it is also possible to enable sqlclosecheck for it. All you have to do is to add the following to your .golangci.yaml:

linters:
  enable:
    - sqlclosecheck

#Conclusions

It's relatively easy to forget about closing objects from database/sql package in Golang standard library. That can lead to all sorts of unexpected behaviours like deadlocks or database malfunction. Thankfully, a linter called sqlclosecheck exists that can automatically free us from those problems. I really recommend you to add this linter to your CI checks.

scarf