Back to Blog

13 Common Beginner Programming Mistakes (and How to Fix Them)

The 13 most common beginner programming mistakes, from skipping error messages to never writing a test, each with a short Go example and the fix.

13 Common Beginner Programming Mistakes (and How to Fix Them)

The most common beginner programming mistakes have little to do with syntax. New programmers skip the error message, write fifty lines before running any of them, ignore the case where input is bad, and guess at bugs instead of looking at the values. These are habits, and habits can be fixed early.

This post covers 13 of them. Each one has a short Go example where code helps, and the fix. Go makes a good teaching language here because its compiler and tools refuse several of these mistakes outright. The section near the end covers those.

TL;DR

  • Read the whole error message. It tells you the file, the line and what went wrong.
  • Run your code every few lines. Small steps make bugs easy to find.
  • Don't paste code, including AI output, that you can't explain line by line.
  • Handle the error path. In Go, check every err before you use the value that came with it.
  • Name things for what they hold, keep functions small, and test the empty and boundary cases.
  • Use Git from day one, keep secrets in environment variables, and write a test before you think you need one.
  • Debug by printing values or stepping through with Delve, not by changing code at random.
  • Make it work before you make it abstract or fast. Then build something that isn't a tutorial.

Table of Contents

1. Not reading the error message

Beginners often see red text and go straight back to the code to change something. The error message is the most useful thing on the screen. It usually names the file, the line, the column and what went wrong.

Here is a small server config loader that doesn't compile:

example.gogo
package main

import (
	"fmt"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	fmt.Println("starting server")
}
example.texttext
./main.go:9:2: declared and not used: port

Read it left to right. main.go is the file, 9 is the line, 2 is the column, and the rest says the variable port is never used. Either use it or delete it. Nothing else in the program is wrong.

Runtime errors work the same way. When a Go program panics, the stack trace lists the function and line where it happened, with the most recent call first. Find the first line that points to your own file and start there.

If error messages feel cryptic, that isn't only you. A 2019 ITiCSE working group that reviewed the research concluded that compiler error messages "present substantial difficulty" for novices (Becker et al., 2019). Reading them slowly and literally is a skill, and it improves quickly with practice.

2. Writing a lot of code before running any of it

Writing a whole program and then running it for the first time feels efficient, but it makes bugs expensive to find. When 80 new lines fail, the bug could be anywhere in them. When 5 new lines fail, it's in those 5.

The fix is a short loop. Write a few lines, run them, read the output, repeat. Print intermediate values while you build, and delete the prints once the piece works. Go compiles fast enough that go run . after every small change costs you a second or two.

3. Copy-pasting code you can't explain

Copying from Stack Overflow or an AI assistant is fine. The mistake is pasting code you can't explain. When it breaks, and it will, you have no idea where to start. You also skip the part where you learn.

Set yourself a rule. Before pasting, read every line and say what it does. If you can't, ask the AI to explain that line, or rewrite it yourself from the explanation. The rule matters more now that AI is part of how most people learn. Among people learning to code, 73.3% use or plan to use AI tools, and 39.5% use them every day (Stack Overflow Developer Survey, 2025). The full argument is in should you still write code by hand.

4. Only coding the happy path

Beginner code tends to assume the file exists, the network is up and the user typed a number. Real input is messier. This program reads a port number and ignores the error:

example.gogo
package main

import (
	"fmt"
	"strconv"
)

func main() {
	input := "80a0"
	port, _ := strconv.Atoi(input)
	fmt.Println("listening on port", port)
}
example.texttext
listening on port 0

The typo turned into port 0 with no warning. Go returns errors as ordinary values, so the fix is to check them and say what failed:

example.gogo
package main

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

func parsePort(raw string) (int, error) {
	port, err := strconv.Atoi(raw)
	if err != nil {
		return 0, fmt.Errorf("invalid port %q: %w", raw, err)
	}
	if port < 1 || port > 65535 {
		return 0, fmt.Errorf("port %d out of range 1-65535", port)
	}
	return port, nil
}

func main() {
	port, err := parsePort("80a0")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Println("listening on port", port)
}
example.texttext
invalid port "80a0": strconv.Atoi: parsing "80a0": invalid syntax
exit status 1

