Agentic AI Engineering with Python: Live Course
GoGo Foundations

Writing Your First Program

Every Go program you will ever write has the same skeleton as the one below. Nine lines, and once you understand each of them properly there is nothing structural left to learn about Go files. So rather than typing it and moving on, we are going to take it apart line by line and then deliberately break it four different ways.

Create the project

Go wants your code to live inside a module, so start there.

mkdir hello
cd hello
go mod init example.com/hello

That last command creates a file called go.mod containing two lines. It marks this folder as the root of a module, and everything below it belongs to that module. The next page in this section explains modules properly, so for now treat it as the thing that makes Go stop complaining.

Now create main.go:

main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, Go")
}

Run it:

go run .
Hello, Go

Line by line

package main

Every Go file starts by declaring which package it belongs to. Packages are how Go groups code, and a folder maps to exactly one package.

The name main is special. It tells the compiler this package should be built into an executable program rather than a library. Any other name, such as package storage or package auth, produces a package that other code can import but that cannot be run on its own.

   package main      →  go build gives you a runnable binary
   package anything  →  go build gives you a reusable library

import "fmt"

fmt is short for format, and it is the standard library package for printing and formatting text. Importing it makes its exported names available under the fmt prefix.

When you need more than one package, group them in parentheses. This is the form you will see in real code, and gofmt will sort them alphabetically for you:

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

func main()

func declares a function. The function named main, inside the package named main, is where execution begins. It takes no parameters and returns nothing, and those are not choices you get to make. This exact signature is required.

When main returns, the program exits.

fmt.Println("Hello, Go")

A call to the Println function from the fmt package. It prints its arguments and adds a newline.

Notice the capital P. In Go, a name that starts with a capital letter is exported, meaning it can be used from outside its own package. A lowercase name is private to its package. There is no public or private keyword because capitalisation carries that information.

fmt.Println("visible")   // exported, you can call it
fmt.println("nope")      // unexported, compile error

This rule applies everywhere in Go, to functions, types, struct fields, constants, and variables. It is one of the most useful things to internalise early.

Break it on purpose

Reading rules teaches you less than watching the compiler enforce them. Try each of these, read the error, then undo it.

Import something and do not use it

import (
    "fmt"
    "os"
)
./main.go:5:5: "os" imported and not used

Most languages warn about unused imports. Go refuses to compile. This feels harsh for about a week, and then you notice you have never once seen a Go file with a stale import in it.

Declare a variable and ignore it

func main() {
    message := "Hello, Go"
    fmt.Println("something else")
}
./main.go:6:5: declared and not used: message

Same principle. An unused local variable is usually a bug or a leftover, so Go treats it as an error rather than a suggestion.

Move the opening brace to its own line

func main()
{
    fmt.Println("Hello, Go")
}
./main.go:5:1: syntax error: unexpected semicolon or newline before {

Go's lexer inserts semicolons at the end of lines automatically. Putting the brace on the next line means a semicolon lands after main(), which ends the declaration before the body arrives. This is why Go has exactly one brace style, and why nobody argues about it.

Rename main to something else

func start() {
    fmt.Println("Hello, Go")
}
runtime.main_main·f: function main is undeclared in the main package

The entry point is fixed by name. There is no configuration file, no manifest, and no annotation that points at a different one.

Printing, three ways

fmt gives you three printing functions you will use constantly, and the difference between them is worth learning now rather than guessing at later.

package main

import "fmt"

func main() {
    name := "Telusko"
    count := 3

    fmt.Print("no newline at the end")
    fmt.Println("adds a newline for you")
    fmt.Printf("%s has %d courses\n", name, count)
}
no newline at the endadds a newline for you
Telusko has 3 courses

Printf uses verbs to say how each value should be rendered. These five cover nearly everything early on:

VerbPrintsExample output
%sa stringTelusko
%dan integer in base ten42
%fa floating point number19.990000
%ta booleantrue
%vany value in its default formatworks for all of the above

When you do not know or do not care which verb applies, %v is the safe answer. There is also %+v, which prints struct field names alongside their values, and it is the single most useful debugging tool in the language. You will meet it in the section on structs.

A slightly bigger first program

Printing a fixed string is not much of a program. This version reads a command line argument, which makes it something you could actually run twice with different results.

main.go
package main

import (
    "fmt"
    "os"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Println("usage: hello <name>")
        os.Exit(1)
    }

    name := os.Args[1]
    fmt.Printf("Hello, %s. Welcome to Go.\n", name)
}
go run . Shiva
Hello, Shiva. Welcome to Go.

os.Args is a slice of strings holding the command line arguments, where position zero is the program's own path and the real arguments start at position one. os.Exit(1) ends the program immediately with a non zero status, which is how a command line tool signals failure to whatever launched it.

os.Exit stops the program on the spot. Any pending defer calls are skipped, and buffered output may never be flushed. It is the right tool for a usage error at the top of main, and the wrong tool almost everywhere else.

You now have a program that compiles, runs, takes input, and fails cleanly. Next, let's look at what go run was actually doing, and how it differs from go build and go install.

How is this guide?

Last updated on