# LeetCode #345 in Go: reverse vowels of a string, and how strings, bytes, and runes work in Go


## Why I started this series

A while back, I went through a job interview that included a coding challenge. It looked pretty easy, so I finished in like 90 seconds, and all tests passed! Then, the (non-technical) interviewer said to me:

*   Interviewer: Why did you choose Golang? Do you actually know how to code in Golang?
    
*   Me: I said, yes, that challenge was easy, and all tests passed!.
    
*   Interviewer: Well, you only got 43% on that challenge.
    

I'm going to be honest with you.

I've been writing about Go for a while: Lambda functions, Kubernetes tooling, HTTP middleware, client-go. But there's a difference between *using* Go and actually *understanding* Go.

I decided to fix that by doing LeetCode challenges. Not to grind interview prep. Not to get a FAANG job. Just to force myself to use the language in ways I wouldn't naturally reach for on a normal workday as a Senior SRE.

And something unexpected happened: every challenge I struggled with exposed a Go concept I hadn't fully understood. This is the first one.

* * *

## The challenge

### LeetCode #345

> https://leetcode.com/problems/reverse-vowels-of-a-string/description/

The problem requires us to reverse the vowels in a string. Note that letter case matters.

```plaintext
Input: "IceCreAm"

Output: "AceCreIm"
```

* * *

## Solution Approach

The solution approach for this problem is to use 2 pointers, the two-pointer collision approach, to continuously swap the elements at the beginning and end.

