The 10-Second Fix
Go uses the casing of the first letter to determine visibility. If you encounter the cannot refer to unexported name error, you are trying to access an identifier that starts with a lowercase letter from outside its home package.
Capitalize the first letter of the function, variable, or type where it is defined to export it.
// In package 'auth' (auth/token.go)
func validateToken() { ... } // Private: error when called externally
func ValidateToken() { ... } // Public: fixed and exported
Once you rename the definition, update your call sites in other packages to use the new capitalized name. Most modern IDEs can handle this refactoring for you in seconds.
Why This Happens: Package-Level Scope
If you are coming from Java or C#, you might be looking for public or private keywords. Go does not use them. Instead, the language designers opted for a system based entirely on character casing:
- Upper Case (A-Z): Exported. This is public and accessible to any package that imports it.
- Lower Case (a-z): Unexported. This is private to the package (the directory) where it lives.
The compiler enforces these rules strictly. When you run go build, it checks every reference against these visibility rules. If it finds a lowercase identifier being used across package boundaries, it stops the build to protect the code's encapsulation.
Common Scenarios
1. Exporting Functions and Variables
You'll likely hit this while building utility packages. You might write a helper function and later realize you need it in your main application logic.
The Problem:
// internal/formatter/text.go
package formatter
func formatUserString(s string) string { ... }
// main.go
package main
import "myproject/internal/formatter"
func main() {
// Error: cannot refer to unexported name formatter.formatUserString
fmt.Println(formatter.formatUserString("hello"))
}
The Solution: Change formatUserString to FormatUserString. Go 1.23 and earlier versions all follow this exact rule without exception.
2. Private Struct Fields and JSON
Even if a struct is exported, its individual fields remain private if they start with a lowercase letter. This is a common stumbling block when working with the encoding/json package. Because the JSON marshal/unmarshal logic lives in an external package, it cannot see your private fields.
Broken Code:
// models/user.go
type User struct {
Name string
email string // private: cannot be seen by other packages
}
// main.go
u := models.User{Name: "Bob", email: "bob@example.com"} // Compiler Error
The Fix: Rename the field to Email. If you are serializing to an API, use struct tags to keep your JSON keys lowercase while keeping your Go fields exported:
type User struct {
Name string `json:"name"`
Email string `json:"email"` // Accessible and follows API naming conventions
}
3. Constants and Types
Constants and custom types follow the same rules. If you define a type duration int in a config package, you won't be able to use it as a function argument in your service layer unless you rename it to Duration.
The "Getter" Pattern for Read-Only Access
Exporting everything isn't always the best move. Sometimes you want other packages to read a value but never modify it. In these cases, keep the variable lowercase (private) but provide an exported function to return its value.
package db
// Private variable prevents accidental overwrites from other packages
var connectionString = "root:password@tcp(127.0.0.1:3306)/app_db"
// ConnectionString provides safe, read-only access
func ConnectionString() string {
return connectionString
}
Verification and Refactoring
To confirm the fix, run a full check on your project. Use go build ./... to compile every package in your module. If the command exits with no output, you've successfully resolved all visibility issues.
If you have hundreds of references to a private name, don't rename them manually. Use your editor's refactoring tools:
- VS Code: Place your cursor on the name and press
F2. - GoLand: Use
Shift + F6. - CLI: Use
gopls renamefor a programmatic approach.
These tools ensure every instance across your entire project is updated instantly, preventing the manual 'search and replace' fatigue that leads to new bugs.

