A Go string is essentially a sequence of bytes.
That sounds like a small implementation detail, but once you start dealing with UTF-8 and Unicode, it becomes important to understand what's actually happening.
Strings in Go
A basic Go string looks familiar:
message := "Hello World"
fmt.Println(message)
Strings can use double quotes:
message := "Hello \t World"
Or backticks:
message := `Hello \t World`
And these two behave differently.
With double quotes, escape sequences are interpreted:
fmt.Println("Hello \t World")
The \t becomes a tab.
With backticks, the contents are treated as a raw string literal:
fmt.Println(`Hello \t World`)
Here, \t remains part of the string.
This is useful when you want the contents of a string to be interpreted literally.
A String Is a Sequence of Bytes
Consider:
str := "Hello You!"
We can convert the string to a byte slice:
bytes := []byte(str)
Then print both:
fmt.Printf("%v\n", str)
fmt.Printf("%v\n", bytes)
The output looks like:
Hello You!
[72 101 108 108 111 32 89 111 117 33]
Those numbers represent the individual bytes that make up the string.
For example, the first character:
H
is represented by the byte:
72
So when Go says a string is a sequence of bytes, this isn't just an abstract definition.
We can actually see those bytes.
So Where Does UTF-8 Come In?
Computers ultimately store data as binary.
That means we need a way to represent text as numbers that computers can store.
That's where character encodings come in.
UTF-8 stands for:
Unicode Transformation Format – 8-bit
It provides a way to encode Unicode characters using between 1 and 4 bytes per character.
This allows the same encoding to represent characters from many different writing systems.
For example, simple ASCII characters such as:
A
B
C
can be represented using a single byte.
Other Unicode characters can require multiple bytes.
And this is where the distinction between characters and bytes becomes important.
Bytes Are Not Characters
Consider this:
str := "Hello"
Each character in this particular string can be represented by one byte.
But that doesn't mean every character is one byte.
For example, Unicode contains characters that require multiple bytes when encoded as UTF-8.
So if we're dealing with text outside the ASCII range, we can't simply assume:
1 character = 1 byte
This is one of those details I rarely think about when writing normal Ruby application code.
Go makes the underlying representation much more visible.
Ruby Comparison
Ruby also lets us inspect the bytes of a string.
str = "Hello You!"
str.bytes
This gives us the byte values making up the string.
So the underlying concept isn't completely foreign to Ruby.
The difference is that Go's type system and standard library make this distinction much more explicit.
That mental model becomes particularly useful when working with files, network protocols, encodings, and other low-level operations.
Formatting with fmt
Another part of my notes that I found useful was Go's fmt package.
It provides formatted output similar to Ruby's printf.
For example:
name := "Alice"
age := 30
fmt.Printf("Name: %s, Age: %d\n", name, age)
The % symbols are called format verbs.
They tell fmt how to format the value.
%v — Print the Value
When you're not particularly concerned about the exact formatting, %v is useful.
name := "Alice"
fmt.Printf("%v\n", name)
It prints the value using its default format.
This is one of the formatting verbs I expect to use frequently.
%T — Print the Type
One I particularly like while learning Go is %T.
age := 30
fmt.Printf("%T\n", age)
This prints the type of the value.
For example:
int
Coming from Ruby, this reminds me of:
puts age.class
Both are useful when you're trying to understand what you're actually working with.
Formatting Numbers
Go also provides formatting verbs for different number representations.
For example:
number := 42
fmt.Printf("%d\n", number)
fmt.Printf("%b\n", number)
fmt.Printf("%o\n", number)
fmt.Printf("%x\n", number)
fmt.Printf("%X\n", number)
These represent the number in:
-
%d— decimal -
%b— binary -
%o— octal -
%x— hexadecimal using lowercase letters -
%X— hexadecimal using uppercase letters
So:
42
can be represented as:
101010
in binary.
Characters and Unicode Formatting
Go also provides formatting verbs for characters and Unicode values.
For example:
fmt.Printf("%c\n", 65)
prints:
A
because 65 corresponds to the Unicode code point for A.
There's also %q for quoted character/string representations.
And %#U can be used to display a Unicode code point in Go syntax.
These formatting options become particularly useful when you're debugging text and encoding-related issues.
Go vs Ruby
Here's how some of these concepts compare.
| Concept | Go | Ruby |
|---|---|---|
| String | Sequence of bytes |
String object |
| String literal | "Hello" |
"Hello" |
| Raw string | `Hello` |
%q{Hello} / other forms |
| Inspect bytes | []byte(str) |
str.bytes |
fmt.Println |
puts |
|
| Formatted output | fmt.Printf |
printf |
| Print type | %T |
.class |
| Default formatting | %v |
puts / interpolation |
| Decimal | %d |
printf("%d", n) |
| Binary | %b |
printf("%b", n) |
| Hexadecimal |
%x / %X
|
printf("%x", n) |
| Character | %c |
chr / formatting |
When you're working with files, network communication, binary data, or encoding problems, understanding that you're ultimately dealing with bytes becomes extremely valuable.
And this is another recurring theme in my Go journey:
Ruby often lets me forget what's happening underneath.
Go occasionally makes me look underneath the hood.
And sometimes, that's exactly what I need to become a better developer.