> The solution approach was extrated from this \[amazing book\](http:// https://books.halfrost.com/leetcode/en/ ), please consider reading this book if you want to improve your Golang coding skills like me.

### The core idea: two pointers moving toward each other

First, let's try to understand how the solution approach works. Imagine the string as a row of people standing in a line, numbered from left to right. You place one finger at the very first person (index `left = 0`) and another finger at the very last person (index `right = len - 1`). Then you move both fingers toward the middle, one step at a time, until they meet or cross.

This is different from two pointers moving in the *same* direction (like for finding duplicates in a sorted array). Here they move toward each other, which is why it's called the "collision" pattern; they're on a collision course toward the center.

### Why this pattern fits "reverse vowels"

Think about what "reversing only the vowels" really means: the vowels that appear in the string should end up in reverse order, but every consonant should stay exactly where it is.

The trick is: you don't need to know the positions of all the vowels ahead of time. You can discover them as you scan from both ends, because:

*   The **leftmost** vowel needs to be swapped with the **rightmost** vowel.
    
*   Then the **second leftmost** vowel needs to be swapped with the **second rightmost** vowel.
    
*   And so on, working inward.
    

That's exactly what the collision pointers give you for free, if you use them correctly.

### Step-by-step logic

1.  Set `left` at the start of the string, `right` at the end.
    
2.  **Move** `left` **forward** until it lands on a vowel (skip consonants).
    
3.  **Move** `right` **backward** until it lands on a vowel (skip consonants).
    
4.  Once both `left` and `right` are sitting on vowels, **swap them**.
    
5.  Move `left` one step further right, and `right` one step further left (since those two are now "done").
    
6.  Repeat until `left >= right` (the pointers meet or cross).
    

Notice the elegance: consonants are never touched or moved; the pointers just glide past them. Only vowels get swapped, and because you're always taking the current outermost unprocessed vowel from each side, they naturally end up reversed relative to each other.

### Walking through an example

Let's use `"leetcode"`.

```plaintext
l  e  e  t  c  o  d  e
0  1  2  3  4  5  6  7
```

*   `left = 0` → `'l'` is not a vowel → move `left` to 1.
    
*   Index 1 → `'e'` is a vowel → stop `left` here.
    
*   `right = 7` → `'e'` is a vowel → stop `right` here.
    
*   Both are vowels → swap index 1 and index 7. Since both are `'e'`, string looks unchanged, but conceptually a swap happened.
    
*   Move `left` to 2, `right` to 6.
    
*   Index 2 → `'e'` is a vowel → stop `left`.
    
*   Index 6 → `'d'` is not a vowel → move `right` to 5.
    
*   Index 5 → `'o'` is a vowel → stop `right`.
    
*   Swap index 2 (`'e'`) and index 5 (`'o'`) → now index 2 is `'o'`, index 5 is `'e'`.
    
*   Move `left` to 3, `right` to 4. Now `left (3) < right (4)` still true, but index 3 is `'t'` (not vowel) → move `left` to 4. Now `left == right`, loop stops.
    

Result: `"leotcede"`. That's the correct answer; the vowels `e, e, o, e` have been reversed to `e, o, e, e` while consonants `l, t, c, d` stayed in place.

### A second, trickier example: mixed case

Try `"Aa"`, both are vowels (`'A'` and `'a'`, the problem says vowels can be uppercase or lowercase). `left=0` is `'A'`, `right=1` is `'a'`, both vowels immediately, so you swap. Result: `"aA"`. This tests that your vowel-check needs to handle **both cases**, not just lowercase.

* * *

## Checklist for how you'd think about implementing it

When you're ready to write this yourself, you'll want to think through:

1.  **How do I quickly check whether a character is a vowel?:** Hint: a small lookup set/map is cleaner than a long chain of `if` statements comparing against `a, e, i, o, u, A, E, I, O, U`.
    
2.  **Can I swap characters directly in a Go** `string`**?:** This is an important Go-specific gotcha: strings in Go are **immutable**, so you cannot do `s[i], s[j] = s[j], s[i]` directly on a `string`. You'll need to convert it into something mutable first.
    
3.  `[]byte` **vs** `[]rune`: Since this problem only deals with ASCII vowels, a `[]byte` conversion works fine and is efficient. (If the problem involved non-ASCII/multi-byte characters, you'd need `[]rune` instead.)
    
4.  **Loop condition**: When do you stop? (`left < right`)
    
5.  **Two separate inner loops or one combined check?**: You need to advance `left` past consonants and advance `right` past consonants, independently, before doing the swap.
    

Let's dig into the Go-specific mechanics that matter for this problem.

### Why Go strings are immutable

In Go, a `string` is internally a small struct; conceptually, it looks like this:

```go
type stringHeader struct {
    data uintptr // pointer to the underlying byte array
    len  int     // length in bytes
}
```

It's a **read-only view** into a sequence of bytes. The language spec deliberately does not let you write to that underlying array through a string variable. So this:

```go
s := "leetcode"
s[0] = 'L' // compile error!
```

...will not even compile. Go gives you: `cannot assign to s[0] (neither addressable nor a map index expression)`.

This immutability exists for good reasons; it lets multiple strings safely **share the same underlying byte array** (e.g., substring operations like `s[2:5]` don't copy memory; they just create a new header pointing into the same data). If strings were mutable, that sharing would be dangerous; modifying one substring could silently corrupt another.

### How to get mutability: convert to `[]byte`

Since you *can* mutate a slice, the standard trick is to convert the string into a `[]byte`:

```go
b := []byte(s)
```

This conversion **copies** the string's bytes into a brand-new, independent slice. Now you're free to do:

```go
b[0] = 'L'
```

...because `b` is a slice, a mutable, addressable view over its own memory. When you're done modifying, you convert back:

```go
result := string(b)
```

That conversion also **copies** the bytes back into a new immutable string. So the full round trip (`string` → `[]byte` → `string`) does involve two copies total, but for a single-pass algorithm like this one, that's totally acceptable and is the idiomatic way to do in-place-style string manipulation in Go.

### `[]byte` vs `[]rune`: why it matters

This is one of the most important Go concepts for string problems, so let's be precise about it.

*   A `byte` is Go's alias for `uint8`, one 8-bit unit. `[]byte(s)` gives you the **raw UTF-8 encoded bytes** of the string.
    
*   A `rune` is Go's alias for `int32`, it represents a single **Unicode code point**, which might be encoded using 1 to 4 bytes in UTF-8.
    

For plain ASCII characters, like the letters `a-z`, `A-Z` that this problem deals with, **each character is exactly one byte**. So `[]byte` gives you a clean one-index-per-character view, which is exactly what the two-pointer approach needs (`b[left]` and `b[right]` each represent one whole letter).

If the string contained non-ASCII characters (like `é`, `日`, or emoji), a single character could span multiple bytes, and indexing into `[]byte` at arbitrary positions could land you in the *middle* of a multi-byte character, corrupting it. In that scenario, you'd need `[]rune(s)` instead, which decodes the UTF-8 bytes into a slice where each element is one full code point, safe to index individually, at the cost of an extra decoding pass.

For this exercise, the constraints guarantee the input consists only of printable ASCII characters, so `[]byte` is the correct and more efficient choice. Good habit to build: **always check the problem's constraints for character set before deciding between** `[]byte` **and** `[]rune`**.**

### Checking "is this a vowel" idiomatically

A few ways this is commonly done in Go, from least to most idiomatic for this case:

**Option A: long boolean chain (works, but verbose and easy to mistype):**

```go
c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' || c == 'A' || ...
```

**Option B: a map used as a set:**

```go
vowels := map[byte]bool{
    'a': true, 'e': true, 'i': true, 'o': true, 'u': true,
    'A': true, 'E': true, 'I': true, 'O': true, 'U': true,
}
// then: vowels[c]
```

This is very readable. Map lookups are O(1) on average, so it doesn't hurt performance meaningfully for such a small, fixed set.

**Option C:** `strings.ContainsRune`**:**

```go
strings.ContainsRune("aeiouAEIOU", rune(c))
```

Short and clean, though technically does a linear scan over 10 characters each time; negligible here, but worth knowing it's not a true O(1) lookup like the map.

For a problem like this with a tiny, fixed alphabet, either B or C is considered clean, idiomatic Go. The map is generally preferred when you want to signal "this is a set membership check" clearly and efficiently.

## Bringing it together conceptually (still no full code)

So your mental model in Go terms becomes:

1.  Convert `s` to `b := []byte(s)` to get a mutable working copy.
    
2.  Run your `left`/`right` collision pointers over `b`, using your vowel-check function/map on `b[left]` and `b[right]`.
    
3.  When both point to vowels, do a byte swap: `b[left], b[right] = b[right], b[left]`; this works fine because `b` is a slice, and Go allows this kind of tuple-assignment swap directly, no temp variable needed.
    
4.  After the loop finishes, convert back: `string(b)`.
    

* * *

## A quick introduction to Big-O notation, and why this matter

Good, let's tackle Big-O intuition first since it'll come up constantly in LeetCode, according to wikipedia:

> Big O notation is a mathematical framework used in computer science to describe how an algorithm's execution time or memory usage scales as the input size grows.

### What "O(1)" actually means

Big-O notation describes how the **cost of an operation grows as the input size grows**. The letter usually used for input size is `n`.

*   **O(n)** means: if you double the input size, the work roughly doubles too. A linear scan through an array is O(n), checking every element once.
    
*   **O(1)** means: the amount of work stays **constant, no matter how big the input is**. It doesn't mean "one operation" literally, it means "a fixed, bounded number of operations that doesn't depend on `n`."
    

Think of it like this analogy: imagine you have a giant phone book with a million names.

*   **O(n) approach**: You start at page 1 and flip through every single page looking for "Smith." If the book had 10 names, that'd be fast. With a million names, it's painfully slow; the work scales with the size of the book.
    
*   **O(1) approach**: Instead, imagine you have a magic index card that instantly tells you "Smith is on page 4213", you jump straight there. Whether the book has 10 names or 10 million, that lookup takes the same tiny amount of time.
    

### Why map lookups are O(1)

A Go `map` is implemented as a **hash table**. When you do `vowels[c]`, Go doesn't loop through every key checking for a match. Instead:

1.  It runs the key (`c`) through a **hash function**; a formula that converts the key into a number very quickly.
    
2.  That number tells it almost directly which "bucket" in memory to look in.
    
3.  It checks that bucket (usually just one item, sometimes a couple if there's a hash collision) and returns the result.
    

This process takes roughly the same amount of time whether your map has 5 entries or 5 million, that's why it's called O(1) **on average** (technically "amortized average case," since hash collisions can rarely cause it to degrade, but for practical purposes, treat it as constant time).

Compare that to `strings.ContainsRune("aeiouAEIOU", rune(c))`; internally, this **does** loop through the characters of that 10-character string comparing one by one until it finds a match or exhausts the string. It's technically O(k) where k is the length of that fixed string (10). Since 10 never changes, people often casually call it "O(1) too" because it's a *bounded constant*; but it's doing more actual comparisons than a map lookup, just not enough to matter for a 10-character set.

**The key exam-style distinction to remember:** O(1) doesn't mean "instant" or "zero cost"; it means "cost that doesn't grow with your input size `n`."

* * *

## Now, the tuple swap

Go has a really elegant built-in feature for swapping: **multiple assignment**. When you write:

```go
b[left], b[right] = b[right], b[left]
```

Here's what happens under the hood, step by step:

1.  Go first evaluates the **entire right-hand side** of the assignment, in order, *before* assigning anything. So it reads the current value of `b[right]` and the current value of `b[left]`, and holds both of them temporarily (essentially in a tuple, like `(valueOfRight, valueOfLeft)`).
    
2.  Then it assigns those held values to the left-hand side, in order: `b[left]` gets the first held value (`old b[right]`), and `b[right]` gets the second held value (`old b[left]`).
    

Because Go evaluates the whole right-hand side *first*, there's no risk of accidentally overwriting `b[left]` before you've read its original value to place into `b[right]`. This is exactly why you **don't need a temporary variable**, unlike languages such as C, where a swap traditionally requires:

```c
temp = b[left];
b[left] = b[right];
b[right] = temp;
```

In Go, the language does that temporary-holding for you internally, and lets you express it in one clean line.

> **Remember:** this works because slice indexing (`b[left]`) is *addressable*, meaning you can actually assign to that memory location. Recall this is exactly why we converted our `string` to a `[]byte` earlier; string indexing (`s[left]`) is **not addressable**, so `s[left], s[right] = s[right], s[left]` would fail to compile, but `b[left], b[right] = b[right], b[left]` works perfectly because `b` is a slice.

* * *

## Writing the full solution

You now have all the conceptual pieces: two-pointer collision logic, why/how to convert `string` ↔ `[]byte`, `[]byte` vs `[]rune`, O(1) vowel checks, and how Go's swap syntax works.

```go
func reverseVowels(s string) string {
    vowels := map[byte]bool{
        'a': true, 'e': true, 'i': true, 'o': true, 'u': true,
        'A': true, 'E': true, 'I': true, 'O': true, 'U': true, 
    }
    
    b := []byte(s)
    left, right := 0, len(b) - 1

    for left < right {
        if !vowels[b[left]] {
            left++
            continue
        }
        if !vowels[b[right]] {
            right--
            continue
        }
        b[left], b[right] = b[right], b[left]
        left++
        right--

    }
    return string(b)
    
}
```

* * *

## Summary

Three things worth keeping from this problem:

*   **Two-pointer collision** is the pattern to reach for whenever a problem wants you to work from both ends of a sequence toward the middle, reversing, palindrome checks, and partitioning problems all lean on this same idea.
    
*   **Go strings are immutable.** Any in-place-style string manipulation goes through a `string` → `[]byte` → `string` round trip.
    
*   **A map is your O(1) toolbox for set membership**, and Go's tuple assignment (`a, b = b, a`) gives you a clean swap for free, as long as both sides are addressable.
    

This problem is small, but it's a solid vehicle for internalizing patterns that show up constantly in string and array problems on LeetCode.

* * *

## **Let's connect!**

One of the best parts of writing in public is the people you meet along the way, engineers at different stages of their journey, working on similar problems from completely different angles.

If something in this post resonated, if you spotted a bug, or if you just want to talk Go, Kubernetes, Platform Engineering, DevOps, or whatever, I'm always happy to hear from you.

*   [**LinkedIn**](https://www.linkedin.com/in/ferrios/)
    
*   [**GitHub**](https://github.com/FerRiosCosta)
    
*   [**Twitter / X**](https://x.com/ferztyle)
    

*Building from Asunción, Paraguay 🇵🇾*
