1. Packages and namespaces
When you write your first programs, a single main.go file looks fine. But a couple of days pass, you end up with a dozen functions, a couple of constants, a few variables—and suddenly the file starts to resemble a closet where you “just for a minute” put some stuff, then “just for a minute” put some more, and now you’re basically living inside the closet.
A package in Go solves exactly this problem: it lets you group code by meaning and provides rules for who can “see” and use what. As a result, your project starts to look like a neat kitchen with drawers rather than a pot where there are spoons, socks, and the mystery of “why does it work”.
Package as a namespace: “the name before the dot” is not magic
If you’ve already written fmt.Println("hello"), then you’ve seen the most important symptom of packages: the dot.
fmt.Println is read like this: “in the fmt package, take the exported name Println”. That is, a package is literally a namespace that protects you from a situation where your program has 15 functions named Print() and you have no idea which one is “the one”.
You can imagine it like this:
- a package is a family name,
- a function/type/variable is a given name,
- and fmt.Println is “Petrov Ivan”, just for code.
And the nice part: in Go this is not just a convention—it’s a real language mechanism.
The package ... line: how a file belongs to a package
Any .go file (except for special cases we are not touching today) starts with a package line: package <name>.
It’s important to catch this idea: a file by itself is not a “unit of the program”. The unit is the package, and a file is just a piece of a package.
Here is a minimal example:
package main
import "fmt"
func main() {
fmt.Println("hi") // hi
}
The line package main means: “this file belongs to the main package”.
Now the key practical fact that saves hours of your life: all files with the same package in the same folder are considered one package. Which means they share a common package-level scope.
Package-level scope: visibility inside a package
Here’s an important moment: inside one package, all files see each other’s declarations. Not “if you import”, not “if you include”, they just see them—because they are one package.
Let’s continue our learning mini-program. Let it be the simplest “day balance”: we read income and expense, compute the difference, and print a report. Previously you’d write everything in main.go. Today we’ll start splitting the code into files, but we’ll keep a single package for now: main.
File main.go
In this file we’ll have the “script”: read, compute, output.
package main
import "fmt"
func main() {
income, expense := readIncomeExpense()
balance := income - expense
fmt.Println(formatReport(income, expense, balance))
}
Notice: readIncomeExpense and formatReport are not declared here. And that’s fine—they can live in other files of the same package.
File io.go
Now we make input reading a separate function, in a separate file, but in the same package:
package main
import "fmt"
func readIncomeExpense() (int, int) {
var income int
var expense int
fmt.Scan(&income, &expense)
return income, expense
}
File report.go
And we’ll also move report formatting out separately:
package main
import "fmt"
func formatReport(income, expense, balance int) string {
return fmt.Sprintf("income=%d expense=%d balance=%d", income, expense, balance)
}
The point is that all three files are parts of a single package main, so they can freely call each other’s functions. This is exactly package-level scope: scope at the package level.
To make this stick, here is a diagram:
flowchart TD
A[Project folder] --> B[main.go package main]
A --> C[io.go package main]
A --> D[report.go package main]
B --> E[One package: main]
C --> E
D --> E
E --> F[Shared package-level names: readIncomeExpense, formatReport, ...]
If you caught yourself thinking “so a file is not a boundary?”, then yes: the boundary is the package, not the file. And this is very Go-ish: fewer entities, less confusion.
2. package main and the entry point
Usually a package is just a container for code. But main is a special case: a program starts from the main package, and the entry point is the main() function.
You can put it like this: if a package is a “box of parts”, then package main is the box used to build an executable program, not a library.
One more useful fact: when Go analyzes a program, it considers entry points for each main package—among them is the main function, and there are also package initialization mechanisms.
We’ll deal with initialization separately; for today it’s enough to keep the idea: main is the “entry into the app”, and it typically contains the “glue” for the scenario.
3. Export: public and internal API
Exported and unexported names: the first-letter rule
Now—the central topic of the lecture, the reason we started all of this.
Go has a very simple rule:
- a name starts with an uppercase letter → it is exported and available from other packages;
- a name starts with a lowercase letter → it is unexported and available only inside its own package.
And this applies to functions, variables, constants, types (and later—also methods and struct fields).
Example from the standard library: fmt.Println vs “what if fmt.println?”
Let’s “touch” this rule directly:
package main
import "fmt"
func main() {
fmt.Println("ok") // ok
// fmt.println("no") // compilation error: no such exported name
}
Why does Println with a capital letter exist, while println with a lowercase letter does not? Because the fmt package exports Println, meaning it makes it part of the public API.
A small table so your brain doesn’t pretend it “already got it”
| Name | Example | Available from another package? | Why |
|---|---|---|---|
| Exported | |
Yes | First letter is uppercase |
| Unexported | |
No | First letter is lowercase |
Yes, it looks too simple to be true. But that’s really how it works. And that’s one reason people like Go: fewer rules, fewer surprising surprises.
Export: inside the package and outside
It’s easy to get confused here: “if I have formatReport starting with a lowercase letter, does that mean main.go won’t be able to call it?”. It will, because main.go and report.go are in the same package.
Export/unexport affects only access between packages. Inside one package, both kinds of names are available.
Let’s improve our code and make formatReport an internal detail, while exposing a more “human” function (in the future). While we’re in one package there will be no difference in access, but the meaning will appear.
File report.go (version with an internal function)
package main
import "fmt"
func formatReport(income, expense, balance int) string {
return fmt.Sprintf("income=%d expense=%d balance=%d", income, expense, balance)
}
This is an unexported function—an “internal helper”.
File main.go (uses the internal function)
package main
import "fmt"
func main() {
income, expense := readIncomeExpense()
balance := income - expense
fmt.Println(formatReport(income, expense, balance))
}
It works because it’s one package.
But if we tried to call formatReport from another package, it would be unavailable. That’s the whole point of the “lowercase letter”: “don’t touch, this is internal kitchen stuff”.
Export is a contract, not “secrecy”
It’s tempting to think “unexported” is about security. It’s not: it’s primarily about a contract and promises.
When you export a name, it’s like telling the outside world: “You can use this. I’ll try not to break it without a good reason.” So a package’s public API is the set of exported identifiers: constants, types, variables, functions, and also (when we get to structs) exported fields and methods.
A very practical design rule follows from that, even for a beginner: don’t export everything. If you make every tiny helper function public, you’ll turn your package into a Saturday market: it has everything, but finding what you need is impossible—and the sellers (that is, you a week later) don’t even remember what is where.
To feel the idea, imagine you’re making a “balance calculator” package (in the future we’ll learn to structure such things as separate packages). Things like ComputeBalance(...) belong in the public contract, but parseIncomeFromWeirdLegacyFormat(...) is better kept internal: it’s needed, but it must not become an “obligation” for external code.
4. Naming rules and a small refactoring
Naming rules: what to call things
Names in Go are not just “beauty”. Because of packages and export, names become part of the architecture. And if a name is chosen poorly, not only your eyes start to hurt—imports, reading code, and everything else suffers too.
Let’s start with the most common beginner rake: naming variables after packages. It’s especially annoying because the bug looks like “it worked yesterday, it doesn’t work today”, and the cause is just one line.
Example: how to accidentally “break fmt”
package main
import "fmt"
func main() {
fmt.Println("before") // before
fmt := "oops"
_ = fmt
// fmt.Println("after") // error: fmt is now a variable, not a package
}
Inside main you created a variable named fmt, and it “shadowed” the fmt package in this scope. Yes, the compiler honestly saves you and won’t let you build the program, but it won’t fix your mood.
The second important principle: exported names should be simple and predictable. If you make a name public, it becomes part of the API, which means people will read it. And people are delicate creatures: if it’s inconvenient, they move to another package.
Mini-table “more readable” vs “less readable”
| Intent | Better | Worse | Why |
|---|---|---|---|
| Public function | |
|
In Go it’s not customary to shout in ALL CAPS |
| Internal helper | |
|
_ is rarely used in names |
| Local variable | |
|
i is fine in a loop, but not for meaning |
Readability is highly valued in Go, and packages reinforce that value: a name is visible not only in a file, but also “through the dot” in other places.
Micro-practice: hide details and build an API
To reinforce “export = contract”, let’s do a small refactoring in our mini-app. We’ll add a function that will be the “main one” for the report, and we’ll hide internal details behind lowercase names.
File report.go (public + internal)
package main
import "fmt"
func PrintBalanceReport(income, expense int) string {
balance := income - expense
return formatReport(income, expense, balance)
}
func formatReport(income, expense, balance int) string {
return fmt.Sprintf("income=%d expense=%d balance=%d", income, expense, balance)
}
We’re still in the main package, so this is more of a “thought exercise”. But it’s important: you start designing an API even inside one package, so that later it’s easier to extract code into separate packages (when the time comes).
File main.go (use the public function)
package main
import "fmt"
func main() {
income, expense := readIncomeExpense()
fmt.Println(PrintBalanceReport(income, expense)) // income=... expense=... balance=...
}
From a code-reading point of view, it became nicer: main now knows less about the report’s details.
5. Typical mistakes
Mistake #1: thinking that “export” affects access between files of the same package.
This is a common confusion after languages where the file is an important boundary. In Go, the boundary is the package. If two files are declared as package main and are in the same folder, they are in one package and see each other at the package-level declarations. Export controls access specifically between packages, not between files.
Mistake #2: trying to call fmt.println or “guess” a name inside a package.
A beginner sometimes reasons like: “Well, Println prints, so println should also print.” It shouldn’t. In Go, access to names from another package is determined by export: if a name is unexported, you won’t see it. The right reaction is to open the package documentation or your IDE hints, not to guess “what it’s called there”.
Mistake #3: exporting everything “just in case”.
When you use an uppercase letter, you are (essentially) publicly promising to support that name. In large projects, a public API is a serious responsibility: it’s harder to change, and it must be maintained. So it’s better from the start to get used to exporting only what is truly a clear entry point, and keeping all helper stuff inside the package. The idea that public APIs consist of exported package identifiers is fundamental in Go.
Mistake #4: “breaking a package” by using a variable with the same name (for example, fmt := ...).
It looks like a small thing, but it breaks readability and causes very annoying compilation errors. If you imported a package, don’t use its name for variables. If your hand reaches for fmt := ..., it’s better to stop and choose a meaningful name: msg, line, text, report.
Mistake #5: confusing the meaning of package main with regular packages.
Sometimes you want to write “package main everywhere so it works”. Sure, it will work… for a while. But main is the application package, the entry point, the place for “glue”. In real code, you almost always want most of the logic to live outside main in other packages, and for main to only assemble everything. While we learn on small examples, main is fine, but it’s important not to turn it into a “dump”.
GO TO FULL VERSION