In the last chapter, you declared variables, learned Go's basic types, and ran your first program with make run. Every statement lived inside a single main.go file. This chapter introduces two tools you will use in every Go project: functions and packages. We will write our first custom packages, add a third-party dependency, and learn the single rule that controls which names other packages can see.
We split the code into two new packages, mathx and textx, then call them from main.go. The mathx package demonstrates the core function signatures in Go. The textx package shows how a third-party library (pulled in with go get) fits into the module.

By the end of this chapter you will:
Check out the finish branch to follow along:
bashgit checkout 07-functions-and-packages-finish
A Go function declaration follows a simple pattern: func name(params) returnType. Here is the simplest case, a function that adds two integers:
gofunc add(a, b int) int {return a + b}
When multiple parameters share a type, you can list them once at the end. a, b int is shorthand for a int, b int.
Functions that return two values are a defining pattern in Go. You will see them everywhere:
gofunc Average(nums ...int) (float64, error) {
Average returns a float64 and an error. The caller must handle both:
go// main.goavg, err := mathx.Average(7, 2, 9, 4, 1)if err != nil {fmt.Println("Average error:", err)} else {fmt.Printf("Average(7, 2, 9, 4, 1) = %.2f\n", avg)}
The if err != nil check is the most common pattern in Go. A function that can fail returns its result alongside an error. If the error is nil, the call succeeded and the first value is safe to use. If it is not nil, something went wrong. We look at errors in depth in chapter 12, but you will write this check from day one.
The second call to Average passes no arguments, so it takes the empty-input path:
go// main.go_, err = mathx.Average()if err != nil {fmt.Println("Average() error:", err)}
The _ (blank identifier) discards the float64 return value. We know there is nothing useful there when an error is returned, so we ignore it. This is a deliberate Go idiom.
Here is the full implementation in mathx/stats.go:
go// mathx/stats.govar ErrNoNumbers = errors.New("mathx: no numbers given")func Average(nums ...int) (float64, error) {if len(nums) == 0 {return 0, ErrNoNumbers}sum := 0for _, n := range nums {sum += n}return float64(sum) / float64(len(nums)), nil}
A sentinel error is a package-level variable that callers compare against to identify a specific failure. ErrNoNumbers is one example. Its message is "mathx: no numbers given", which is exactly what the program prints when you pass no arguments.
When the input is valid, Average returns nil as the error. nil on an error means no error.
MinMax uses another feature: named return values.
go// mathx/stats.gofunc MinMax(nums ...int) (min, max int) {if len(nums) == 0 {return 0, 0}min, max = nums[0], nums[0]for _, n := range nums[1:] {min = smaller(min, n)max = larger(max, n)}return min, max}
The signature (min, max int) names the return values. Inside the function, min and max are pre-declared variables. Naming them makes the signature self-documenting: a reader knows at a glance which value is the minimum and which is the maximum without reading the function body.
Named returns also allow a bare return with no arguments (sometimes called a "naked return"). This returns the current values of the named variables. We do not use that form here because the explicit return min, max is clearer. Use named returns for the documentation value they add to signatures, but avoid bare returns except in very short functions.
Both MinMax and Average accept nums ...int. The ... before a type makes it variadic. This means the function accepts any number of arguments of that type. Inside the function, nums acts as a []int slice. You call a variadic function like this:
gomin, max := mathx.MinMax(7, 2, 9, 4, 1)
Or with no arguments at all:
goavg, err := mathx.Average()
If you already have a slice and want to spread it into a variadic call, append ... to the variable: mathx.MinMax(mySlice...).
Every .go file starts with a package declaration. The declaration names the package this file belongs to. Files in the same directory must all declare the same package name.
go-for-beginners/
├── main.go ← package main
├── mathx/
│ └── stats.go ← package mathx
└── textx/
└── format.go ← package textx
One directory, one package. That is the rule.
package main is special. It is the entry point of an executable program. A file in package main that contains func main() is where go run and go build start execution. Every other package in this project is a library package that main imports.
The module path is declared in go.mod:
module github.com/mt26691/go-for-beginners
To import mathx, you combine the module path with the package's directory:
go// main.goimport ("fmt""github.com/mt26691/go-for-beginners/mathx""github.com/mt26691/go-for-beginners/textx")
The import path is always the module path plus the package's directory path. It is not the package name (though they match here, they do not have to). Go uses the directory to find the package, not the package declaration.
After importing, you prefix each exported name with the package name:
gomin, max := mathx.MinMax(7, 2, 9, 4, 1)label := textx.Label(raw)
Go has no public or private keywords. Visibility is controlled by a single rule: an identifier that starts with a capital letter is exported, and one that starts with a lowercase letter is unexported (package-private).
That is the entire rule.

MinMax, Average, and ErrNoNumbers in mathx start with capitals, so main can call them. The helpers smaller and larger start with lowercase letters, so they are invisible outside mathx. Attempting to call mathx.smaller from main.go is a compile error.
The same applies in textx. Label is exported; squeeze is not:
go// textx/format.gofunc Label(raw string) string {cleaned := squeeze(strings.TrimSpace(raw))return titler.String(cleaned)}func squeeze(s string) string {return strings.Join(strings.Fields(s), " ")}
Label calls squeeze freely because they live in the same package. From outside textx, squeeze does not exist.
This rule applies to everything in Go: functions, variables, types, struct fields, and constants. Capital means exported, and lowercase means package-private.
mathx/stats.go imports only "errors" from the standard library. textx/format.go imports "strings" (also standard) plus two packages from golang.org/x/text:
go// textx/format.goimport ("strings""golang.org/x/text/cases""golang.org/x/text/language")
golang.org/x/text is an extended standard library package maintained by the Go team but not bundled with the language. We added it with go get:
bashgo get golang.org/x/text/casesgo get golang.org/x/text/language
Running those commands added a require line to go.mod and created go.sum:
require golang.org/x/text v0.38.0
go.sum holds cryptographic checksums for each downloaded module, so Go can verify the download is authentic and has not been tampered with. Both files belong in version control. You never edit them by hand.
strings.Title used to be the idiomatic way to title-case a string, but it was deprecated in Go 1.18 because it does not handle non-ASCII characters correctly. golang.org/x/text/cases is the maintained replacement:
go// textx/format.govar titler = cases.Title(language.English)func Label(raw string) string {cleaned := squeeze(strings.TrimSpace(raw))return titler.String(cleaned)}
Label chains three transformations: trim outer whitespace, collapse inner whitespace runs into a single space (via squeeze), then title-case the result. The input " go backend service " comes out as "Go Backend Service".
bashmake run
Output:
== Functions & Packages ==
-- Functions --
MinMax(7, 2, 9, 4, 1) = 1, 9
Average(7, 2, 9, 4, 1) = 4.60
Average() error: mathx: no numbers given
-- Packages --
Label(" go backend service ") = "Go Backend Service"
Every line matches exactly what the code produces. The error line for the empty Average() call shows the sentinel error message from ErrNoNumbers. The Label output shows the full transformation from messy whitespace to clean title case.
Next up is control flow: if, for, and switch. The if err != nil pattern you met here shows up everywhere in chapter 8, where writing it starts to feel like second nature.