For every input, ask what happens if it's missing, empty, the wrong type or too big. Each answer becomes a check in your code.

5. Vague names

data, tmp, res, x2 and thing tell the next reader nothing, and in two weeks that reader is you. A name should say what the value holds, in the words of the problem.

example.gogo
// Hard to follow
for _, d := range data {
	if d.s == 500 {
		tmp++
	}
}

// Clear
for _, req := range requests {
	if req.Status == 500 {
		serverErrors++
	}
}

Go's convention is short names for short scopes and longer names for wider ones. i is fine for a three-line loop. A package-level variable called c is not. If you struggle to name something, that's often a sign it does two things.

6. One huge function that does everything

A 200-line main that reads a file, parses it, calculates totals and prints a report is hard to test and harder to change. Split it by job. Here's a small CLI that counts HTTP status codes in an access log:

example.gogo
package main

import (
	"bufio"
	"fmt"
	"io"
	"maps"
	"os"
	"slices"
	"strings"
)

func main() {
	if len(os.Args) != 2 {
		fmt.Fprintln(os.Stderr, "usage: logstats <access.log>")
		os.Exit(2)
	}
	f, err := os.Open(os.Args[1])
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	defer f.Close()

	counts, err := countStatusCodes(f)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	printReport(os.Stdout, counts)
}

// countStatusCodes reads access log lines and counts the status code,
// which is the last field on each line.
func countStatusCodes(r io.Reader) (map[string]int, error) {
	counts := make(map[string]int)
	sc := bufio.NewScanner(r)
	for sc.Scan() {
		fields := strings.Fields(sc.Text())
		if len(fields) == 0 {
			continue
		}
		counts[fields[len(fields)-1]]++
	}
	return counts, sc.Err()
}

func printReport(w io.Writer, counts map[string]int) {
	for _, code := range slices.Sorted(maps.Keys(counts)) {
		fmt.Fprintf(w, "%s %d\n", code, counts[code])
	}
}
example.texttext
200 2
404 1
500 1

main now only wires things together. countStatusCodes takes an io.Reader instead of a filename, so a test can pass it a strings.Reader without touching the disk. printReport takes an io.Writer for the same reason. Each function fits on one screen and does one thing.

7. Off-by-one errors and empty input

Loops that run one step too far are a classic. This one parses a CSV row and uses <= where it needs <:

example.gogo
package main

import (
	"fmt"
	"strings"
)

func main() {
	fields := strings.Split("alice,admin,active", ",")
	for i := 0; i <= len(fields); i++ {
		fmt.Println(fields[i])
	}
}
example.texttext
alice
admin
active
panic: runtime error: index out of range [3] with length 3

goroutine 1 [running]:
main.main()
	/home/you/csvrow/main.go:11 +0xc0
exit status 2

The panic says exactly what happened. Index 3 was used on a slice of length 3, whose last valid index is 2, at line 11. In Go the simplest fix is to not manage the index at all: for _, field := range fields can't run past the end.

The quieter cousin is empty input. This function averages request latencies:

example.gogo
package main

import "fmt"

func averageLatency(ms []float64) float64 {
	var total float64
	for _, v := range ms {
		total += v
	}
	return total / float64(len(ms))
}

func main() {
	fmt.Println(averageLatency([]float64{120, 80, 100}))
	fmt.Println(averageLatency(nil))
}
example.texttext
100
NaN

No requests means dividing by zero, which gives NaN for floats and a panic for integers. Always ask what your function does with zero items, one item and the maximum. Those three cases catch a lot of boundary bugs.

8. Not using version control from day one

Many beginners put off Git until a "real" project. Then they break something that worked an hour ago and have no way back. Git is the undo button for your whole project, and it costs almost nothing to start:

example.bashbash
git init
git add .
git commit -m "Parse port from PORT env var"

Commit every time something works, with a message that says what changed. When you break it, git diff shows what you changed since the last good state. Push to GitHub or another host and you also have a backup and a portfolio.

9. Hardcoding config and secrets

