Go Recipes - Formatting Output

Table of Contents

Formatting Output

These recipes offer some tips for formatting Go output. We first dicuss some less well known features of the standard fmt package and then go on to discuss some ways of formatting your output that the fmt package doesn't offer.

For other recipes see here.

Custom formatting of data types

Summary

The fmt package is the standard tool you should use for formatting output but you can extend it and tune the behaviour for your own data types. The simplest way to adjust the output that fmt will produce is to write a 'String' method for your type as follows.

type Location struct{
      file string
      line int
}

// String returns a string representing the contents of the Location
func (l Location)String() string {
      return l.file + ":" + strconv.Itoa(l.line)
}

If you are using gosh to run the code you will need to use the 'global' parameter for the code shown above.

Then when you print the value you will see it has been formatted using the 'String' method.

l := Location{
      file: "main.go",
      line: 42,
}

fmt.Println("Println:", l)
fmt.Printf("     %%s:  %s\n", l)
fmt.Printf("     %%v:  %v\n", l)
fmt.Printf("     %%+v: %+v\n", l)
fmt.Printf("     %%#v: %#v\n", l)

Try calling this with and without the String method declared.

The String method is a useful way of controlling how your types are displayed. It is used to produce the output from the formats "%v" and "%+v" (or "%s").

References

See the standard package fmt

Custom formatting of the Go-syntax representation

The fmt package verb 'v' can also be used to produce a Go-syntax representation of the value when given a '#' flag. We'll discuss here how we can control the output in this case.

Summary

The String method on a type is used, if it exists, by the fmt package to display a value in a lot of cases. The "%#v" format though will not use this, to change the way that the Go-syntax representation appears you need to provide a GoString method for your type. You might want to provide this method to hide secret credentials so that they don't appear in log files for instance. For this use case remember that you'll also need to provide a String method. Also be aware that this is not sufficient to prevent the secret from being exposed but it might help to avoid accidental exposure.

type Secret struct{
      username string
      password string
}

// String returns a string representing the contents of the Secret
func (s Secret)String() string {
      return s.username
}

// GoString returns a string representing the contents of the Secret
// in a Go-syntax representation
func (s Secret)GoString() string {
      return `main.Secret{username:"` + s.username + `", password: "..."}`
}

Then when you print the value you will see it has been formatted using the GoString method.

s := Secret{
      username: "Nick",
      password: "TopSecret",
}

fmt.Println("Println:", s)
fmt.Printf("     %%s:  %s\n", s)
fmt.Printf("     %%v:  %v\n", s)
fmt.Printf("     %%+v: %+v\n", s)
fmt.Printf("     %%#v: %#v\n", s)

References

See the standard package fmt

Complete control over formatting

Summary

If you need complete control over the display of your value you can provide a Format method. This gives your formatting code access to the values of the width and precision options and to the flags and the verb chosen to print. It is the most complicated to use but gives you the most control over formatting.

type Secret struct{
      username string
      password string
}

// Format formats a Secret
func (s Secret)Format(f fmt.State, verb rune) {
      switch verb {
      case 'z':
	      fmtStr := "Secret for %"
	      if wid, ok := f.Width(); ok {
		      fmtStr += strconv.Itoa(wid)
	      }
	      if prec, ok := f.Precision(); ok {
		      fmtStr += "." + strconv.Itoa(prec)
	      }
	      fmtStr += "s"
	      f.Write([]byte(fmt.Sprintf(fmtStr, s.username)))
      default:
	      f.Write([]byte("%!"+string(verb)+"(Secret)"))
      }
}

Then when you print the value you will see it has been formatted using the Format method. Note that the verb you use does not have to be one of the standard runes that the fmt package uses.

s := Secret{
      username: "Nick",
      password: "TopSecret",
}

fmt.Println("Println:", s)
fmt.Printf("     %%z:  %z\n", s)
fmt.Printf("     %%v:  %v\n", s)
fmt.Printf("     %%+v: %+v\n", s)
fmt.Printf("     %%#v: %#v\n", s)

Note that if a Format method is available it is used for all the value forms ("%v", "%+v" and "%#v") whereas the String method is only used for the first two and the GoString method is only used for the last form.

References

See the standard package fmt

Wrapping text

Summary

If you have a large block of text that you want to display to the user you have a problem. The issue is that simply printing it out will mean that it wraps at random places in the text as the terminal wraps at the edge of the screen. It might even be truncated at the edge of the screen depending on the terminal you are using.

You can wrap the text yourself by adding newlines at the appropriate places but that is laborious, tedious and needs to be repeated whenever you change the text.

Alternatively you can use a package to do the wrapping for you.

Discussion

