Go Module 1 – Getting started with Go 1.27
Your first Go program: go run and go build
Write a Go program, run it with go run, build a binary with go build, read its build information and fix the first compile errors beginners meet.
What you will learn
- Write a main package and run it with go run
- Fix the first compile errors: an unused import, an unused variable and a brace on its own line
- Format a file with gofmt and find a suspicious call with go vet
- Build a binary with go build, read its build information and build it for another system
Before you start
What you’ll learn · Before you start
What you will learn
- Write a main package and run it with go run
- Fix the first compile errors: an unused import, an unused variable and a brace on its own line
- Format a file with gofmt and find a suspicious call with go vet
- Build a binary with go build, read its build information and build it for another system
Before you start
On this page
A Go program of ten lines, built with one command, becomes a 2.3 MB file that runs on its own, and the same command builds it for Linux and Windows from a Mac. On the way you meet the three errors every newcomer sees first: an unused import, an unused variable and an opening brace on a line of its own.
You will write and run a main package, fix those three errors, let gofmt lay out the code and go vet check it, build an executable and read the build information it carries, and build the same program for two other systems.
A program starts in func main of package main
Every Go file begins with a package clause, and a program is the package named main. Here is a whole program:
// hello.go: a whole Go program. It starts in func main of package main.
package main
import "fmt"
func main() {
name := "Asha"
fmt.Println("Hello,", name)
fmt.Printf("%s has %d letters\n", name, len(name))
} Output
Hello, Asha Asha has 4 letters
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run hello.go
Read it from the top. package main says this file belongs to the package main: The package that makes a program. It must be named main and declare a func main that takes no arguments and returns nothing; running the program runs that function..
import "fmt" makes the standard formatting package available under the name fmt. func main() is where the program
starts: the specification says a program runs by initializing it and then calling the function main of package
main, which takes no arguments and returns no value, and the program ends when main returns. Inside it,
name := "Asha" declares a variable and gives it a value at once, fmt.Println prints its arguments separated by
spaces, and fmt.Printf fills %s with a string and %d with a whole number.
go run hello.go compiled that file and ran it; its output is under the code. Step through what the command did, and
what changes when you run the same, unchanged file a second time:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The command. go run takes a main package, here the single file hello.go, builds it and runs the result. Arguments after the file name are passed on to the program.
- Compile. The compiler turns hello.go into machine code. Packages it imports, such as fmt, are compiled once and then taken from the build cache. By default go run leaves out the information a debugger uses, to save time.
- Link. The linker joins that code with the compiled packages and the Go runtime into one executable, written into a temporary folder that the go command removes when it is done.
- Keep a copy. Since Go 1.24 the executable is also kept in the build cache. Repeated runs get faster, at the cost of a larger cache.
- Run. The go command starts the executable, which prints Hello, Asha and Asha has 4 letters. The exit status of go run itself is not the exit status of the program.
- A second run. Run the same, unchanged file again and the go command finds the executable in the build cache and starts it at once, with no compile and no link.
go run is the command for writing and trying a program: it leaves no file beside your code. The program it built
lives in a temporary folder while it runs, and a copy waits in the build cache for the next run.
Checkpoint
Check: A main package, run with go run
Go refuses an unused import or an unused variable
Programs change while you write them, and a line you no longer need is easy to leave behind. Go does not let it stay. This file imports a package it never uses and declares a variable it never reads:
// unused.go: an import and a variable that are never used.
package main
import (
"fmt"
"os"
)
func main() {
count := 3
fmt.Println("Hello, Go")
} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments ./unused.go:6:2: "os" imported and not used ./unused.go:10:2: declared and not used: count
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run unused.go
The run failed before anything was built, with two errors: "os" imported and not used and
declared and not used: count. The first comes from the specification itself, which makes it illegal to import a
package without referring to anything it exports. The second is an implementation restriction: the specification allows
a compiler to reject a variable declared inside a function body and never used, and the standard compiler does. An
unused function, or an unused variable at package level, outside every function, compiles; only these two cases stop
the build.
| What the file contains | What the compiler does |
|---|---|
| An import whose package the file never uses | Stops: illegal in the specification |
| A local variable that is set and never used | Stops: the compiler’s restriction |
A local variable you assign to _ |
Compiles: the value is thrown away on purpose |
| An unused function or package-level variable | Compiles |
The blank identifier: The name _ (an underscore), which takes a value and throws it away. It lets code ignore a result on purpose, or import a package only for what it does when it starts. _ is the deliberate way out. _ = count uses the value by
throwing it away, and import _ "image/png" imports a package only for what it does when the program starts.
Silencing the error and keeping the line
_ = count makes the error go away, and it also hides the mistake the error found. Use it for a moment while a
change is half done, then delete both lines, or use the variable. For imports, let your editor organise them: gopls
can add and remove imports for you, usually when you save.
A line break can end a statement, so the brace stays on the line
Go source has no semicolons at the ends of lines, because the compiler inserts them. That rule, called semicolon insertion: Go's rule that a semicolon is added after a line whose last token could end a statement, such as a name, a literal or a closing bracket. It is why an opening brace must stay on the line before it., decides where an opening brace may go:
// brace.go: the opening brace on a line of its own.
package main
import "fmt"
func main()
{
fmt.Println("Hello, Go")
} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments
./brace.go:7:1: syntax error: unexpected semicolon or newline before {
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run brace.go
The compiler stopped at line 7, column 1: syntax error: unexpected semicolon or newline before {. Step through what it
did with the two lines:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- The line ends. Go source has no semicolons at the ends of lines because the compiler adds them. The specification's rule adds one after a line whose last token is a name, a literal, a few keywords such as return, ++ or --, or a closing ), ] or }.
- A semicolon goes in. Line 6 ends with ), so a semicolon is added after it. The compiler now reads func main(); as a complete declaration of a function without a body.
- The brace alone. Line 7 starts with an opening brace. At this point the compiler expects a new declaration, and a block cannot start one.
- The error. The compiler stops with syntax error, unexpected semicolon or newline before {, at line 7, column 1. Move the brace to the end of line 6, where gofmt puts it, and the file compiles.
The same rule holds for if, for, switch and select: Effective Go notes that the opening brace of a control
structure cannot go on the next line, because a semicolon would be inserted before it. So there is no brace style to
choose in Go, and the tool of the next section puts every brace where it belongs anyway. The rule is also why most Go
lines have no semicolon at all: you write them only between the clauses of a for loop, as in
for i := 0; i < 3; i++, or to put two statements on one line.
Checkpoint
Check: Unused imports and variables · Semicolons and where braces go
gofmt lays the code out, and go vet finds what compiles but is wrong
Go has one layout for source code, and a tool applies it. gofmt: The tool that lays out Go source in the one standard format, with tabs for indentation and spaces for alignment. gofmt -l lists the files it would change and gofmt -w rewrites them. indents with tabs, aligns with
spaces, and puts every space and blank line where the standard format wants it, so layout never has to be discussed in
a code review; go fmt ./... runs it on every package of a module.
Here are two files: the first typed without care, the second laid out well but with a bug:
package main
import "fmt"
func main() {
name:="Asha"
if len(name)>3 {
fmt.Println("Hello,",name)
}
} // vetme.go: it compiles, but the verb %d does not fit a string.
package main
import "fmt"
func main() {
name := "Asha"
fmt.Printf("Hello, %d\n", name)
} from session import run
# gofmt lists, shows and fixes layout; go vet finds a bug the compiler accepts.
run(
"""
gofmt -l tidy
gofmt -d tidy/messy.go
gofmt -w tidy/messy.go
gofmt -l tidy
go vet tidy/vetme.go
go run tidy/vetme.go
""",
expect_failure=["gofmt -d tidy/messy.go", "go vet tidy/vetme.go"],
) Output
$ gofmt -l tidy
tidy/messy.go
$ gofmt -d tidy/messy.go
diff tidy/messy.go.orig tidy/messy.go
--- tidy/messy.go.orig
+++ tidy/messy.go
@@ -1,8 +1,10 @@
package main
+
import "fmt"
+
func main() {
- name:="Asha"
- if len(name)>3 {
- fmt.Println("Hello,",name)
- }
+ name := "Asha"
+ if len(name) > 3 {
+ fmt.Println("Hello,", name)
+ }
}
[exit status 1]
$ gofmt -w tidy/messy.go
$ gofmt -l tidy
$ go vet tidy/vetme.go
tidy/vetme.go:8:21: fmt.Printf format %d has arg name of wrong type string
[exit status 1]
$ go run tidy/vetme.go
Hello, %!d(string=Asha)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 format_session.py
| Command | What it does | In the session |
|---|---|---|
gofmt -l |
Lists the files whose layout differs | tidy/messy.go |
gofmt -d |
Shows the changes as a diff | blank lines added, tabs and spaces fixed |
gofmt -w |
Rewrites the files | afterwards -l lists nothing |
go vet |
Reports code that is probably wrong | the %d verb with a string |
gofmt -d ended with exit status 1 after showing its changes, which lets a script fail on unformatted code. The last two commands show why go vet: The go command that reports code which compiles but is probably wrong, such as a Printf verb that does not fit its argument. matters: vetme.go compiles, and it runs, but it
prints Hello, %!d(string=Asha), because %d expects a number and got the string Asha. vet caught the mistake before
the program ran: fmt.Printf format %d has arg name of wrong type string. Editors can run gofmt each time you save,
and gopls reports static-analysis findings while you type.
Key idea
gofmt decides the layout and go vet looks for code that compiles but is probably wrong; run both before every commit.
Checkpoint
Check: gofmt and go vet
go build writes one executable that records how it was built
go run keeps nothing beside your code. To get a file you can test, copy or ship, make the program a module and build
it. The session makes a module named example.com/greeter from hello.go, builds it, runs the result and looks inside:
from session import run
# Make a module from hello.go, build it, run the binary and read what it carries.
run("""
mkdir greeter
cp hello.go greeter/main.go
cd greeter && go mod init example.com/greeter
cat greeter/go.mod
cd greeter && go build
ls greeter
./greeter/greeter
du -h greeter/greeter
go version -m greeter/greeter
file greeter/greeter
""") Output
$ mkdir greeter $ cp hello.go greeter/main.go $ cd greeter && go mod init example.com/greeter go: creating new go.mod: module example.com/greeter go: to add module requirements and sums: go mod tidy $ cat greeter/go.mod module example.com/greeter go 1.27.1 $ cd greeter && go build $ ls greeter go.mod greeter main.go $ ./greeter/greeter Hello, Asha Asha has 4 letters $ du -h greeter/greeter 2.3M greeter/greeter $ go version -m greeter/greeter greeter/greeter: go1.27.1 path example.com/greeter mod example.com/greeter (devel) build -buildmode=exe build -compiler=gc build CGO_ENABLED=1 build CGO_CFLAGS= build CGO_CPPFLAGS= build CGO_CXXFLAGS= build CGO_LDFLAGS= build GOARCH=arm64 build GOOS=darwin build GOARM64=v8.0 $ file greeter/greeter greeter/greeter: Mach-O 64-bit executable arm64
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 build_session.py
go mod init wrote a go.mod with the module path and the go line: Go 1.27.1 wrote go 1.27.1, its own version.
go build printed nothing, which means it worked, and left an executable named after the last part of the module path,
greeter. The file runs without the go command, and du measured it at 2.3 MB: the program’s own code is tiny, and the
rest is the Go runtime and the packages it uses.
go version -m prints the build information: What the go command records inside every executable it builds: the Go version, the module path and the build settings. go version -m prints it. that every Go executable carries:
| Line | What it says |
|---|---|
greeter/greeter: go1.27.1 |
The Go release that built it |
path and mod |
The main package and its module; (devel) because the folder has no version-control tag |
-buildmode=exe, -compiler=gc |
An ordinary executable, built by the standard compiler |
CGO_ENABLED=1 and the CGO_ flags |
cgo, the bridge to C code, was available on this Mac; this program uses none |
GOOS=darwin, GOARCH=arm64, GOARM64=v8.0 |
The system and processor it was built for |
Those lines are what you read when a binary misbehaves somewhere else: they say which release built it, for which system, and with which settings.
Versions stamped from version control, since Go 1.24
Since Go 1.24, go build also stamps the main module’s version from the version-control tag or commit when the code
is in a repository, with +dirty for uncommitted changes. go version -m on a binary in production then tells you
which release or commit it was built from.
Checkpoint
Check: go build and build information
One computer builds for every system
GOOS and GOARCH choose the target of a build. Set them in front of go build and the same Mac builds for another
operating system and processor; this is cross-compiling: Building a program on one system for another, such as a Linux or Windows executable on a Mac. In Go, the GOOS and GOARCH variables name the target system and processor., and Go needs nothing extra
installed for it:
from session import run
# The same module, built on this Mac for Linux and for Windows.
run("""
mkdir greeter
cp hello.go greeter/main.go
cd greeter && go mod init example.com/greeter
cd greeter && GOOS=linux GOARCH=amd64 go build -o greeter-linux
cd greeter && GOOS=windows GOARCH=amd64 go build -o greeter.exe
file -b greeter/greeter-linux | cut -d, -f1-4
file -b greeter/greeter.exe
du -h greeter/greeter-linux greeter/greeter.exe
""") Output
$ mkdir greeter $ cp hello.go greeter/main.go $ cd greeter && go mod init example.com/greeter go: creating new go.mod: module example.com/greeter go: to add module requirements and sums: go mod tidy $ cd greeter && GOOS=linux GOARCH=amd64 go build -o greeter-linux $ cd greeter && GOOS=windows GOARCH=amd64 go build -o greeter.exe $ file -b greeter/greeter-linux | cut -d, -f1-4 ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked $ file -b greeter/greeter.exe PE32+ executable (console) x86-64, for MS Windows $ du -h greeter/greeter-linux greeter/greeter.exe 2.3M greeter/greeter-linux 2.4M greeter/greeter.exe
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cross_session.py
Step through the three builds of the two sessions:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- One source file. The module holds one file, main.go, a copy of hello.go. Nothing in it says which system it is for.
- For this Mac. go build with no settings builds for the machine it runs on. file reported a Mach-O 64-bit executable for arm64, and du measured 2.3 MB.
- For Linux. GOOS=linux and GOARCH=amd64 in front of the same command made an ELF executable for x86-64 Linux, statically linked, also 2.3 MB. A Linux server runs it with nothing installed.
- For Windows. GOOS=windows made a PE32+ console program for x86-64 Windows, 2.4 MB, named greeter.exe with -o, the ending Windows expects of a program.
- Three files, one Mac. All three came from the same source on the same Mac, with no compiler for Linux or Windows installed. Each runs only on the system it was built for.
file reported the Linux build as an ELF executable for x86-64, statically linked: An executable that holds every library it needs, so it runs on a machine where none of them is installed. Go's Linux executables are statically linked when cgo is not used.:
it holds everything it needs and runs on a Linux server where Go was never installed. The cgo documentation says cgo is
turned off by default when cross-compiling, which is why the Linux file needs no C library. The Windows build is a PE32+
console program of 2.4 MB. Each file runs only on the system it was built for: copy the Mac file to a Linux server and
the kernel refuses to start it.
Checkpoint
Check: Building for other systems
Practise: change, format, vet, build
Install Go first if you have not (the previous lesson), then work in a new folder:
- Copy hello.go into it and run
go run hello.go: it prints the two lines shown above. - Change
mainso that it prints a three-line receipt of your own, an item, a quantity and a price per line, usingfmt.Printfwith%sand%d. - Run
gofmt -l .andgo vet hello.gountil both print nothing. - Run
go mod init example.com/receipt, thengo build, and start the program it wrote. - Run
go version -mon the file and find its GOOS and GOARCH.
Then test the six ideas of this lesson. Each question comes with its explanation, and the weakest idea is asked first:
Practice that adapts to you
Practise this lesson
12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What is the difference between go run and go build?
go run compiles a main package, links it into an executable in a temporary folder and runs it at once, passing on any arguments after the package. Since Go 1.24 that executable is also kept in the build cache, so running unchanged code again skips the build, and by default go run leaves out the information debuggers use. go build compiles and links the same package but writes the executable beside your code, or to the file named by -o, and does not run it. Use go run while you write and try a program, and go build to make the file you test, copy or ship. Both build for the system named by GOOS and GOARCH, and only go build leaves a file you can copy to another machine.
Two longer interview questions of this module start from this lesson:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
Go refuses to compile a function with an unused local variable, or a file with an unused import, yet it accepts an unused function or an unused package-level variable. Why the difference, and how do you work with the rule while you debug?
-
Fresher to mid
A binary built on a developer's Mac runs fine there, but the same file copied to a Linux server fails with "exec format error". Explain why, how you would build it correctly, and how you would check any binary before it ships.
Key takeaways
- A program is the package
mainwith afunc main, andgo runcompiles it into a temporary executable, runs it and keeps a copy in the build cache for the next run. - An unused import or an unused local variable stops the build;
_throws a value away on purpose, briefly. - The compiler inserts a semicolon after a line that ends with
), so an opening brace must stay on the line that starts its block. - gofmt gives every file the one standard layout, and go vet reports code that compiles but is probably wrong, such as a Printf verb that does not fit its argument.
go mod initandgo buildturn the program into one executable, 2.3 MB here, whose build informationgo version -mprints.GOOSandGOARCHbuild the same code for other systems from one computer; the Linux build was statically linked.
Terms in this lesson
- blank identifier
- The name _ (an underscore), which takes a value and throws it away. It lets code ignore a result on purpose, or import a package only for what it does when it starts.
- build information
- What the go command records inside every executable it builds: the Go version, the module path and the build settings. go version -m prints it.
- cross-compiling
- Building a program on one system for another, such as a Linux or Windows executable on a Mac. In Go, the GOOS and GOARCH variables name the target system and processor.
- go vet
- The go command that reports code which compiles but is probably wrong, such as a Printf verb that does not fit its argument.
- gofmt
- The tool that lays out Go source in the one standard format, with tabs for indentation and spaces for alignment. gofmt -l lists the files it would change and gofmt -w rewrites them.
- package main
- The package that makes a program. It must be named main and declare a func main that takes no arguments and returns nothing; running the program runs that function.
- semicolon insertion
- Go's rule that a semicolon is added after a line whose last token could end a statement, such as a name, a literal or a closing bracket. It is why an opening brace must stay on the line before it.
- statically linked
- An executable that holds every library it needs, so it runs on a machine where none of them is installed. Go's Linux executables are statically linked when cgo is not used.
References
- Tutorial: Get started with Go (The Go Authors)
- How to Write Go Code (The Go Authors)
- Command go (The Go Authors)
- Command gofmt (The Go Authors)
- Command vet (The Go Authors)
- Package fmt (The Go Authors)
- Package runtime: GOOS and GOARCH (The Go Authors)
- The Go Programming Language Specification: Program execution (The Go Authors)
- The Go Programming Language Specification: Semicolons (The Go Authors)
- The Go Programming Language Specification: Import declarations (The Go Authors)
- The Go Programming Language Specification: Variable declarations (The Go Authors)
- Effective Go: Formatting (The Go Authors)
- Effective Go: Semicolons (The Go Authors)
- Command cgo (The Go Authors)
- Frequently Asked Questions (FAQ) (The Go Authors)
- Go 1.24 Release Notes (The Go Authors)
Report a problem with this lesson
Kept only in this browser. Your Learn progress