Decision by embedding context
| Context | How to interpolate |
|---|---|
| JSON value (non-test) | jsonu.Jprintf with "%s" (content, caller quotes) or %q (full JSON literal) -- default. jsonu.Jfprintf(w, ...) when writing directly to an io.Writer (http.ResponseWriter, *bytes.Buffer, ...) -- same verbs and escaping as Jprintf. json.Marshal of a typed value only when there is no template at all. fmt.Sprintf with %q only for safe-ASCII operands (appdef.QName.String(), identifiers, fixed enums) -- Go's %q emits \v / \xNN that JSON parsers reject. Never raw "%s" |
| URL path segment | %s with url.PathEscape(seg) |
| URL query value | %s with url.QueryEscape(val) |
| URL host:port | net.JoinHostPort (also satisfies nosprintfhostport); never fmt.Sprintf("%s:%d", ...) |
| File path | filepath.Join; never fmt.Sprintf("%s/%s", ...) |
| Shell argument | pass each arg to exec.Command(name, arg1, arg2, ...); never interpolate |
| SQL literal | parameterized queries / placeholders; never interpolate |
| HTML attribute or text node | "%s" with html.EscapeString(s); never raw %s, never %q (Go-quoting is not HTML escaping; an embedded " still breaks out of the attribute) |
URL inside HTML attribute (href, src) |
URL-escape the user parts first (url.PathEscape / url.QueryEscape), then html.EscapeString the whole URL, then "%s" |
| Go-quoted literal inside a larger string | %q; never "%s" |
| Human log / error message | %q over '%s' so empty strings and embedded quotes stay visible |
_test.go exception: in test-only JSON fixtures / request bodies / expected snippets, plain fmt.Sprintf with "%s" or literal quoted strings is allowed. All other contexts still apply.
Rules
- never wrap
%sin double quotes ("%s") in a rawfmt.Sprintfoutside the_test.goJSON exception; the gocriticsprintfQuotedStringenforces it. For JSON usejsonu.Jprintf(keeps"%s") or%q; for Go-quoted literals use%q; for HTML keep"%s"AND switch escaper tohtml.EscapeString - never wrap
%sin single quotes ('%s') in a human message; use%q - never
json.Marshalan individual string just to embed it in a larger JSON template -- usejsonu.Jprintfinstead - safe
Stringeroperand (e.g.appdef.QName.String(), integer-derived ids): raw%swithout extra escaping is allowed only for human messages; JSON, HTML and URL still require their context escaper (the risk there is structural, not value sanitization) jsonu.Jprintf/json.Marshalof a Gostringcoerces invalid UTF-8 to U+FFFD; for arbitrary binary bytes marshal a[]byte(encoded as base64)fmt.Sprintf("%s", x.String())/fmt.Sprint(stringer)-- usex.String()(gocriticredundantSprint)fmt.Sprintf("%d", i)forint/int64-- usestrconv.Itoa/strconv.FormatInt(perfsprintinteger-format)- string concatenation in a loop -- use
strings.Builder(perfsprintconcat-loop) - a JSON template (
fmt.Sprintforjsonu.Jprintf) MUST be self-balanced:{...}and[...]in the same template; never split the closing brace into a different branch / write - if a JSON object MUST be streamed across multiple writes, exactly one piece of code owns the opening
{and its matching}and emits both on every reachable path (success, error, empty) - ad-hoc error / status responses (
{"status":N,"errorDescription":"..."}): emit the whole object in oneWrite-- ajsonu.Jprintftemplate (default), ajson.Marshaled struct (typed schema), or afmt.Sprintftemplate with%q(safe-ASCII operands) - when an existing non-test JSON
fmt.Sprintfsite is being touched, rewrite tojsonu.Jprintf; if a rewrite is out of scope, at minimum replace every"%s"with%q - streaming JSON to an
io.Writer(http.ResponseWriter,*bytes.Buffer, ...) -- usejsonu.Jfprintfinstead offmt.Fprintf; same verb rules asjsonu.Jprintf. The returnederroris infallible for*bytes.Buffer: handle with// notest+panic(err)perar-golang.md
Anti-patterns
bad -- raw "%s" in JSON (breaks on quotes, backslashes, newlines):
body := fmt.Sprintf(`{"args":{"AppQName":"%s","NumPartitions":%d}}`, app.Name, app.NumParts)
bad -- json.Marshal an individual string to embed it (verbose, throwaway var, ignored error):
name, _ := json.Marshal(app.Name)
body := fmt.Sprintf(`{"args":{"AppQName":%s,"NumPartitions":%d}}`, name, app.NumParts)
good -- jsonu.Jprintf (default for JSON construction):
body := jsonu.Jprintf(`{"args":{"AppQName":"%s","NumPartitions":%d}}`, app.Name, app.NumParts)
acceptable -- fmt.Sprintf with %q for safe-ASCII operands only:
body := fmt.Sprintf(`{"args":{"AppQName":%q,"NumPartitions":%d}}`, app.Name, app.NumParts)
acceptable -- json.Marshal of a typed value when there is no template:
b, _ := json.Marshal(map[string]any{"args": map[string]any{"AppQName": app.Name, "NumPartitions": app.NumParts}})
body := string(b)
bad / good -- URL query value:
url := fmt.Sprintf("/search?q=%s", q) // bad
url := "/search?q=" + url.QueryEscape(q) // good
bad -- HTML attribute with %q (Go-quoting is not HTML escaping):
html := fmt.Sprintf(`<a href=%q>%s</a>`, ref, text)
good -- "%s" with html.EscapeString for both attribute and text:
html := fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(ref), html.EscapeString(text))
good -- URL inside href: URL-escape the user parts first, then HTML-escape the whole URL:
ref := "/items/" + url.PathEscape(itemID) + "?q=" + url.QueryEscape(query)
html := fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(ref), html.EscapeString(text))
bad / good -- redundant Sprint of a Stringer:
s := fmt.Sprint(qname) // bad
s := qname.String() // good
Checklist before writing any fmt.Sprintf / fmt.Errorf / fmt.Fprintf
- Classify the embedding context (see table above)
_test.goJSON fixtures: plain"%s"allowed; all other contexts still apply- Pick the verb and escaper from the table for that exact context
- On a
sprintfQuotedStringlinter hit -- in JSON switch tojsonu.Jprintf(keep"%s") or%q(safe-ASCII only); in HTML switch the escaper tohtml.EscapeStringand keep"%s"(do NOT switch to%q) - Drop
fmt.Sprintf("%s", x)/fmt.Sprint(x); usestrconvinstead offmt.Sprintf("%d", ...)
Source: voedger/voedger — distributed by TomeVault.