Welcome to Chapter 7! Up until now, your code has lived entirely inside a single main.go file. That is perfect for small experiments, but real-world apps grow quickly.
To build clean, maintainable software, you need two superpowers:
In this guide, you will learn how to write advanced functions, create your own packages, pull in external libraries, and master the single rule Go uses to handle code visibility.
We are going to take our code and split it into two brand-new library packages: mathx and textx. Then, we will call them both from our main file, main.go.
mathx will teach you how to handle parameters, multiple returns, and flexible inputs.textx will show you how to pull in and use outside libraries using go get.Want to jump straight to the finished code? Run this in your terminal:
git checkout 07-functions-and-packages-finish
Every basic Go function follows this exact blueprint: func name(parameters) returnType.
gofunc add(a, b int) int {return a + b}
a, b int? When neighboring parameters share the exact same type, you only have to write the type once at the end. It saves you from writing a int, b int.Unlike many programming languages, Go functions can throw back more than one value at the same time. This is used everywhere in Go—especially for error handling.
Let's look at a function that calculates an average, but might fail if you give it no numbers:
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}
When you call a function that returns multiple things, you catch them all at once using :=:
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 Most Common Pattern in Go: The
if err != nilcheck is the golden rule of Go. If a function might fail, it gives you a result and an error. Iferrisnil, everything went perfectly. Iferris notnil, something broke, the result is junk, and you need to handle the issue.
If you pass no inputs to Average(), it triggers an error. Because the calculated number is meaningless during an error, we can throw it away using the blank identifier (_):
go// main.go_, err = mathx.Average() // We use '_' because we don't care about the float64 hereif err != nil {fmt.Println("Average() error:", err)}
You can also give your return values actual names directly inside the function signature:
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 // Explicitly returning variables is clearest!}
(min, max int) knows exactly what those two returned numbers represent without reading the actual logic.return without specifying variables. However, unless the function is tiny, it's always clearer to explicitly write return min, max.Look closely at the nums ...int parameter inside MinMax and Average. Those three dots (...) mean: "Accept as many arguments as the user wants to pass!"
You can call it with five numbers, zero numbers, or a hundred numbers:
gomin, max := mathx.MinMax(7, 2, 9, 4, 1)avg, err := mathx.Average()
Inside the function, Go automatically packages those inputs up into a standard slice ([]int) for you to loop through.
... to the end of your slice variable to unpack it: mathx.MinMax(mySlice...).Think of a package as a labeled storage box. Everything inside that box can see and share code freely, but outside code can only use what the box explicitly allows.
.go file sitting inside the same folder must declare the exact same package name at the top of the file.mathx/ folder contains package mathx) makes your project incredibly easy to navigate.Here is how our project directories are structured:

package main is special. It tells Go that this isn't just a library—it's the entry point where the program executes func main().To map your packages, Go relies on two pieces of information:
| Term | What it is | Example |
|---|---|---|
| Module Path | The unique base "address" of your entire project (set up inside go.mod). | github.com/mt26691/go-for-beginners |
| Import Path | The base Module Path + the specific subfolder your package lives in. | github.com/mt26691/go-for-beginners/mathx |
To use your custom libraries inside main.go, point your import blocks directly to their Import Paths:
go// main.goimport ("fmt""github.com/mt26691/go-for-beginners/mathx""github.com/mt26691/go-for-beginners/textx")
Once imported, call their functions by prefixing them with the package name:
gomin, max := mathx.MinMax(7, 2, 9, 4, 1)label := textx.Label(" go clean code ")
Go completely skips keywords like public or private. Instead, access control comes down to a single visual rule:
Capital Letter = Exported (Public). Other packages are allowed to use it.
Lowercase Letter = Unexported (Private). Only files inside the same package can use it.

| Identifier Name | Type | Visibility | Can main.go see it? |
|---|---|---|---|
MinMax | Function | Capital ➔ Exported | Yes |
Average | Function | Capital ➔ Exported | Yes |
ErrNoNumbers | Variable | Capital ➔ Exported | Yes |
smaller | Function | lowercase ➔ Unexported | No (Compile Error!) |
squeeze | Function | lowercase ➔ Unexported | No (Compile Error!) |
This rule is universal across Go. It applies to variables, functions, types, constants, and custom fields.
Let's look at textx/format.go to see this in action:
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 can call squeeze easily because they both live inside the textx package box.textx.squeeze() inside main.go, Go will throw a compilation error because it starts with a lowercase letter.While Go has an incredible built-in standard library (like "strings" and "errors"), you will eventually want to use code written by other developers.
For instance, the built-in strings.Title was deprecated in Go 1.18 because it didn't handle non-ASCII characters well. The Go team provides a superior tool inside the external package golang.org/x/text.
Open your terminal and run go get followed by the package path:
bashgo get golang.org/x/text/casesgo get golang.org/x/text/language
Running these commands does two things automatically:
go.mod Updates: Adds a require tracking line showing the exact version downloaded.go.sum Creation: Generates a file containing secure cryptographic checksums to guarantee your downloads are safe and haven't been modified maliciously.Note: Both of these files are managed by Go. Always commit them to version control, and never edit them by hand.
Now we can use the advanced casing library securely inside textx/format.go:
go// textx/format.goimport ("strings""golang.org/x/text/cases""golang.org/x/text/language")var titler = cases.Title(language.English)func Label(raw string) string {cleaned := squeeze(strings.TrimSpace(raw)) // Trims ends, collapses double spacesreturn titler.String(cleaned) // Properly title-cases strings}
Now that our code is neatly split across main, mathx, and textx, run your application:
bashmake run

Look at how beautifully it works! Our empty Average() call safely surfaces our sentinel package error (mathx: no numbers given), and our Label call elegantly cleans up the messy spaced string into a clean title.
Now that you know how to package code and handle errors, we are ready to dive into control flow: if, for, and switch statements. Since you have already practiced the if err != nil pattern, the next chapter will feel like a natural next step!