โ† Back to DevBytes

Solving Valid Parentheses in Go: Step-by-Step Guide

Introduction to Valid Parentheses

The Valid Parentheses problem is one of the most classic algorithmic challenges you will encounter in coding interviews and competitive programming. The problem statement is deceptively simple: given a string containing only the characters (, ), {, }, [, and ], determine if the input string is valid. A string is considered valid when every opening bracket has a corresponding closing bracket of the same type, and the brackets are closed in the correct order.

In this tutorial, we will walk through solving this problem using the Go programming language. We will cover the underlying data structure, the algorithm, a complete implementation, edge cases, complexity analysis, and best practices to keep in mind when writing production-ready Go code.

Why This Problem Matters

At first glance, validating parentheses might seem like a trivial exercise. However, this problem is foundational because it teaches several critical concepts that appear repeatedly in real-world software engineering:

Mastering this problem gives you a mental model you can apply to more complex parsing and validation tasks.

Understanding the Problem

Before writing any code, let us clearly define what makes a string valid. Consider the following examples:

The key insight is that the most recently opened bracket must be the first one closed. This Last-In-First-Out behavior is exactly what a stack provides.

The Algorithm: Stack-Based Approach

The algorithm can be summarized in a few clear steps:

This approach guarantees that we validate both the matching type and the correct nesting order.

Implementing the Solution in Go

Go does not have a built-in generic stack type in its standard library, but we can easily simulate one using a slice. Slices in Go are dynamic arrays that allow efficient append and pop operations from the end, which is perfect for stack behavior.

Basic Implementation

Here is a clean, readable implementation of the Valid Parentheses solution:

package main

import "fmt"

func isValid(s string) bool {
    // Map closing brackets to their corresponding opening brackets
    matching := map[rune]rune{
        ')': '(',
        '}': '{',
        ']': '[',
    }

    // Use a slice as a stack
    stack := []rune{}

    for _, char := range s {
        // If it is a closing bracket
        if open, isClosing := matching[char]; isClosing {
            // Stack must not be empty and top must match
            if len(stack) == 0 || stack[len(stack)-1] != open {
                return false
            }
            // Pop from the stack
            stack = stack[:len(stack)-1]
        } else {
            // It is an opening bracket, push onto stack
            stack = append(stack, char)
        }
    }

    // Valid only if stack is empty
    return len(stack) == 0
}

func main() {
    testCases := []string{
        "()",
        "()[]{}",
        "(]",
        "([)]",
        "{[]}",
        "",
        "(",
        "((()))",
        "((())",
    }

    for _, tc := range testCases {
        fmt.Printf("isValid(%q) = %v\n", tc, isValid(tc))
    }
}

When you run this program, you should see output confirming which strings are valid and which are not. The use of rune instead of byte ensures that the code handles Unicode characters correctly, even though the problem only involves ASCII brackets.

How the Code Works

Let us trace through the input "{[]}" to understand the flow:

Now consider the invalid input "([)]":

Complexity Analysis

Understanding the performance characteristics of your solution is essential, especially in interview settings.

This is optimal for this problem. You cannot do better than O(n) time because you must examine every character, and you cannot do better than O(n) space in the worst case because you may need to store all opening brackets.

Handling Edge Cases

A robust solution must handle edge cases gracefully. Here are the scenarios to consider:

Our implementation handles all of these correctly. The check len(stack) == 0 before accessing the top element prevents index-out-of-range panics, and the final len(stack) == 0 check catches any unclosed brackets.

Best Practices for Go Implementations

Use a Map for Bracket Pairs

Using a map to associate closing brackets with their opening counterparts makes the code clean and easy to extend. If you ever need to support additional bracket types, such as angle brackets < and >, you simply add another entry to the map.

Prefer Slices Over Custom Stack Types for Simplicity

For a problem this small, building a custom stack struct is overkill. Go slices provide all the operations you need with minimal boilerplate. However, if you find yourself using stacks across multiple files, consider defining a reusable type:

package main

type Stack struct {
    data []rune
}

func (s *Stack) Push(r rune) {
    s.data = append(s.data, r)
}

func (s *Stack) Pop() (rune, bool) {
    if len(s.data) == 0 {
        return 0, false
    }
    top := s.data[len(s.data)-1]
    s.data = s.data[:len(s.data)-1]
    return top, true
}

