Visibility in Go - Demystifying Uppercase and Lowercase Identifiers
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Go's simplicity extends to its visibility rules, which are remarkably straightforward and elegant. Unlike languages that rely on explicit keywords like public
, private
, or protected
, Go leverages a convention that is deeply ingrained in its identifier naming: the casing of the first letter. This seemingly trivial detail—whether an identifier begins with an uppercase or lowercase letter—is the sole determinant of its visibility. This article will delve into the nuances of these rules, illustrating how they govern the accessibility of various Go constructs.
The Core Principle: Exported vs. Unexported
At its heart, Go defines two levels of visibility for identifiers:
- Exported (Public): An identifier starting with an uppercase letter is "exported." This means it is visible and accessible from outside the package in which it is declared.
- Unexported (Private/Package-Local): An identifier starting with a lowercase letter is "unexported." This means it is only visible and accessible within the package in which it is declared. It cannot be accessed from outside that package.
This rule applies universally to all top-level declarations:
- Variables
- Constants
- Functions
- Types (structs, interfaces, etc.)
- Struct fields
- Interface methods
Let's break this down with examples.
Package-Level Visibility
Consider a scenario where you have a package named geometry
.
// geometry/shapes.go package geometry import "fmt" // Circle is an exported struct representing a circle. type Circle struct { Radius float64 // Radius is an exported field color string // color is an unexported field } // area calculates the area of a circle. It's unexported. func area(r float64) float64 { return 3.14159 * r * r } // NewCircle is an exported constructor function. func NewCircle(radius float64, c string) *Circle { return &Circle{Radius: radius, color: c} } // GetArea is an exported method that returns the area of the circle. func (c *Circle) GetArea() float64 { // Can access private field 'color' and unexported function 'area' // because they are within the same package. fmt.Printf("Calculating area for a %s circle.\n", c.color) return area(c.Radius) } // privateConstant is an unexported constant. const privateConstant = "This is private to the geometry package." // ExportedConstant is an exported constant. const ExportedConstant = "This is public to the geometry package." // privateVar is an unexported package-level variable. var privateVar = 10 // PublicVar is an exported package-level variable. var PublicVar = 20
Now, let's see how another package, say main
, interacts with geometry
.
// main.go package main import ( "fmt" "your_module/geometry" // Replace your_module with your actual module path ) func main() { // Accessing exported types, functions, and constants c := geometry.NewCircle(5.0, "red") fmt.Println("Circle Radius:", c.Radius) // OK: Radius is exported fmt.Println("Circle Area:", c.GetArea()) // OK: GetArea is exported // fmt.Println("Circle Color:", c.color) // COMPILE ERROR: c.color is unexported // fmt.Println(geometry.area(10)) // COMPILE ERROR: area is unexported // fmt.Println(geometry.privateConstant) // COMPILE ERROR: privateConstant is unexported // fmt.Println(geometry.privateVar) // COMPILE ERROR: privateVar is unexported fmt.Println("Exported Constant:", geometry.ExportedConstant) // OK: ExportedConstant is exported fmt.Println("Public Variable:", geometry.PublicVar) // OK: PublicVar is exported // Creating a Circle directly. Only exported fields can be set directly. // We can set Radius but not color. // If we need to set color, we must use an exported method or constructor. c2 := geometry.Circle{Radius: 7.0} // OK to set exported fields // c3 := geometry.Circle{radius: 7.0} // COMPILE ERROR: radius is unexported (even though the field name is Radius) // c4 := geometry.Circle{color: "blue"} // COMPILE ERROR: color is unexported fmt.Println("Circle2 Radius:", c2.Radius) // An important note: Even if you create a struct directly using a composite literal, // you can only initialize exported fields from outside the package. // You cannot directly set an unexported field using a composite literal // if the literal is in a different package. }
The error messages you would get for attempting to access unexported identifiers would be similar to:
c.color undefined (cannot refer to unexported field or method color)
geometry.area undefined (cannot refer to unexported name geometry.area)
Why This Design?
Go's approach to visibility offers several advantages:
- Simplicity and Readability: The rule is exceptionally simple to remember and apply. There's no need to learn multiple keywords or complex access modifiers. A glance at an identifier reveals its visibility.
- Explicit Intent: By convention, when you write
Foo
you are explicitly stating its public nature, andfoo
implies internal usage. This reinforces good API design. - Encourages Better Design: It subtly encourages developers to think about what parts of their code need to be exposed as an API and what should remain an implementation detail. This inherently leads to better encapsulation and modularity.
- Enforces Encapsulation: Unexported elements act as internal components of a package, allowing the package maintainer to refactor or change their implementation without affecting external users. Only the exported API is part of the "contract."
Specific Cases and Best Practices
Struct Fields
As shown in the Circle
example, individual fields within a struct
also follow these rules. This allows for fine-grained control over which parts of a data structure are accessible from outside the package. Often, unexported fields are used for internal state that is managed via exported methods (e.g., NewCircle
to initialize color
and GetArea
to use it).
Interface Methods
Similar to struct fields, methods declared within an interface also follow the visibility rules. If an interface method begins with an uppercase letter, it's an exported method, meaning any type implementing this interface must provide an exported method with that signature.
// geometry/shapes.go (continued) package geometry // Shape is an exported interface type Shape interface { Area() float64 // Exported method perimeter() float64 // Unexported method - This is possible, but less common for // interfaces intended for external use, as external types // implementing this would also need an unexported 'perimeter' method, // which they can only provide if they are in the *same* package. // This is more often used for internal interfaces. } // Square implements the Shape interface (hypothetically) type Square struct { side float64 } func (s *Square) Area() float64 { // Must be exported to satisfy Shape's exported Area() return s.side * s.side } func (s *Square) perimeter() float64 { // Must be unexported to satisfy Shape's unexported perimeter() return 4 * s.side }
Constructor Functions
It's a common Go idiom to have unexported struct types, with one or more exported functions that act as "constructors" to create instances of these types. This allows the package to maintain strict control over how its types are instantiated and initialized.
// internal/user.go package internal // user is an unexported struct representing a user. type user struct { id string name string } // NewUser is an exported constructor function for the 'user' type. func NewUser(id, name string) *user { return &user{id: id, name: name} } // GetName is an exported method to access the user's name. func (u *user) GetName() string { return u.name } // privateMethod is unexported. func (u *user) privateMethod() { // ... do something internal ... }
// main.go package main import ( "fmt" "your_module/internal" ) func main() { // u := internal.user{id: "123", name: "Alice"} // COMPILE ERROR: user is unexported u := internal.NewUser("456", "Bob") // OK: NewUser is exported fmt.Println("User Name:", u.GetName()) // OK: GetName is exported // fmt.Println("User ID:", u.id) // COMPILE ERROR: u.id is unexported (even through u itself is a pointer to an unexported type) // u.privateMethod() // COMPILE ERROR: privateMethod is unexported }
Here, internal.user
is entirely hidden from main
. The main
package can only interact with it through the exported NewUser
function and GetName
method. This provides robust encapsulation.
Naming Conventions and Context
While the casing rule is strict, the meaning of "public" or "private" is always relative to the package boundary. A variable tempCount
might be unexported within package metrics
, invisible externally. But if its value is returned by an exported function metrics.GetMetricCount()
, then conceptually, its value becomes "public" through the function. This further emphasizes that Go's visibility design guides API surfaces, not just raw data access.
Conclusion
Go's visibility rules, dictated solely by the initial letter's casing (uppercase for exported, lowercase for unexported), are a cornerstone of its design philosophy: simplicity, clarity, and convention over configuration. This elegant mechanism promotes good software architecture by making encapsulation the default and requiring explicit intent for API exposure. Understanding and adhering to these rules is fundamental to writing idiomatic, maintainable, and robust Go applications, ensuring that package internals remain isolated while stable APIs are clearly defined.