Teaching go-critic a new trick with %#q

Recently, I learned that %#q in the fmt package wraps a string in backquotes. Unlike plain %q, which adds double quotes ", the # flag switches to backquotes `.

This little discovery led me to extend a go-critic check that didn’t know about it.

Screenshot of the PR on go-critic to extend the sprintfQuotedString checker

The discovery

I came across %#q while reviewing a pull request in Lima , where I’m a reviewer.

It turns out this isn’t just a cosmetic preference. The PR solves a real readability problem on Windows, described in issue #4898 . Windows file paths use backslashes, and %q escapes each one, so a path renders with doubled backslashes:

msg="failed to resolve vm for \"C:\\Users\\john\\.lima\\default\\lima.yaml\""

With %#q, the path is wrapped in backquotes and no escaping is needed, which is far easier to read:

msg="failed to resolve vm for `C:\Users\john\.lima\default\lima.yaml`"

That’s why the PR replaces %q with %#q in logs and errors so that paths and identifiers are wrapped in backquotes instead of double quotes:

// before
return fmt.Errorf("failed to read meta data file %q: %w", metaDataPath, err)
// after
return fmt.Errorf("failed to read meta data file %#q: %w", metaDataPath, err)

The verbs %q and %#q exist exactly so you don’t have to wrap a string by hand:

  • Wrapping a string with "%s" can be simplified to %q, which also safely escapes special characters.
  • Wrapping a string with "`%s`" can be simplified to %#q, which uses raw backtick quoting when the string contains no backtick characters; otherwise it falls back to the %q form.
ℹ️ Go string literals

Double-quoted strings ("...") process escape sequences such as \\ and \n, while backtick strings (`...`) are raw without escaping.

It’s all clearly documented in the fmt package docs ; I just had never run into the # flag before.

The gap in go-critic

ℹ️ What is go-critic?

go-critic is a Go linter that bundles a large collection of checks on style, performance issues, and common errors. It is supported by golangci-lint via the gocritic linter.

go-critic has a sprintfQuotedString check that recommends using %q instead of manually wrapping a string with "%s":

fmt.Printf(`"%s"`, v) // suggestion: use %q instead of "%s" for quoted strings

But the check didn’t detect the backquote case. This code passed without any warning:

fmt.Printf("`%s`", v) // not detected, but could be simplified to %#q

So the linter caught one half of the pattern but not the other.

Extending the check

I created a PR that extends the sprintfQuotedString rule to also flag manually backquoted strings and suggest %#q.

Now both forms are covered:

  • fmt.Sprintf(`"%s"`, s) transforms to fmt.Sprintf("%q", s)
  • fmt.Sprintf("`%s`", s) transforms to fmt.Sprintf("%#q", s)

A small change, but it closes a gap and helps everyone use the idiomatic fmt verbs.

The payoff: simpler code

These verbs aren’t just shorter, they let you delete code. While working on revive , I used the same insight to simplify code by replacing a hand-escaped "%s" with %q:

// before
fmt.Sprintf("Import alias \"%s\" is redundant", imp.Name.Name)
// after
fmt.Sprintf("Import alias %q is redundant", imp.Name.Name)

Conclusion

Reading the docs of tools you use every day pays off. A flag as small as # can simplify your format strings, and a linter is the perfect place to encode that knowledge so the whole community benefits.


By the way, I’m a revive maintainer, a linter expert, and a golangci-lint contributor . If you want to write a custom linter, or set up CI/CD that helps AI write better code, reach out — I’d be happy to help.


See also