There are several text wrapping packages available. I've chosen three and I'll discuss them in order of complexity. Some of the packages have similar names and if you are using gosh to try out the examples you might find that goimports doesn't correctly populate the import statements, especially if you don't already have the module in your cache. In this case you can use gosh's "-import" parameter to give the correct package to import.

  • go-wordwrap

    The first package is go-wordwrap (with package name "wordwrap"); this offers one method: WrapString which simply returns a string wrapped at word boundaries up to a maximum line length. Like this:

    s := "The quality of mercy is not strained."+
          " It droppeth as the gentle rain from heaven"
    fmt.Println(wordwrap.WrapString(s, 30))
    

    This will split the string into two lines, each no longer than 30 characters. It will only split at word boundaries so if a word is very long then it may exceed the maximum length.

    The maximum line length cannot be negative since the parameter is a uint so your code won't even compile if you try. If you set it to zero then no wrapping is performed. You might argue that this is a bug since it's not something you would want.

    Note that this package is still accepting pull requests but has only seen minimal changes recently.

  • wordwrap

    The next package is wordwrap; this is a bit more complicated. You first generate a WrapperFunc and then call it on the strings you want to wrap. Like this:

    s := "The quality of mercy is not strained."+
          " It droppeth as the gentle rain from heaven"
    wrapper := wordwrap.Wrapper(30, false)
    fmt.Println(wrapper(s))
    

    Again the string is split into two lines, each no longer than 30 characters. The advantage of controlling this approach is that you can just pass the function around and you only need to set the parameters when you first create it. Unlike with the previous package you can set the maximum line length to a negative value as it takes an integer parameter but the code panics if the value is less than one.

    You can control whether words may be split in the middle by setting the second parameter (called breakWords) to the Wrapper function.

    s := "A string including a very long word,"+
          " 1234567890123456789012345678901234567890"+
          " and some shorter ones"
    wrapperNoBreak := wordwrap.Wrapper(30, false)
    fmt.Println(wrapperNoBreak(s))
    wrapperBreak := wordwrap.Wrapper(30, true)
    fmt.Println(wrapperBreak(s))
    

    The wordwrap package also offers an Indent function that will add a prefix to one or all the lines on the given string. The following will just add the prefix to the first line.

    s := "The quality of mercy is not strained."+
          " It droppeth as the gentle rain from heaven"
    wrapper := wordwrap.Wrapper(30, false)
    fmt.Println(
          wordwrap.Indent(wrapper(s), "Portia (as Balthazar): ", false))
    

    And this will add the prefix to every line.

    s := "Some text to be written to a Go program as a comment."+
          " Yet more comment text."
    wrapper := wordwrap.Wrapper(30, false)
    fmt.Println(
          wordwrap.Indent(wrapper(s), "// ", true))
    

    Note that this package is not in a module and has not been changed for a few years.

  • twrap

    The last package we'll discuss is twrap (for t(ext)wrap); this takes a different approach and offers much more control over how the text is wrapped.

    Like the last package we discussed, to use it you first construct an object and then call wrapping functions on that. It differs from either of the other packages in that it writes the text directly rather than returning a string. You can choose the writer when you create the wrapper, the default is standard out. If you want a string you can pass a bytes.Buffer which satisfies the io.Writer interface and then call the String method on the buffer. Like the first package discussed it will not split words outside of word boundaries.

    s := "The quality of mercy is not strained."+
          " It droppeth as the gentle rain from heaven"
    twc, err := twrap.NewTWConf()
    if err != nil {
          fmt.Println("Error creating the TWConf: ", err)
          os.Exit(1)
    }
    twc.Wrap(s, 30)
    

    This is a bit long-winded because we have the error checking but you can call the NewTWConfOrPanic function instead and that will panic on error instead.

    Other differences to note are that the Wrap function takes an indent parameter rather than a maximum line length parameter. The maximum line length is set once on the TWConf object when you construct it, defaulting to 80 columns. To get the same behaviour as the previous examples we would need to write the following code:

    s := "The quality of mercy is not strained."+
          " It droppeth as the gentle rain from heaven"
    twc := twrap.NewTWConfOrPanic(twrap.SetTargetLineLen(30))
    twc.Wrap(s, 0)
    

    But twrap can do much more than this.

    You can have prefixed text.

    s := "The quality of mercy is not strained."+
          " It droppeth as the gentle rain from heaven"
    twc := twrap.NewTWConfOrPanic()
    twc.WrapPrefixed("Portia (as Balthazar): ", s, 10)
    

    You can have the first line of a block of text with a different indentation than the subsequent lines.

    s := "The quality of mercy is not strained."+
          " It droppeth as the gentle rain from heaven"
    twc := twrap.NewTWConfOrPanic()
    twc.Wrap2Indent(s,
          45, // the first line paragraph indent
          40) // other lines indent
    

    And you can even have the first line of subsequent paragraphs having a different indent from the first line of text. The twrap package splits the supplied text into separate blocks around newline characters, these blocks form paragraphs with their own, potentially different first line indentation.

    s := "The quality of mercy is not strained."+
          " It droppeth as the gentle rain from heaven"
    twc := twrap.NewTWConfOrPanic()
    twc.Wrap3Indent("Label: "+s+"\n"+s+"\n"+s,
          40, // first line indent
          49, // first line paragraph indent
          47) // other line indent
    

    When printing a line twrap will print repeated white space characters which can allow for some simple formatting.

    s := "A simple list:\n"+
          "  first item    some text describing the first item\n"+
          "  second item   more text, this time describing the 2nd item\n"+
          "  third item    the end. No more text"
    twc := twrap.NewTWConfOrPanic()
    twc.Wrap2Indent(s, 40, 50)
    

    You can also use twrap to print lists (you can change the list prefix string when you construct the TWConf).

    list := []string {
    	"the world",
    	"the flesh",
    	"and the devil",
    }
    twc := twrap.NewTWConfOrPanic()
    twc.List(list, 10)
    

    Or indexed lists.

    list := []string {
    	"the world",
    	"the flesh",
    	"and the devil",
    }
    twc := twrap.NewTWConfOrPanic()
    twc.IdxList(list, 10)
    

    Lastly, this package is in a Go module and is actively maintained.

