A single file is enough for a greeting. As an application grows, related functions need a home that callers can understand. Go provides packages for organizing code and modules for managing a collection of packages and its dependencies. Understanding the difference makes import errors much less mysterious.
A source file belongs to a package through its package declaration. Ordinary source files in one directory belong to the same package. A module starts at the directory containing go.mod and can contain several package directories. An import path identifies a package, usually by combining the module path with its directory path. The module creation tutorial introduces this relationship.
Build a small package boundary
Create a fresh directory and initialize a module:
mkdir go-packages
cd go-packages
go mod init example.com/go-packages
Create the directories shown below, using your editor or file manager. There are two source files:
go-packages/
go.mod
main.go
internal/
greeting/
greeting.go
Save the reusable behavior in internal/greeting/greeting.go:
package greeting
import "strings"
func Message(name string) string {
name = strings.TrimSpace(name)
if name == "" {
name = "friend"
}
return "Hello, " + name + "!"
}
Save the executable entry point in main.go:
package main
import (
"fmt"
"example.com/go-packages/internal/greeting"
)
func main() {
fmt.Println(greeting.Message(" Asha "))
fmt.Println(greeting.Message(""))
}
From the module root, run:
go fmt ./...
go run .
go build ./...
The output is Hello, Asha! followed by Hello, friend!. Message accepts text, removes surrounding whitespace, chooses a fallback, and returns a result. It does not decide how to display that result. main handles printing. This separation lets a future HTTP handler reuse the greeting without changing its behavior.
Understand names and visibility
greeting.Message combines the imported package name and the function name. A name beginning with an uppercase letter is exported, meaning other packages can access it. A lowercase helper remains accessible only within its own package. Visibility applies to types, variables, and struct fields too.
The folder name and package name usually match. The import string is a path, while the identifier used in code comes from the package declaration unless you provide an import alias. Renaming a folder without updating its import path can therefore break a build even when the function itself is unchanged.
internal has a special import restriction: code outside the directory tree rooted at its parent cannot import that package. Here it keeps the greeting available to this project without making it an API for unrelated modules. It is a code organization rule, not encryption or a secret store. The module layout guide describes when this structure is useful.
Keep dependency files understandable
go.mod records the module path, Go language/toolchain requirements, and any required modules. Leave the generated Go directive in place. Do not replace it with a version copied from an unrelated tutorial. A go.sum file contains checksums used when verifying downloaded module content; this standard-library-only example may not create one.
Run go mod tidy after changing imports to add missing module requirements and remove unused ones. Commit go.mod and, when present, go.sum in a real project. Use go get with a deliberately selected dependency version when adding or updating a library; use go install with a version when installing a command. These are different jobs. Read managing dependencies before making dependency changes you do not understand.
Pitfalls when splitting code
Go rejects import cycles. If package A imports B, B cannot import A, directly or indirectly. Move shared concepts into a smaller package or reconsider which package owns the behavior. Avoid creating a package for every function; a package should represent a coherent responsibility.
Do not run go mod init in every subdirectory. That creates separate modules, which this example does not need. Also avoid naming packages utils by habit: a name such as greeting tells a caller what belongs there.
Exercise: add a farewell
Add an exported Farewell(name string) string function. It should trim whitespace, use friend for blank input, and return Goodbye, NAME!. Call it from main with both a normal and blank name.
Solution guidance
Extract the existing name-cleaning steps into a lowercase helper such as cleanName. Call that helper from both exported functions, then build each message with its own prefix. Keep the helper in the greeting package, where it expresses a shared rule. Expected results include Goodbye, Asha! and Goodbye, friend!; test whitespace-only input as well as an empty string.
Previous: Getting Started. Next: Variables, Types, and Control Flow.

