This concept becomes much easier once you remember one rule:
Starts with an uppercase letter → Exported
Starts with a lowercase letter → Unexported
In Go, this is how you control whether something can be accessed from another package.
1. First: What is an identifier?
An identifier is simply a name you give to something in Go.
Examples:
var age int
func PrintMessage() {}
type User struct {}
const MaxUsers = 100
Here, these are identifiers:
age
PrintMessage
User
MaxUsers
Go uses the first letter of an identifier to decide whether other packages can access it.
2. Exported vs Unexported
| Feature | Exported | Unexported |
|---|---|---|
| First letter | Uppercase | Lowercase |
| Example function | PrintMessage() | printMessage() |
| Example variable | Username | username |
| Example type | User | user |
| Example struct field | Name | name |
| Accessible inside same package | ✅ Yes | ✅ Yes |
| Accessible from another package | ✅ Yes | ❌ No |
| Similar idea in other languages | Public | Private-ish |
| Main purpose | Public package API | Internal implementation |
The easiest mental model is:
Uppercase = package exposes it
Lowercase = package keeps it internal
3. WHAT is an exported identifier?
An exported identifier is something that another Go package is allowed to access.
For example:
func SayHello() {
fmt.Println("Hello")
}
Because SayHello begins with uppercase S, another package can use:
greeting.SayHello()
4. WHAT is an unexported identifier?
An unexported identifier can normally only be used inside the package where it was declared.
For example:
func buildMessage() {
fmt.Println("Building message")
}
Because buildMessage begins with lowercase b, another package cannot do:
greeting.buildMessage()
That would produce a compile error.
5. WHY does Go have this?
Packages need a way to separate:
Things users should use
from
Things used internally by the package
Imagine a package like this:
greeting
│
├── SayHello() ← users should call this
├── NewGreeting() ← users should call this
│
├── buildMessage() ← internal helper
└── validateName() ← internal helper
There is usually no reason for users of the package to know about internal functions such as buildMessage().
Go therefore lets the package expose only its intended API.
6. HOW does Go decide?
The rule is extremely simple.
flowchart TD
A[Identifier] --> B{First letter uppercase?}
B -->|Yes| C[Exported]
B -->|No| D[Unexported]
C --> E[Other packages can access it]
D --> F[Other packages cannot access it]
For example:
User
Print
MaxConnections
Name
are exported.
But:
user
print
maxConnections
name
are unexported.
7. Complete Self-Contained Example
Because exported/unexported identifiers are mainly about communication between packages, the best example needs two packages.
We will create this tiny project:
exportdemo/
│
├── go.mod
├── main.go
│
└── greeting/
└── greeting.go
Step 1: go.mod
Create:
go.mod
with:
module example.com/exportdemo
go 1.22
This simply gives our Go project a module name.
Step 2: greeting/greeting.go
Create a directory named:
greeting
Inside it create:
greeting.go
with:
package greeting
// Message is exported because it starts with uppercase M.
// Other packages can access it.
var Message = "Hello from the greeting package"
// secretMessage is unexported because it starts with lowercase s.
// Only code inside package greeting can access it.
var secretMessage = "This is a secret message"
// SayHello is exported because it starts with uppercase S.
// Other packages can call it.
func SayHello() string {
return secretMessage
}
// buildMessage is unexported because it starts with lowercase b.
// Only code inside package greeting can call it.
func buildMessage() string {
return "Message created internally"
}
Notice something important here.
SayHello() can access:
secretMessage
even though secretMessage is unexported.
Why?
Because both are inside:
package greeting
Unexported does not mean:
only this function can access it
It means:
only this package can access it
8. main.go
Create:
main.go
with:
package main
import (
"fmt"
"example.com/exportdemo/greeting"
)
func main() {
// Message starts with uppercase M.
// Therefore it is exported from package greeting.
fmt.Println(greeting.Message)
// SayHello starts with uppercase S.
// Therefore we can call it from package main.
fmt.Println(greeting.SayHello())
// The following DOES NOT work because secretMessage
// starts with lowercase s.
//
// fmt.Println(greeting.secretMessage)
// This also DOES NOT work because buildMessage
// starts with lowercase b.
//
// fmt.Println(greeting.buildMessage())
}
9. Run the program
From the exportdemo directory:
go run .
Output:
Hello from the greeting package
This is a secret message
10. What exactly happened?
Our program has two packages:
main
greeting
Inside greeting we declared:
Message
secretMessage
SayHello
buildMessage
Their visibility is:
| Identifier | Starts With | Exported? | Accessible from main? |
|---|---|---|---|
Message | M uppercase | ✅ | ✅ |
secretMessage | s lowercase | ❌ | ❌ |
SayHello | S uppercase | ✅ | ✅ |
buildMessage | b lowercase | ❌ | ❌ |
So this works:
greeting.Message
and this works:
greeting.SayHello()
But this doesn’t:
greeting.secretMessage
and this doesn’t:
greeting.buildMessage()
11. An important point: unexported does NOT mean inaccessible everywhere
Suppose we have:
package greeting
var secretMessage = "Secret"
func SayHello() string {
return secretMessage
}
This is perfectly valid.
SayHello() and secretMessage belong to the same package.
Therefore:
SayHello()
can use:
secretMessage
The restriction only becomes important when another package tries to access it.
Think of the package as a house:
Package greeting
┌──────────────────────────────┐
│ │
│ Message ← exported │
│ SayHello() ← exported │
│ │
│ secretMessage ← internal │
│ buildMessage ← internal │
│ │
└──────────────────────────────┘
↑
│
Package main can only
access exported names
12. Struct fields follow the same rule
This rule is not limited to functions.
Consider:
type User struct {
Name string
age int
}
Here:
Name
is exported.
But:
age
is unexported.
Therefore another package can do:
user.Name
but cannot do:
user.age
Again:
Name → uppercase → exported
age → lowercase → unexported
13. Functions follow the same rule
func StartServer() {}
Exported:
StartServer
↑
uppercase
Another package can call it.
But:
func startServer() {}
is unexported.
14. Types follow the same rule
Exported:
type User struct{}
Unexported:
type user struct{}
15. Constants and variables follow the same rule
Exported:
const MaxUsers = 100
var ServerName = "production"
Unexported:
const maxUsers = 100
var serverName = "production"
16. Methods follow the same rule
Exported:
func (u User) PrintName() {
}
Unexported:
func (u User) printName() {
}
17. One rule applies everywhere
You do not need separate rules for variables, functions, structs, methods, and constants.
Remember this:
| Code | Exported? |
|---|---|
User | ✅ |
user | ❌ |
Print() | ✅ |
print() | ❌ |
Name | ✅ |
name | ❌ |
MaxSize | ✅ |
maxSize | ❌ |
Connect() | ✅ |
connect() | ❌ |
18. WHEN should you export something?
Export something when users of your package are supposed to use it.
For example:
package database
func Connect() {
}
If another package needs to connect to the database:
database.Connect()
then Connect needs to be exported.
19. WHEN should you keep something unexported?
Use lowercase names for implementation details.
For example:
package database
func Connect() {
validateConfig()
openConnection()
}
func validateConfig() {
}
func openConnection() {
}
Other packages only need:
database.Connect()
They don’t need:
validateConfig()
openConnection()
So those helpers remain unexported.
This gives you a clean package API.
20. WHERE will you see this in real Go programs?
Very frequently.
Consider the standard fmt package.
You write:
fmt.Println("Hello")
Notice:
Println
P
↑
uppercase
Println is exported by the fmt package.
Similarly:
http.ListenAndServe(...)
ListenAndServe begins with uppercase L, so the net/http package exposes it.
This naming convention is fundamental to how Go packages work.
21. One subtle point: main is lowercase
You may wonder:
func main() {
}
Why isn’t it:
func Main() {
}
main() is special.
Go specifically looks for:
package main
func main()
as the program’s entry point.
It isn’t designed to be exported and called by other packages.
So:
main
being lowercase is intentional.
22. Exported is similar to public
If you know languages such as Java, C#, C++, or TypeScript, you can roughly think:
Go Java-like idea
SayHello() ≈ public
sayHello() ≈ package internal
But Go does not require keywords such as:
public
private
protected
Instead, Go uses naming.
Java-style idea:
public void SayHello() {
}
Go:
func SayHello() {
}
The uppercase S does the job.
23. Final Comparison
| Question | Exported | Unexported |
|---|---|---|
| What? | Visible outside package | Hidden from other packages |
| How? | First letter uppercase | First letter lowercase |
| Why? | Create public package API | Hide implementation details |
| When? | Other packages need it | Only your package needs it |
| Where? | Functions, types, fields, methods, variables, constants | Same |
| Example | Connect() | connect() |
| Other package can use? | ✅ Yes | ❌ No |
| Same package can use? | ✅ Yes | ✅ Yes |
24. The 10-second rule to remember
Whenever you see:
greeting.SayHello()
ask:
Does
SayHellostart with an uppercase letter?
Yes:
SayHello
↑
S
Therefore it is exported.
Whenever you see:
sayHello
lowercase:
sayHello
↑
s
Therefore it is unexported.
So remember:
Uppercase → Exported → Other packages can use it
lowercase → unexported → Only its package can use it
That single rule will handle almost every beginner-level exported/unexported identifier question you encounter in Go.