func (s *Stack) Peek() (rune, bool) {
    if len(s.data) == 0 {
        return 0, false
    }
    return s.data[len(s.data)-1], true
}

func (s *Stack) IsEmpty() bool {
    return len(s.data) == 0
}

func isValidWithStack(s string) bool {
    matching := map[rune]rune{
        ')': '(',
        '}': '{',
        ']': '[',
    }

    stack := &Stack{}

    for _, char := range s {
        if open, isClosing := matching[char]; isClosing {
            top, ok := stack.Peek()
            if !ok || top != open {
                return false
            }
            stack.Pop()
        } else {
            stack.Push(char)
        }
    }

    return stack.IsEmpty()
}

This approach improves readability and reusability when your codebase grows.

Use Runes for Character Iteration

Always iterate over strings using range, which yields rune values, rather than indexing with s[i], which yields byte values. This avoids subtle bugs with multi-byte Unicode characters and is idiomatic Go.

Write Table-Driven Tests

Go's testing conventions favor table-driven tests. Here is an example test file that validates the solution thoroughly:

package main

import "testing"

func TestIsValid(t *testing.T) {
    tests := []struct {
        name     string
        input    string
        expected bool
    }{
        {"simple pair", "()", true},
        {"multiple pairs", "()[]{}", true},
        {"mismatched", "(]", false},
        {"wrong order", "([)]", false},
        {"nested valid", "{[]}", true},
        {"empty string", "", true},
        {"single open", "(", false},
        {"single close", ")", false},
        {"deeply nested", "(((())))", true},
        {"unclosed nested", "((()", false},
        {"extra closing", "())", false},
        {"mixed valid", "({[]})", false},
        {"long valid", "(((((())))))[]{}{}", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := isValid(tt.input)
            if result != tt.expected {
                t.Errorf("isValid(%q) = %v, expected %v",
                    tt.input, result, tt.expected)
            }
        })
    }
}

Run the tests with go test -v to see detailed output for each case. Table-driven tests make it trivial to add new cases as you discover edge cases in production.

Avoid Premature Optimization

It might be tempting to add an early exit check, such as returning false if the string length is odd. While this is a valid optimization, it only helps in specific cases and adds a tiny amount of complexity. In most real-world scenarios, the linear scan is fast enough, and clarity should take priority. Only add such optimizations if profiling shows they are necessary.

Common Mistakes to Avoid

Extending the Solution

Once you understand the basic solution, you can extend it to handle more complex scenarios. For example, you might want to ignore non-bracket characters in the input, effectively validating only the bracket structure of a string that contains other text:

func isValidWithNoise(s string) bool {
    matching := map[rune]rune{
        ')': '(',
        '}': '{',
        ']': '[',
    }

    opening := map[rune]bool{
        '(': true,
        '{': true,
        '[': true,
    }

    stack := []rune{}

    for _, char := range s {
        if opening[char] {
            stack = append(stack, char)
        } else if open, isClosing := matching[char]; isClosing {
            if len(stack) == 0 || stack[len(stack)-1] != open {
                return false
            }
            stack = stack[:len(stack)-1]
        }
        // Ignore all other characters
    }

    return len(stack) == 0
}

This variant is useful when validating brackets in source code snippets or configuration files where other characters are present.

Conclusion

The Valid Parentheses problem is a perfect introduction to stack-based thinking and remains one of the most valuable algorithms to master. By using a Go slice as a stack, a map to define bracket pairs, and careful edge-case handling, you can write a solution that is both efficient and easy to understand. The O(n) time and space complexity is optimal, and the patterns you learn here transfer directly to more advanced parsing problems, compiler design, and expression evaluation. Whether you are preparing for a coding interview or building a real-world validator, the principles covered in this guide will serve as a reliable foundation for your Go programming toolkit.

๐Ÿ›  Tools from DevBytes

Inventory Tracker Pro โ€” Excel inventory system, low-stock alerts ยท $19
AI Dev Kit for Mac โ€” local AI dev environment templates ยท $9.99
KeyMapper for Mac โ€” custom keyboard shortcut toolkit ยท $7.99

โ† Back to all articles