Infix to Postfix in Go

A complete infix to postfix converter in Go, plus a postfix evaluator. Both build clean under go vet and were run before publishing.

The complete Go program

Save this as infix_to_postfix.go. It uses only the standard library, handles multi-character operands, all six operators and nested brackets, and includes a postfix evaluator so you can check a conversion numerically.

infix_to_postfix.go
// infix_to_postfix.go — convert an infix expression to postfix (RPN)
// using Dijkstra's shunting-yard algorithm.
//
// Build and run:
//	go build -o infix_to_postfix infix_to_postfix.go
//	./infix_to_postfix "A + B * C"
package main

import (
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
)

var precedence = map[rune]int{
	'+': 1, '-': 1,
	'*': 2, '/': 2, '%': 2,
	'^': 3,
}

// Only ^ groups right to left; everything else groups left to right.
func isRightAssociative(op rune) bool { return op == '^' }

func isOperandRune(r rune) bool {
	return r == '_' || r == '.' ||
		(r >= '0' && r <= '9') ||
		(r >= 'a' && r <= 'z') ||
		(r >= 'A' && r <= 'Z')
}

// ToPostfix converts an infix expression to its postfix form.
func ToPostfix(expression string) (string, error) {
	var output []string
	var stack []rune

	runes := []rune(expression)
	for i := 0; i < len(runes); i++ {
		ch := runes[i]

		switch {
		case ch == ' ' || ch == '\t' || ch == '\n':
			continue

		// Operands may be several characters: 12, 3.5, count, x1.
		case isOperandRune(ch):
			j := i
			for j < len(runes) && isOperandRune(runes[j]) {
				j++
			}
			output = append(output, string(runes[i:j]))
			i = j - 1

		case ch == '(':
			stack = append(stack, ch)

		case ch == ')':
			for len(stack) > 0 && stack[len(stack)-1] != '(' {
				output = append(output, string(stack[len(stack)-1]))
				stack = stack[:len(stack)-1]
			}
			if len(stack) == 0 {
				return "", errors.New(`unbalanced brackets: missing "("`)
			}
			stack = stack[:len(stack)-1] // discard the "("

		default:
			if _, ok := precedence[ch]; !ok {
				return "", fmt.Errorf("unexpected character %q at position %d", ch, i)
			}
			for len(stack) > 0 {
				top := stack[len(stack)-1]
				if top == '(' {
					break
				}
				higher := precedence[top] > precedence[ch]
				equalLeft := precedence[top] == precedence[ch] && !isRightAssociative(ch)
				if !higher && !equalLeft {
					break
				}
				output = append(output, string(top))
				stack = stack[:len(stack)-1]
			}
			stack = append(stack, ch)
		}
	}

	for len(stack) > 0 {
		top := stack[len(stack)-1]
		if top == '(' {
			return "", errors.New(`unbalanced brackets: missing ")"`)
		}
		output = append(output, string(top))
		stack = stack[:len(stack)-1]
	}

	return strings.Join(output, " "), nil
}

// EvaluatePostfix evaluates a postfix expression whose operands are numbers.
func EvaluatePostfix(postfix string) (float64, error) {
	var stack []float64

	for _, token := range strings.Fields(postfix) {
		r := []rune(token)
		if len(r) == 1 {
			if _, isOp := precedence[r[0]]; isOp {
				if len(stack) < 2 {
					return 0, errors.New("malformed postfix expression")
				}
				b := stack[len(stack)-1]
				a := stack[len(stack)-2]
				stack = stack[:len(stack)-2]

				var value float64
				switch r[0] {
				case '+':
					value = a + b
				case '-':
					value = a - b
				case '*':
					value = a * b
				case '/':
					value = a / b
				case '%':
					value = float64(int64(a) % int64(b))
				case '^':
					value = pow(a, b)
				}
				stack = append(stack, value)
				continue
			}
		}

		number, err := strconv.ParseFloat(token, 64)
		if err != nil {
			return 0, fmt.Errorf("%q is not a number", token)
		}
		stack = append(stack, number)
	}

	if len(stack) != 1 {
		return 0, errors.New("malformed postfix expression")
	}
	return stack[0], nil
}