References

Tabulating data

Summary

Tables are a standard way of showing information especially when you have a lot of data to report. So how can you build tables in Go? As is often the case, the Go standard library has something you can use but there are other packages available as well.

I'll discuss two packages, tabwriter from the standard library and the col package. Although they both will print tables they take a very different approach and offer very different interfaces.

Discussion

  • tabwriter

    The tabwriter package provides a filter that you can write to and that will pad its output into columns defined by tabs in the input. Let's look at a quick example.

    lines := []string{
    	"The quality\tof mercy\tis not strained\n",
    	"It droppeth\tas the gentle rain\tfrom heaven\n",
    	"Upon\tthe place\tbeneath\n",
    }
    tw := tabwriter.NewWriter(
    	os.Stdout, //output
    	0,         // min width
    	1,         // tabwidth
    	1,         // padding
    	' ',       // padding character
    	0)         // flags
    for _, l := range lines {
    	tw.Write([]byte(l))
    }
    // tabwriter buffers output so you must call Flush when you finish
    tw.Flush()
    

    The tabwriter package offers some tweaks to its behaviour through the flags and the other parameters you can give. For instance, increasing the padding value gives more space between the columns of text.

  • col

    The col package is more complicated than tabwriter but it can do a lot more. It takes a very different approach.

    There are two core structures and a single interface that col is built around, these are a Report, a Col and a Formatter. You construct the report from columns and a column has a formatter for displaying the data and one or more strings for the column header. You need to specify the column width and the data type of each column. Once you have constructed the Report, you can print the data. Repeatedly call the PrintRow method passing the values to be printed. The type of the values should match the types expected by the formatter for the corresponding column. Let's see an example.

    rpt := col.StdRpt(
    	col.New(colfmt.Int{W:4}, "year"),
    );
    rpt.PrintRow(2019)
    rpt.PrintRow(2020)
    rpt.PrintRow(2021)
    

    This will print this single column of year numbers. Not so exciting but now let's add a column.

    rpt := col.StdRpt(
    	col.New(colfmt.Int{W: 4}, "year"),
    	col.New(colfmt.Int{W: 2}, "number of pupils"),
    );
    rpt.PrintRow(2019, 34)
    rpt.PrintRow(2020, 36)
    rpt.PrintRow(2021, 29)
    

    That looks OK but the number of pupils column title is very wide for the data so let's put it on two rows.

    rpt := col.StdRpt(
    	col.New(colfmt.Int{W: 4}, "year"),
    	col.New(colfmt.Int{W: 2}, "number of","pupils"),
    );
    rpt.PrintRow(2019, 34)
    rpt.PrintRow(2020, 36)
    rpt.PrintRow(2021, 29)
    

    That looks better but now let's break down the number of pupis by gender.

    rpt := col.StdRpt(
    	col.New(colfmt.Int{W: 4}, "year"),
    	col.New(colfmt.Int{W: 2}, "number of pupils", "total"),
    	col.New(colfmt.Int{W: 2}, "number of pupils", "male"),
    	col.New(colfmt.Int{W: 2}, "number of pupils", "female"),
    );
    rpt.PrintRow(2019, 34, 18, 16)
    rpt.PrintRow(2020, 36, 18, 18)
    rpt.PrintRow(2021, 29, 13, 16)
    

    Now we see one of the useful features of col, title spanning. This is where adjacent columns sharing parts of the column name have the shared parts spanned, this leads to a more compact table.

    One last example, demonstrating another Formatter.

    rpt := col.StdRpt(
    	col.New(colfmt.Int{W: 4}, "year"),
    	col.New(colfmt.Int{W: 2}, "number of pupils", "total"),
    	col.New(colfmt.Int{W: 2}, "number of pupils", "male"),
    	col.New(colfmt.Int{W: 2}, "number of pupils", "female"),
    	col.New(colfmt.WrappedString{W: 30}, "class motto"),
    );
    rpt.PrintRow(2019, 34, 18, 16, "Death or Glory")
    rpt.PrintRow(2020, 36, 18, 18, "The quality of mercy is not strained,"+
          " it droppeth as the gentle rain from heaven")
    rpt.PrintRow(2021, 29, 13, 16, "Whatever!")
    

    There are several standard formatters provided but you can write your own provided that it satisfies the Formatter interface.

    This barely scratches the surface of what you can do with the col package. If you are looking for a way to format tables of data to a terminal this is worth investigating.

References

See col and tabwriter

Author: Nick Wells

Created: 2021-06-12 Sat 18:00

Validate