String interpolation in Go

Avatar of the author Willem Schots
3 Jul, 2024
~5 min.
RSS

Coming from other languages it can be surprising that Go does not support string interpolation.

What do I mean by string interpolation?

Well, take this snippet of PHP for example:

$x = 49;
$msg = "Hello world";
echo "The number is $x and the message is $msg";

When running the program PHP will evaluate the double quoted string and automatically replace the $x and $msg placeholders with the appropriate values. In the end, The number is 49 and the message is Hello world will be echoed.

This evaluation and replacement is called string interpolation. You’re essentially providing mini-templates in which you can directly refer to variables.

As said before, Go does not support string interpolation.

In this article we’ll take a look at alternatives and we’ll discuss why Go doesn’t support string interpolation.

Formatting

You’ve probably seen the fmt package before if you’ve done a “Hello world” style tutorial.

fmt stands for “format” or “formatting”. This package provides a lot of functionality for dealing with formatted text.

fmt.Sprint

If you want a syntax that is superficially similar to string interpolation, you should take a look at fmt.Sprint.

This function concatenates all provided arguments into a single string.

  • Arguments are combined in the order they are provided.
  • Will use default format for each type unless the type implements the fmt.Stringer interface, in which case fmt.Sprint will call the String method.
  • Spaces are inserted between non-string arguments.

If we recreate our earlier example in Go, we get the following code.

main.go
package main

import (
	"fmt"
)

func main() {
	x := 49
	msg := "Hello world"
	out := fmt.Sprint("The number is ", x, " and the message is ", msg)

	fmt.Println(out)
}

Alternatively, if you immediately want to print the string to standard output you can use fmt.Print instead of fmt.Sprint.

This pattern applies generally in the fmt package, every function prefixed with “S” returns a string and has a version without the prefix that prints the string to standard output instead.

fmt.Sprintf

fmt.Sprintf also returns a single string but it uses a layout for formatting instead of concatenating the arguments.

This layout uses “verbs” that will replaced by provided values. Some common verbs are:

  • %v for values in their default format.
  • %d base 10 integers.
  • %b base 2 integers.
  • %f decimal points but no exponents.
  • %s string or bytes.
  • %q quoted string or bytes.

This is similar to the C printf and scanf verbs.

Some verbs allow for formatting options. You can find the full list here.

If we again recreate out earlier example, it looks like this:

main.go
package main

import (
	"fmt"
)

func main() {
	x := 49
	msg := "Hello world"
	out := fmt.Sprintf("The number is %d and the message is %s", x, msg)

	fmt.Println(out)
}

In my view there are two downsides to fmt.Sprintf:

  • Order of verbs in the layout and function arguments are coupled. This can lead to issues when you have several layouts with verbs in different positions.
  • Formatting options can be quite cryptic. I often need to check the documentation.

Templating

When you’re dealing with larger pieces of text where you want to refer to variables by name, re-use templates or define custom formatting rules.

If you’re dealing with large pieces of text and want to:

  • Refer to variables by name.
  • Re-use similar parts of text.
  • Define custom formatting rules or functions.

You’re probably in need of a full templating system.

The Go standard library contains two templating packages: text/template for plain text and html/template for HTML templates.

The reason for this split is that the HTML package has extra functionality to secure HTML output against specific attacks.

Below we use the text/template package to recreate our earlier example:

main.go
package main

import (
	"log"
	"os"
	"text/template"
)

func main() {
	const tmpl = "The number is {{.Number}} and the message is {{.Message}}"

	// create a new template called test and parse the above text template.
	t, err := template.New("test").Parse(tmpl)
	if err != nil {
		log.Fatal(err)  
	}

	type Data struct {
		Number int
		Message string
	}

	err = t.Execute(os.Stdout, Data{
		Number: 49,
		Message: "Hello World",
	})
	if err != nil {
		log.Fatal(err)  
	}
}

As you can see, the text/template package takes a two-step approach to creating templates:

  1. Create the textual template(s) in memory (t in this case). This needs to be done only once for each template after which it can be re-used.
  2. Execute the template. This will insert the provided values into the template and write the result to the provided io.Writer.

The html/template package works in a similar way.

Concatenation

To concatenate strings together you can also use the + or += operators. This only works if all involved values are strings.

See the example below.

main.go
package main

import (
	"fmt"
)

func main() {
	x := 49
	xString := fmt.Sprint(x)
	msg := "Hello world"
	out := "The number is " + xString + " and the message is " + msg

	fmt.Println(out)
}

Here you can see that we first need to convert the integer x to a string before we can concatenate it.

Why no string interpolation?

There have been several proposals to add string interpolation to Go, see:

The main arguments against these prosals seem to be:

  • While possibly more readable, for simple cases it’s roughly similar to a fmt.Sprint call.
  • If layout options are supported, it’s not necessarily more readable than a fmt.Sprintf call.
  • It will require “lifting” formatting rules into the Go spec, complicating the spec.

This is related to string interpolation in general, but I personally prefer to keep strings “portable”. String interpolation binds strings to the current scope and context, making it a bit more work to move them around when refactoring.

Anyway, that’s it for this article, let me know if you have any questions or suggestions.

Happy coding!

🎓

Keep Learning. Subscribe to my Newsletter.

Gain access to more content and get notified of the latest articles:

I send emails every 1-2 weeks and will keep your data safe. You can unsubscribe at any time.

Hello! I'm the Willem behind willem.dev

I created this website to help new Go developers, I hope it brings you some value! :)

You can follow me on Twitter/X, LinkedIn or Mastodon.

Thanks for reading!