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 — 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#.