Putting a database password or API key straight into the source works until you push it to a public repo. GitGuardian counted 28.65 million new hardcoded secrets added to public GitHub commits in 2025, up 34% from the year before (GitGuardian, 2026). Ports, URLs and credentials also differ between your laptop and production, so hardcoding them means editing code to deploy.

Read them from the environment instead:

example.gogo
package main

import (
	"cmp"
	"fmt"
	"os"
)

func main() {
	addr := ":" + cmp.Or(os.Getenv("PORT"), "8080")
	apiKey := os.Getenv("PAYMENTS_API_KEY")
	if apiKey == "" {
		fmt.Fprintln(os.Stderr, "PAYMENTS_API_KEY is not set")
		os.Exit(1)
	}
	fmt.Println("listening on", addr)
}

cmp.Or returns the first non-empty value, so PORT gets a sensible default. The API key gets no default, and the program refuses to start without it. If you keep local values in a .env file, add it to .gitignore before your first commit.

10. Never writing a test

Beginners test by running the program and looking at the output. That works once. It doesn't tell you when a later change breaks something that used to work. Go ships testing in the standard toolchain, so there's nothing to install. First, fix averageLatency from mistake 7 so it reports whether there was any data:

example.gogo
func averageLatency(ms []float64) (float64, bool) {
	if len(ms) == 0 {
		return 0, false
	}
	var total float64
	for _, v := range ms {
		total += v
	}
	return total / float64(len(ms)), true
}

Then add a table test next to it:

example.gogo
func TestAverageLatency(t *testing.T) {
	tests := []struct {
		name   string
		in     []float64
		want   float64
		wantOK bool
	}{
		{"three requests", []float64{120, 80, 100}, 100, true},
		{"one request", []float64{42}, 42, true},
		{"no requests", nil, 0, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, ok := averageLatency(tt.in)
			if got != tt.want || ok != tt.wantOK {
				t.Errorf("averageLatency(%v) = %v, %v, want %v, %v", tt.in, got, ok, tt.want, tt.wantOK)
			}
		})
	}
}
example.texttext
--- PASS: TestAverageLatency (0.00s)
    --- PASS: TestAverageLatency/three_requests (0.00s)
    --- PASS: TestAverageLatency/one_request (0.00s)
    --- PASS: TestAverageLatency/no_requests (0.00s)
PASS

Put it in a file ending in _test.go and run go test ./.... Each row is one case, so adding a new edge case is one more line. The no requests row is the input that returned NaN in mistake 7. Now it has a defined answer and a test that keeps it that way.

11. Debugging by guessing

The beginner debugging loop goes like this: change something, run it, still broken, change something else. After ten rounds the code has new bugs and the original one is still there. In a study of 21 students from seven universities, some "used few strategies, applied them ineffectively" and introduced new bugs while debugging (Murphy et al., 2008).

Look before you change. Form a guess about what's wrong, then check it by printing the actual values:

example.gogo
log.Printf("order=%+v total=%d items=%d", order, total, len(order.Items))

%+v prints struct field names, which saves you from guessing which number is which. When prints get noisy, use a debugger. Delve is the standard one for Go, and VS Code and GoLand both use it under the hood:

example.bashbash
dlv debug . -- access.log
(dlv) break main.go:43
(dlv) continue
(dlv) print fields
[]string len: 3, cap: 3, [
	"GET",
	"/api/orders",
	"200",
]

Line 43 is the counts[...]++ line in the log counter from mistake 6. The program stops there and you inspect the real value of fields instead of guessing it.

Once you find the bug, write a test that reproduces it before you fix it. Then it can't come back quietly.

12. Abstracting or optimizing before it works

It's tempting to design interfaces, config systems and plugin layers for a program that doesn't run yet. Or to replace a clear loop with something clever because it might be faster. Both make code harder to change before you know what it needs to do.

Get it working with the plainest code you can write. Wait until you've written the same thing three times before pulling it into a shared function. In Go, define an interface when a second implementation actually shows up, not when you imagine one. Optimize only after you've measured, and Go gives you the tools for that: go test -bench for benchmarks and pprof for profiles.

13. Staying in tutorials and asking for help without a repro

