Go byte units and localized formatting
11 minutes
Tip using Ko-fi or Buy Me a Coffee
This article will go through creating byte unit methods for both metric and binary prefixes. The two stringers will take a float64 value and round it with a metric unit.
Decimal(15000).String()
returns15.0 kB
Binary(15000).String()
returns14.6 KiB
Unlike many other byte converters, this will also format the number using international digit separators. While numbers are universal, formatting is not.
For example, 15000.55
can be displayed differently depending on the country or language.
English: 15,000.55
Germany: 15.000,55
France: 15 000,55
Even if your program is primarily in English, it could be nice to allow users to localize numbers and byte units.
You can find the final result on GitHub or run it at Go Playground.
Localized separators
First, create a new global float64
named Decimal and a String
method to convert any float64
values to a string.
package main
type Decimal float64
func (n Decimal) String() string {
return ""
}
Set up the main function with a placeholder number to print.
func main() {
s := Decimal(1000).String()
fmt.Println(s)
}
Running this is pointless as it will return a blank newline.
To make this string method useful, let’s apply language support using the Go text extended libraries. First, import the language package and create a t
language.Tag parameter for String()
.
import (
"fmt"
"golang.org/x/text/language"
)
func (n Decimal) String(t language.Tag) string {
return ""
}
Add a new p
message printer that formats our float64
value to the supplied language.
import (
"fmt"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
func (n Decimal) String(t language.Tag) string {
p := message.NewPrinter(t)
// round the n float to 2 precision points
return p.Sprintf("%.2f", n)
}
Finally, update the main function to use the t
language Tag parameters, using English, German and French.
func main() {
const n = 1000.01
en := Decimal(n).String(language.English)
de := Decimal(n).String(language.German)
fr := Decimal(n).String(language.French)
fmt.Println(en)
fmt.Println(de)
fmt.Println(fr)
}
The result so far can be run in Go.
1package main
2
3import (
4 "fmt"
5
6 "golang.org/x/text/language"
7 "golang.org/x/text/message"
8)
9
10type Decimal float64
11
12func (n Decimal) String(t language.Tag) string {
13 p := message.NewPrinter(t)
14 // round the n float to 2 precision points
15 return p.Sprintf("%.2f", n)
16}
17
18func main() {
19 const n = 1000.01
20 en := Decimal(n).String(language.English)
21 de := Decimal(n).String(language.German)
22 fr := Decimal(n).String(language.French)
23 fmt.Println(en)
24 fmt.Println(de)
25 fmt.Println(fr)
26}
$ go run .
1,000.01
1.000,01
1 000,01
Decimal units
Below the Decimal
type, create a new kb const with a Decimal float64
type and a value of 1000
.
type Decimal float64
const kb Decimal = 1000
Back to the String method, we will implement the kB
unit. Create an x
variable for switch case usage and a copy of the n
Decimal float to the f
variable. Finally, update the return to use the f
variable.
1func (n Decimal) String(t language.Tag) string {
2 p := message.NewPrinter(t)
3 f := n
4 x := n
5 switch {}
6 // round the n float to 2 precision points
7 return p.Sprintf("%.2f", f)
8}
Update the switch to handle any x
value greater or equal to the kb
const and move the p
message printer to the default case.
switch {
case x >= kb:
// do nothing
default:
// round the n float to 2 precision points
return p.Sprintf("%.2f", f)
}
In the switch case, divide the f
Decimal value by the kb
const and return the result. The Sprintf
method also should return the kilobyte unit symbol.
f /= kb
is the same as f = f / kb
.
1switch {
2 case x >= kb:
3 f /= kb
4 // round the n float to 1 precision point
5 return p.Sprintf("%.1f %s", f, "kB")
6 default:
7 // round the n float to 2 precision points
8 return p.Sprintf("%.2f", f)
9}
We need to remove the two precision points from the default case as it returns the value as bytes, which display as integers, not precision floats.
default:
return p.Sprintf("%.0f", f)
The result so far should convert any byte value 1000
or greater into Kilobytes
with one precision point. All other values get kept as bytes.
package main
import (
"fmt"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
type Decimal float64
const kb Decimal = 1000
func (n Decimal) String(t language.Tag) string {
p := message.NewPrinter(t)
f := n
x := n
switch {
case x >= kb:
f /= kb
// round the n float to 1 precision point
return p.Sprintf("%.1f %s", f, "kB")
default:
return p.Sprintf("%.0f", f)
}
}
func main() {
const n = 1000.01
en := Decimal(n).String(language.English)
de := Decimal(n).String(language.German)
fr := Decimal(n).String(language.French)
fmt.Println(en)
fmt.Println(de)
fmt.Println(fr)
fmt.Println(Decimal(123).String(language.Arabic))
}
$ go run .
1.0 kB
1,0 kB
1,0 kB
١٢٣
You can play around changing the n
const value. But the switch logic has a problem. Whenever the n
value is negative, the String
method will display it as bytes.
const n = -1000.01
$ go run .
-1,000
To negate this, update the x
var to save the absolute value of n
.
1import (
2 "fmt"
3 "math"
4)
5
6func (n Decimal) String(t language.Tag) string {
7 p := message.NewPrinter(t)
8 f := n
9 x := Decimal(math.Abs(float64(n)))
$ go run .
-1.0 kB
Let’s add support for Megabytes
. Create a new mb Decimal
const with a value of one million.
1const (
2 kb Decimal = 1_000
3 mb Decimal = 1_000_000
4)
Add a new case in the switch above the existing kb case to handle n
values greater or equal to mb
. All units that are equal or larger than a Megabyte
will use all caps symbols and two precision points.
1switch {
2case x >= mb:
3 f /= mb
4 // round the n float to 2 precision points
5 return p.Sprintf("%.2f %s", f, "MB")
6case x >= kb:
7 f /= kb
8 // round the n float to 1 precision point
9 return p.Sprintf("%.1f %s", f, "kB")
10default:
11 return p.Sprintf("%.0f", f)
12}
To test the new case, change the n
value in main to a byte value larger than a million.
func main() {
const n = 9_876_543
$ go run .
9.88 MB
Now we will finish up the method to support units up to Exabytes (EB). In the Decimal
const collection, add variables gb
, tb
, pb
, and eb
. Instead of using super long integer values, use the more manageable scientific notation.
const (
kb Decimal = 1e+3
mb Decimal = 1e+6
gb Decimal = 1e+9
tb Decimal = 1e+12
pb Decimal = 1e+15
eb Decimal = 1e+18
)
If these floats seem cryptic, consider the values as the digit 1
with x number of zeros.
kB = 1e+3
is a 1
combined with three zeros to make 1000
.
MB = 1e+6
is 1
combined with six zeros to make 1000000
.
Create three constants to hold the various floating-point formats with none, one, and two precision points.
const (
precision0 = "%.0f"
precision1 = "%.1f\u00A0%s"
precision2 = "%.2f\u00A0%s"
)
%f
prints the float value.
%s
prints the unit symbols (kB, MB, etc.).
\u00A0
is Unicode codepoint 160
, no-break space to ensure the unit symbol is always with the float value.
The const precision0
is for byte values, so there is no need for a unit symbol.
In the switch, replace the p.Sprintf
formats with the precision consts.
switch {
case x >= mb:
f /= mb
return p.Sprintf(precision2, f, "MB")
case x >= kb:
f /= kb
return p.Sprintf(precision1, f, "kB")
default:
return p.Sprintf(precision0, f)
}
Complete the switch with cases to support EB, PB, TB, and GB units in descending order.
switch {
case x >= eb:
f /= eb
return p.Sprintf(precision2, f, "EB")
case x >= pb:
f /= pb
return p.Sprintf(precision2, f, "PB")
case x >= tb:
f /= tb
return p.Sprintf(precision2, f, "TB")
case x >= gb:
f /= gb
return p.Sprintf(precision2, f, "GB")
case x >= mb:
f /= mb
return p.Sprintf(precision2, f, "MB")
case x >= kb:
f /= kb
return p.Sprintf(precision1, f, "kB")
default:
return p.Sprintf(precision0, f)
}
Try out some silly large n
byte values and run the results.
func main() {
const n = 92233720368547758079223
en := language.English
de := language.German
fr := language.French
fmt.Println("English", Decimal(n).String(en))
fmt.Println("German", Decimal(n).String(de))
fmt.Println("French", Decimal(n).String(fr))
}
$ go run .
English 92,233.72 EB
German 92.233,72 EB
French 92 233,72 EB
Binary units
Creating the Binary method will be much quicker as it’s very similar to the Decimal
method. The main difference is while the Decimal
uses Base-10 metrics (KB = 1000), the Binary
uses Base-2 metrics (KiB = 1024).
Create a new Binary
type and constants for the binary bytes. These will rely on the left shift operator.
type (
Decimal float64
Binary float64
)
const (
kib Binary = 1 << 10
mib Binary = 1 << 20
gib Binary = 1 << 30
tib Binary = 1 << 40
pib Binary = 1 << 50
eib Binary = 1 << 60
)
If these are confusing, left-shifting implements powers of 2.
1 << 1
equates to2
1 << 2
equates to2 _ 2 = 4
1 << 3
equates to2 _ 2 _ 2 = 8
1 << 10
equates to2 _ 2 _ 2 _ 2 _ 2 _ 2 _ 2 _ 2 _ 2 _ 2 = 1,024
1 << 20
equates to1024 _ 1024 = 1,048,576
1 << 30
equates to1024 _ 1024 \* 1024 = 1,073,741,824
Create a duplicate of the Decimal
String
method but replace the Decimal
with Binary
types.
1func (n Binary) String(t language.Tag) string {
2 p := message.NewPrinter(t)
3 f := n
4 x := Binary(math.Abs(float64(n)))
5 switch {
6 case x >= eb:
7 f /= eb
8 return p.Sprintf(precision2, f, "EB")
9 case x >= pb:
10 f /= pb
11 return p.Sprintf(precision2, f, "PB")
12 case x >= tb:
13 f /= tb
14 return p.Sprintf(precision2, f, "TB")
15 case x >= gb:
16 f /= gb
17 return p.Sprintf(precision2, f, "GB")
18 case x >= mb:
19 f /= mb
20 return p.Sprintf(precision2, f, "MB")
21 case x >= kb:
22 f /= kb
23 return p.Sprintf(precision1, f, "kB")
24 default:
25 return p.Sprintf(precision0, f)
26 }
27}
Finally, update the switch cases to use the binary constants and return binary units.
1func (n Binary) String(t language.Tag) string {
2 p := message.NewPrinter(t)
3 f := n
4 x := Binary(math.Abs(float64(n)))
5 switch {
6 case x >= eib:
7 f /= eib
8 return p.Sprintf(precision2, f, "EiB")
9 case x >= pib:
10 f /= pib
11 return p.Sprintf(precision2, f, "PiB")
12 case x >= tib:
13 f /= tib
14 return p.Sprintf(precision2, f, "TiB")
15 case x >= gib:
16 f /= gib
17 return p.Sprintf(precision2, f, "GiB")
18 case x >= mib:
19 f /= mib
20 return p.Sprintf(precision2, f, "MiB")
21 case x >= kib:
22 f /= kib
23 return p.Sprintf(precision1, f, "KiB")
24 default:
25 return p.Sprintf(precision0, f)
26 }
27}
Let’s wrap it up and test it out by adding some Binary
methods in the main func.
1package main
2
3import (
4 "fmt"
5 "math"
6
7 "golang.org/x/text/language"
8 "golang.org/x/text/message"
9)
10
11type (
12 Decimal float64
13 Binary float64
14)
15
16const (
17 kb Decimal = 1e+03
18 mb Decimal = 1e+06
19 gb Decimal = 1e+09
20 tb Decimal = 1e+12
21 pb Decimal = 1e+15
22 eb Decimal = 1e+18
23)
24
25const (
26 kib Binary = 1 << 10
27 mib Binary = 1 << 20
28 gib Binary = 1 << 30
29 tib Binary = 1 << 40
30 pib Binary = 1 << 50
31 eib Binary = 1 << 60
32)
33
34const (
35 precision0 = "%.0f"
36 precision1 = "%.1f\u00A0%s"
37 precision2 = "%.2f\u00A0%s"
38)
39
40func main() {
41 const n = 10000000000
42 en := language.English
43 de := language.German
44 fr := language.French
45 fmt.Println("English", Decimal(n).String(en))
46 fmt.Println("English", Binary(n).String(en))
47 fmt.Println("German", Decimal(n).String(de))
48 fmt.Println("German", Binary(n).String(de))
49 fmt.Println("French", Decimal(n).String(fr))
50 fmt.Println("French", Binary(n).String(fr))
51}
52
53func (n Decimal) String(t language.Tag) string {
54 p := message.NewPrinter(t)
55 f := n
56 x := Decimal(math.Abs(float64(n)))
57 switch {
58 case x >= eb:
59 f /= eb
60 return p.Sprintf(precision2, f, "EB")
61 case x >= pb:
62 f /= pb
63 return p.Sprintf(precision2, f, "PB")
64 case x >= tb:
65 f /= tb
66 return p.Sprintf(precision2, f, "TB")
67 case x >= gb:
68 f /= gb
69 return p.Sprintf(precision2, f, "GB")
70 case x >= mb:
71 f /= mb
72 return p.Sprintf(precision2, f, "MB")
73 case x >= kb:
74 f /= kb
75 return p.Sprintf(precision1, f, "kB")
76 default:
77 return p.Sprintf(precision0, f)
78 }
79}
80
81func (n Binary) String(t language.Tag) string {
82 p := message.NewPrinter(t)
83 f := n
84 x := Binary(math.Abs(float64(n)))
85 switch {
86 case x >= eib:
87 f /= eib
88 return p.Sprintf(precision2, f, "EiB")
89 case x >= pib:
90 f /= pib
91 return p.Sprintf(precision2, f, "PiB")
92 case x >= tib:
93 f /= tib
94 return p.Sprintf(precision2, f, "TiB")
95 case x >= gib:
96 f /= gib
97 return p.Sprintf(precision2, f, "GiB")
98 case x >= mib:
99 f /= mib
100 return p.Sprintf(precision2, f, "MiB")
101 case x >= kib:
102 f /= kib
103 return p.Sprintf(precision1, f, "KiB")
104 default:
105 return p.Sprintf(precision0, f)
106 }
107}
$ go run .
English 10.00 GB
English 9.31 GiB
German 10,00 GB
German 9,31 GiB
French 10,00 GB
French 9,31 GiB
Congratulations, you now have working Binary
and Decimal
methods. I’ve also written a collection of unit tests for both. You can find the code on GitHub or run it at Go Playground.

Written by Ben Garrett