// pow avoids importing math for one call, and keeps integer exponents exact.
func pow(base, exponent float64) float64 {
	n := int(exponent)
	if float64(n) != exponent || n < 0 {
		result := 1.0
		for i := 0; i < int(exponent); i++ {
			result *= base
		}
		return result
	}
	result := 1.0
	for i := 0; i < n; i++ {
		result *= base
	}
	return result
}

func main() {
	expression := "A + B * C - (D / E)"
	if len(os.Args) > 1 {
		expression = strings.Join(os.Args[1:], " ")
	}

	postfix, err := ToPostfix(expression)
	if err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}

	fmt.Println("Infix:  ", expression)
	fmt.Println("Postfix:", postfix)
}

Build and run it

go build -o infix_to_postfix infix_to_postfix.go
./infix_to_postfix "A + B * C - (D / E)"

Sample output

Infix:   A + B * C - (D / E)
Postfix: A B C * + D E / -

Run it with no arguments and it converts that same default expression, which is a quick way to confirm the build works before trusting it with your own input.

How it works

The algorithm keeps two collections: an output list that only ever grows, and a stack holding operators and left brackets while they wait for their operands. Every token falls into one of four cases.

  • Operand — goes straight to the output; it never touches the stack.
  • Left bracket — pushed, where it acts as a fence nothing pops past.
  • Right bracket — pop to the output until the matching left bracket surfaces, then discard both. Brackets never appear in postfix.
  • Operator — pop while the operator on top binds at least as tightly, then push. That one condition is where precedence and associativity live.

Tokenizing as a separate step is what allows multi-character operands. Walking the string one character at a time would read x1 as two operands and quietly produce the wrong answer.

The stack in Go

Go has no stack type in its standard library, and does not need one: a slice is a stack. append pushes, s[len(s)-1] peeks and s = s[:len(s)-1] pops. Re-slicing does not copy, so this is as cheap as a purpose-built structure.

stack = append(stack, ch)              // push
top := stack[len(stack)-1]             // peek
stack = stack[:len(stack)-1]           // pop

What is idiomatic here

Errors are values

A malformed expression returns an error rather than panicking, so the caller decides what to do. The fmt.Errorf call with %q quotes the offending character, which matters when it is a space or a quote.

Runes, not bytes

The loop walks []rune rather than the raw string. Indexing a Go string yields bytes, so a multi-byte character would be split mid-symbol and reported at the wrong position.

The two rules that decide correctness

Nearly every wrong implementation gets one of these two wrong, and both live in the same loop condition.

Right-associativity of ^

A ^ B ^ C means A ^ (B ^ C), so the postfix form is A B C ^ ^ — not A B ^ C ^. That is why the loop pops on equal precedence only when the incoming operator is not right-associative.

Where % belongs

Modulo shares precedence with * and /, not with ^. Putting it on the wrong level changes the answer for any expression that mixes it with multiplication.

Evaluating the postfix result

Evaluation is the easier half: push operands, and on an operator pop two values, apply, push the result. Order matters — the second value popped is the left operand, which is what makes - and / come out the right way round. Converting and then evaluating 3 + 4 * 2 / (1 - 5) ^ 2 gives 3.5.

FAQ

Why a slice instead of container/list?

A doubly linked list allocates a node per element and scatters them across memory. A slice keeps everything contiguous and amortises growth, which is both faster and far less code for a stack.

Does this handle multi-digit numbers and variable names?

Yes. The tokenizer consumes a whole run of letters, digits, underscores and dots as one operand, so 12.5, count and x1 all convert correctly.

Why is pow written by hand instead of math.Pow?

Only to keep the listing dependency-free and exact for integer exponents. Swapping in math.Pow is a one-line change if you prefer it.

Check your output: paste any expression into the converter to see the same result with the full stack trace, or compare implementations in C · C++ · Python · Java · JavaScript · Rust · C#.