Go Tutorial: Exported and Unexported Identifiers

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

FeatureExportedUnexported
First letterUppercaseLowercase
Example functionPrintMessage()printMessage()
Example variableUsernameusername
Example typeUseruser
Example struct fieldNamename
Accessible inside same package✅ Yes✅ Yes
Accessible from another package✅ Yes❌ No
Similar idea in other languagesPublicPrivate-ish
Main purposePublic package APIInternal 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:

IdentifierStarts WithExported?Accessible from main?
MessageM uppercase
secretMessages lowercase
SayHelloS uppercase
buildMessageb 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:

CodeExported?
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

QuestionExportedUnexported
What?Visible outside packageHidden from other packages
How?First letter uppercaseFirst letter lowercase
Why?Create public package APIHide implementation details
When?Other packages need itOnly your package needs it
Where?Functions, types, fields, methods, variables, constantsSame
ExampleConnect()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 SayHello start 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.

Related Posts

Go Tutorials: Go Methods

Go methods become much easier once you understand one idea: A method is simply a function attached to a type. And the important rule is: A receiver…

Read More

Go Tutorials: Modules, Packages, Subpackages, Submodules, and Workspaces

This tutorial gives you one complete mental model for: The most important idea is: And sometimes: These two designs are very different. 1. What is a Go…

Read More

Go Tutorials: Numeric Types Beginner Tutorial

Go has four main families of numeric types: The simplest way to remember them is: Type family Stores Example int Whole numbers, positive or negative -10, 0,…

Read More

Go Tutorials: Go Testing, Benchmarking, and Profiling

These three topics become much easier once you separate the questions they answer: Topic Main question Main Go tool Result Testing Does my code work correctly? go…

Read More

Go Tutorials: Generics

1. What are Generics? Generics let you write one piece of code that works with multiple types while keeping Go’s compile-time type safety. Suppose you want a…

Read More

Go Concurrency Management Made Simple

This tutorial covers: The goal is: One concept → one purpose → one complete runnable example. Every example is independent. Save any example as: and run: 0….

Read More