Your First Go Program and the go Command: Modules and Project Basics
Module 1: Go Language Intro & Dev Environment | Lesson: m01-l03 | Level: Beginner After this lesson you will be able to: understand the basic structure of a Go program, master the core usage of the
gocommand, and develop a sound sense of module-based project layout.
Learning Objectives
- Explain the roles of
package mainandfunc mainas the entry point of a Go program. - Describe the
importmechanism and how the capitalization of an identifier's first letter controls package visibility. - Use
go mod initto initialize a module, and master common subcommands such asgo run,go buildandgo vet. - Recognize and avoid common project-layout mistakes, such as relative imports or misusing the import restrictions of
package main.
Core Concepts
2.1 The Basic Structure of a Go Program
2.1.1 Packages and the Entry Function
What: Go organizes code around "packages". A package is a group of source files in the same directory that share a single package declaration; it is the unit of compilation and code reuse. Every Go file must belong to exactly one package.
Why: Go has no class inheritance. Packages are organized into a hierarchy by import path, but packages are not nested declarations, so the semantics are simpler than namespaces.
How:
- package main: declares that the current file belongs to the main package. Only a program built from the main package produces an executable binary.
- func main(): the main package must contain exactly one main function with no parameters and no return values; it is the entry point of program execution.
2.1.2 import and Package Visibility
What: use the import keyword to pull in other packages.
Why: Go isolates namespaces by package name, avoiding collisions.
How:
- Visibility rule: in Go, the visibility of an identifier (variable, function, type, etc.) is determined by the case of its first letter. A capital first letter means exported — other packages can access it; a lowercase first letter means unexported — it is visible only inside the package.
- Import restriction: in module mode, import can only use a standard-library path or a module path; it does not support relative paths like ./sub. Relative imports are a leftover from the GOPATH era and are no longer available.
- Important constraint: package main cannot be imported by other packages. If you try to import a package main, the compiler reports is a program, not an importable package. Therefore, when demonstrating "a capital first letter means accessible", you must use a non-main package.
2.2 The go Command in Depth
The Go toolchain provides a rich set of subcommands that are the core of day-to-day development.
2.2.1 go mod init
What: initialize the current directory as a Go module.
Why: Go 1.16+ enforces module management. Without an initialized module, commands such as go get fail directly with go.mod file not found.... A single-file go run x.go still works, but dependency management and version recording must rely on go.mod.
How: go mod init <module-name>. The module name usually follows the domain/project convention.
2.2.2 go run / build / install
go run: compiles and immediately runs a source file. Great for quickly testing a snippet.go build: compiles source files. It produces an executable only when the package ismain; for a library package it by default just checks that it compiles, without producing an output file.go install: compiles and installs a package. The executable (a main package) is installed intoGOBIN(defaultGOPATH/bin) and can be called directly; an ordinary library package is only compiled and checked, with no installed artifact. In module mode, to install a tool from another module usego install example.com/tool@latest.
2.2.3 go vet / fmt / doc / test / mod tidy
go vet: flags suspicious constructs, such as a mismatchedfmt.Printfargument type. Such problems print garbage like%!d(...)rather than panicking, but they obscure the real result.go fmt: formats code and unifies style.go doc: shows documentation for the standard library or local packages.go test: runs test files (named*_test.go).go mod tidy: cleans upgo.modandgo.sum, adding missing dependencies and removing unused ones.
Example Code
Example 1: Hello World and Module Initialization
This example creates a standard Go program structure and shows the basic use of package main and func main.
package main
import (
"fmt"
)
func main() {
// Print Hello World
fmt.Println("Hello, World!")
// Demonstrate a simple variable declaration and output
name := "Go"
fmt.Printf("Welcome to %s!\n", name)
}
Expected output:
Hello, World!
Welcome to Go!
Example 2: Visibility Demo and a vet Check
This example demonstrates two files: a non-main package mypkg showing the export rules, and a main package that calls it. It also shows how go vet flags a semantic error.
Note: to explain visibility within a single-file demo, we show only the main package code here. In a real project, mypkg should live in its own directory. To satisfy the "single file must be runnable" constraint, we write the mypkg logic directly inside the main package, but explain the visibility rules through comments.
package main
import (
"fmt"
)
// ExportedFunc starts with a capital letter, meaning it is exported (accessible from other packages).
// Note: because this file belongs to package main, and package main cannot be imported,
// this function only takes effect within the current package. If moved into the mypkg package,
// other packages could then call it.
func ExportedFunc() string {
return "I am exported"
}
// unexportedFunc starts with a lowercase letter, meaning it is unexported (visible only within this package).
func unexportedFunc() string {
return "I am unexported"
}
func main() {
// Call the exported function
fmt.Println(ExportedFunc())
// Call the unexported function
fmt.Println(unexportedFunc())
// Demonstrate a go vet check: the format verb does not match the argument type.
// It compiles and runs, but go vet warns about it.
// We deliberately write it wrong to show what vet does.
var s string = "hello"
// The line below triggers a go vet warning: format %d has arg s of wrong type string.
// At runtime it prints %!d(string=hello).
fmt.Printf("String value: %d\n", s)
}
Expected output:
I am exported
I am unexported
%!d(string=hello)
Common Mistakes & Pitfalls
-
Misusing relative imports
- Wrong code:
import "./mypkg" - Why: relative-path imports are forbidden in Go 1.11+ module mode.
- Correct approach: use the module path, e.g.
import "example.com/myapp/mypkg".
- Wrong code:
-
Misunderstanding the visibility of
package main- Wrong assumption: "if a function starts with a capital letter, other packages can import and call it."
- Why:
package mainis not an importable package. Even if a function is exported, other packages cannot import themainpackage. - Correct approach: put reusable code in a non-main package (such as
mypkg) and import and call it from the main package.
-
Believing
go vetblocks compilation- Wrong assumption: "if
go vetreports an error, the program cannot run." - Why:
go vetis a static analysis tool and does not block compilation. It checks suspicious semantic points the compiler does not report (such as a Printf argument type mismatch). - Correct approach: make
go vetpart of your CI/CD pipeline to catch potential problems early.
- Wrong assumption: "if
Best Practices
- Always use module mode: the first thing you do in a new project is run
go mod init, so that dependency versions stay under control. - Follow package naming conventions: package names should be short, lowercase, and free of underscores, and should not clash with the standard library.
- Run
go mod tidyregularly: run it before committing code so thatgo.modandgo.sumstay clean and free of redundant dependencies. - Make good use of
go vetandgo fmt: run them continuously while coding, to keep style consistent and free of semantic errors. - Distinguish executable packages from library packages: the
mainpackage only holds entry logic; core business logic should live in library packages, making it easier to test and reuse.
Exercises
- Concept question: explain why an exported function in
package maincannot be imported by other packages. - Coding exercise: create a module named
calcthat contains anAddfunction (capital first letter) and asubfunction (lowercase first letter). CallAddfrom themainpackage and print the result. -
Advanced/debugging exercise: what is wrong with the following code? How do you fix it? ```go package main
import ( "fmt" )
func main() { var x int = 10 var y string = "hello" fmt.Printf("x: %d, y: %d\n", x, y) } ```
Reference Answers
Reference Answers (click to expand / collapse)
Question 1
Answer: because package main is the entry package of an executable program as defined by the Go language; the Go spec states that the main package cannot be imported by other packages. Even if its functions start with a capital letter (exported), other packages cannot access it through an import statement. This prevents the program entry point from being referenced by accident and keeps the program structure clear.
Question 2
Reference code:
package main
import (
"fmt"
"example.com/calc"
)
func main() {
result := calc.Add(1, 2)
fmt.Println("Result:", result)
}
Note: in a real project, the calc package should live in its own directory, containing an add.go file that defines func Add(a, b int) int { return a + b }. This is a single-file demo, assuming the module path is example.com/calc.
Question 3
The problem: fmt.Printf uses the %d verb to format the string variable y, causing a type mismatch. It compiles and runs, but the output is %!d(string=hello), and go vet warns about it.
The fix: change the format verb for y to %s.
package main
import (
"fmt"
)
func main() {
var x int = 10
var y string = "hello"
// Fixed: changed %d to %s
fmt.Printf("x: %d, y: %s\n", x, y)
}
Lesson Summary
- A Go program enters through
package mainandfunc main. - The capitalization of an identifier's first letter controls visibility, but
package mainitself cannot be imported. go mod initis the starting point of module management, andgo run/build/vetare the core commands of daily development.- Avoid relative imports, and use
go vetto catch semantic errors.
Next lesson preview: variables, constants and primitive types, including the zero-value mechanism and type conversion.