Tutorials make you feel productive because every step works. The first time you open a blank file, you find out how much you actually retained. At some point you have to build something nobody walked you through: a CLI that renames files, a small HTTP API, a bot for a chat app. It will be messy, and you'll learn more from it than from the next three tutorials. How to learn to code in 2026 covers how to structure that practice, and how to learn Go lays out a full plan for Go.

When you get stuck, you'll ask for help, and how you ask matters. "My code doesn't work" gets you nothing. Cut the problem down to the smallest program that still shows the bug, then share that program, the exact error output, what you expected and what you tried. You'll often find the bug yourself while cutting it down. For Go, the Go Playground gives you a shareable link to a runnable program.

Which of these mistakes does Go catch for you?

Go was designed for large codebases maintained by many people, and several of its rules happen to block beginner mistakes before the code runs.

  • Unused variables and imports don't compile. The compiler rejects declared and not used and "strings" imported and not used. The Go FAQ calls this "trading short-term convenience for long-term build speed and program clarity" and notes that an unused variable may indicate a bug.
  • gofmt formats every Go file the same way, so you never debate spacing or brace position and messy indentation can't hide a bug (gofmt).
  • go vet catches suspicious code. It flags things like a %d verb given a string, which compiles fine but prints garbage (cmd/vet).
  • Errors are values you have to handle. A function that can fail returns an error. When it returns a value and an error, you can't use the value without also receiving the error, even if you then discard it with a visible _. The happy-path mistake from section 4 becomes something you have to write on purpose (Errors are values).
  • Testing is built in. go test and the testing package ship with Go, so there's no framework to choose before your first test (testing).
  • The race detector finds data races. Run tests or programs with -race and Go reports concurrent access to shared memory with the file and line of both accesses (Data Race Detector).

Here is go vet catching a formatting bug that compiles:

example.gogo
package main

import "fmt"

func main() {
	active := "12"
	fmt.Printf("%d active users\n", active)
}
example.texttext
./main.go:7:14: fmt.Printf format %d has arg active of wrong type string

Go won't stop you from writing a 200-line function or naming a variable tmp. It does catch the mechanical mistakes, which leaves you more time for the ones that need judgment. For Go-specific traps like shadowed errors or append surprises, see common Go mistakes to avoid.

FAQ

What is the most common mistake beginner programmers make?

In the largest study of novice compile errors, covering 37 million compilations by more than 250,000 Java students, the most frequent error was mismatched brackets or quotes. Those got fixed quickly. The expensive mistakes were the ones the compiler didn't flag, such as ignoring a return value, which usually took more than 17 minutes to fix or never got fixed (Altadmri & Brown, 2015). As a habit, not reading the error message is the one that slows down everything else.

How do I stop making the same programming mistakes?

Write a test for every bug you fix, so it can't come back without you noticing. Keep a short list of mistakes you've made more than once and check your code against it before you commit. Tools help too. In Go, go vet and gofmt catch a class of mistakes automatically.

Is Go a good language for beginners who make a lot of mistakes?

Yes. Go's compiler rejects unused variables and imports, its errors are explicit return values, and go vet, gofmt and go test ship with the language. Its error messages are short and point to a file and line. See is Go a good first programming language for the full comparison.

Should beginners use AI to write code?

Use it to explain code, errors and concepts, not to write code you can't follow. A good rule is to never paste in code you couldn't rewrite from memory the next day. You'll need that understanding to fix AI code when it's almost right.

How long does it take to stop making beginner mistakes?

For most people these habits fade after a few months of writing code regularly, because each one costs you time until you fix it. Daily practice on real programs gets you there faster than long occasional sessions. The mechanical mistakes go first. Judgment calls like naming and function size take longer.

Start writing Go today

Every mistake on this list gets easier to avoid the more code you write and the faster you see what went wrong. That's the idea behind LevelUpGo. Each lesson is a Go exercise that runs on a real Go toolchain in your browser, so from your first day you read actual compiler errors, handle err values and watch tests fail. Start with the Go Fundamentals track.

Write Go like a senior engineer

Interactive lessons in your browser. The first ones are free.

Try a free lessonOr create a free account