🌡️ The Go Journey – Building a Temperature Converter in Go

🌡️ The Go Journey – Building a Temperature Converter in Go
Photo by Pawel Czerwinski / Unsplash

When you’re learning a new language, small but meaningful projects are gold.
They let you practice real input/output, experiment with syntax, and get instant feedback.

For my first practical Go project, I decided to build a temperature converter — the kind of utility that’s simple enough to finish in one sitting, yet rich enough to explore Go’s fundamentals: command-line args, error handling, string manipulation, and unit tests.


Motivation & Setup

A temperature converter is perfect for Go beginners because it touches everything that matters early on:

  • Real input/output through the command line.
  • Simple arithmetic that’s easy to test.
  • Error handling and formatting that show off Go’s clarity.

You’ll need Go 1.22+ (though any recent version will work).

Run your program directly with:


go run main.go 100K

And when you add tests later:


go test

That’s the beauty of Go — batteries included, no third-party runners required.


⚙️ Parsing Command-Line Arguments

Let’s start with user input.

Go exposes command-line arguments through the built-in os.Args slice.
os.Args[0] is the program name, and the rest are arguments passed in the terminal.

Here’s the skeleton:

package main

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

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage: go run main.go <temperature><C|F>")
        os.Exit(1)
    }

    input := strings.TrimSpace(strings.ToUpper(os.Args[1]))
    unit := input[len(input)-1]
    valueStr := input[:len(input)-1]

    value, err := strconv.ParseFloat(valueStr, 64)
    if err != nil {
        fmt.Println("Invalid temperature value.")
        os.Exit(1)
    }

    switch unit {
    case 'C':
        fmt.Printf("%.2f°C = %.2f°F\n", value, CelciustoFarenheit(value))
    case 'F':
        fmt.Printf("%.2f°F = %.2f°C\n", value, FarenheitToCelcius(value))
    default:
        fmt.Println("Unknown unit. Use C or F.")
        os.Exit(1)
    }
}

A few things are happening here:

  • strings.TrimSpace removes accidental spaces.
  • strings.ToUpper lets you handle both “c” and “C”.
  • unit := input[len(input)-1] grabs the last character.
  • valueStr := input[:len(input)-1] slices out everything before it.

Go strings can be indexed like arrays — len(input)-1 safely gets the last byte (in this case, a single-letter ASCII unit).

And as always in Go, you check errors first, not last.


🔢 Numbers & Formatting

Once we have valueStr, we turn it into a number with strconv.ParseFloat.


value, err := strconv.ParseFloat(valueStr, 64)

Go doesn’t do automatic conversions — that’s intentional.
The compiler forces you to think about types, which saves you from runtime surprises later.

For output, fmt.Printf makes formatting dead simple:


fmt.Printf("%.2f°C = %.2f°F\n", value, CelciusToFarenheit(value))

%.2f keeps your decimals neat, and Go handles Unicode perfectly,
so symbols like °C and °F just work.


Control Flow & Functions

Let’s move conversion logic into small helper functions:


func CelsiusToFahrenheit(c float64) float64 {
	return (c * 9 / 5) + 32
}

func FahrenheitToCelsius(f float64) float64 {
	return (f - 32) * 5 / 9
}

func CelsiusToKelvin(c float64) float64 {
	return c + 273.15
}

func KelvinToCelsius(k float64) float64 {
	return k - 273.15
}

They’re pure functions — no side effects, no global state — so they’re perfect for testing later.

Then, use a switch statement to branch logic based on the unit:


switch unit {
  case 'C':
    ...
  case 'F':
    ...
}

This keeps the code readable and flat — no nested if-else chains.


Testing Culture

Testing is part of Go’s culture — not an afterthought.

Create a file called converter_test.go and write a table-driven test (the Go norm):


func TestCelciustoFarenheit(t *testing.T) {
    tests := []struct {
        c, want float64
    }{
        {0, 32},
        {100, 212},
    }

    for _, tt := range tests {
        t.Run(fmt.Sprintf("%vC", tt.c), func(t *testing.T) {
            got := CtoF(tt.c)
            if got != tt.want {
                t.Errorf("got %.2f, want %.2f", got, tt.want)
            }
        })
    }
}

Here’s what’s cool:

  • You define test cases as anonymous structs.
  • Loop over them — concise and expressive.
  • t.Run gives each case its own name in test output.
  • Each test message is clear (got vs want).

To run them:


go test 

and you'll see result:

PASS
ok      go-temperature  0.764s

That’s the Go way — fast, self-contained, no setup.🧠 Takeaways & Next Steps

In this project, we touched:

  • Go’s packages and imports.
  • Command-line args via os.Args.
  • Strings, runes, and byte slicing.
  • Functions and switch statements.
  • Error handling and early exits.
  • Unit testing with table-driven tests.
  • The Go mindset: explicit, clean, and predictable.

If you want to go further:

  • Add inverse conversions (Kelvin → Fahrenheit).
  • Validate malformed input (like “100x”).
  • Explore Go’s flag package for richer CLI tools.
  • Format output with colors or units aligned in a table.

And above all — keep writing small, testable programs.
Each one will sharpen your understanding of Go’s design philosophy.

The whole repository you can find here