Fix

"cannot refer to unexported name"

Fix the 'cannot refer to unexported name' error by capitalizing the first letter of the identifier to make it public.

When the compiler blocks your function call

You are building a package to handle database connections. You write a helper function parseDSN in conn.go to split the connection string. You switch to pool.go in the same directory, import your package, and try to call dbpool.parseDSN. The compiler rejects the build. You stare at the code. The function is right there. It works in the test file. It just won't work from the pool code.

The error is not a bug. It is a feature. Go uses capitalization to enforce visibility boundaries. The compiler is protecting your package design by hiding implementation details from the outside world.

Capitalization controls visibility

Go does not use keywords like public or private. The language relies on a single rule: the first letter of an identifier determines its visibility. If a name starts with an uppercase letter, it is exported. Exported names are visible to any package that imports the defining package. If a name starts with a lowercase letter, it is unexported. Unexported names are visible only to code inside the same package.

This rule applies to everything. Functions, variables, constants, types, and struct fields all follow the same convention. The compiler enforces this at build time. There is no runtime overhead. The code simply does not exist in the compiled binary for external access.

This design keeps the language simple. You do not need to remember special keywords. You think about visibility every time you name something. It also forces discipline. When you export a name, you are making a promise to users of your package. You cannot change the signature without potentially breaking their code. Unexported names give you freedom to refactor internals without affecting the public API.

Minimal example

The following code shows the difference between exported and unexported functions. The package mypkg defines two functions. One starts with a capital letter. The other starts with a lowercase letter.

// mypkg/mypkg.go
package mypkg

// PublicFunc is exported and visible to other packages.
// It returns a greeting string.
func PublicFunc() string {
    return "Hello from mypkg"
}

// privateFunc is unexported and visible only within mypkg.
// It contains implementation details.
func privateFunc() string {
    return "Secret logic"
}

When you import mypkg in another package, the compiler allows access to PublicFunc but rejects privateFunc.

// main/main.go
package main

import (
    "fmt"
    "myproject/mypkg"
)

// main runs the program and demonstrates visibility rules.
func main() {
    // PublicFunc starts with a capital letter.
    // The compiler allows this call because the name is exported.
    fmt.Println(mypkg.PublicFunc())

    // privateFunc starts with a lowercase letter.
    // Uncommenting this line triggers a compile error.
    // fmt.Println(mypkg.privateFunc())
}

If you uncomment the call to privateFunc, the compiler rejects the program with cannot refer to unexported name mypkg.privateFunc. The error is immediate and precise. It tells you exactly which name is hidden.

Capitalization is the lock. The compiler is the guard.

How the compiler checks boundaries

When you run go build, the compiler resolves every identifier in your code. It starts with the current package. If an identifier is not found locally, it checks imported packages. For each import, the compiler loads the symbol table. The symbol table lists all exported names. Unexported names are omitted from the table.

When the compiler sees mypkg.privateFunc, it looks up mypkg in the symbol table. It finds PublicFunc. It does not find privateFunc. The lookup fails. The compiler emits the error and stops. This happens during the compilation phase. No binary is produced. The error prevents you from shipping code that relies on unstable internal details.

This mechanism also protects you from accidental coupling. If you refactor privateFunc into two smaller functions, any code that tried to call it would have broken. Because the compiler blocks external access, you can change the internals freely. Only the exported names matter to the outside world.

Realistic pattern: Encapsulated structs

A common use case for unexported names is struct fields. You often want to expose a struct type but control how external code modifies the data. You make the fields unexported and provide exported methods to access or update them. This is encapsulation.

// user/user.go
package user

// User represents a system user.
// Fields are unexported to prevent direct modification.
type User struct {
    id       int
    email    string
    password string
}

// NewUser creates a user and validates input.
// It returns a pointer to the User struct.
func NewUser(email, password string) *User {
    if email == "" {
        return nil
    }
    return &User{
        email:    email,
        password: password,
    }
}

// Email returns the user's email address.
// This is the only way external code can read the email.
func (u *User) Email() string {
    return u.email
}

// SetEmail updates the email after validation.
// The receiver name u matches the type User.
func (u *User) SetEmail(email string) error {
    if email == "" {
        return fmt.Errorf("email cannot be empty")
    }
    u.email = email
    return nil
}

External code interacts with the User struct through methods. It cannot touch the fields directly.

// main/main.go
package main

import (
    "fmt"
    "myproject/user"
)

func main() {
    u := user.NewUser("alice@example.com", "secret")
    
    // Access data via exported methods.
    fmt.Println(u.Email())

    // Direct field access fails.
    // The compiler rejects this with cannot refer to unexported field email.
    // fmt.Println(u.email)
}

Convention aside: Receiver names should be short and match the type. Use (u *User) or (u User). Avoid (this *User) or (self *User). The community standard is one or two letters. This keeps method signatures clean and readable.

Encapsulation isn't optional. It's the default.

Pitfalls and compiler errors

Visibility rules cause confusion in a few specific scenarios. Understanding these patterns saves debugging time.

Interfaces require exported methods

If you define an interface in one package and implement it in another, the methods must be exported. The interface contract is public. The implementation must match the contract exactly, including visibility.

// logger/logger.go
package logger

// Logger defines the interface for logging.
type Logger interface {
    Log(msg string)
}
// app/app.go
package app

import "myproject/logger"

// ConsoleLogger implements logger.Logger.
type ConsoleLogger struct{}

// log is unexported and cannot satisfy the interface.
// func (c *ConsoleLogger) log(msg string) {}

// Log is exported and satisfies the interface.
func (c *ConsoleLogger) Log(msg string) {}

If you use log instead of Log, the compiler rejects the code with type ConsoleLogger does not implement logger.Logger (method log has unexported name). The error message explicitly mentions the unexported method. Fix the capitalization to resolve the issue.

Nested structs hide inner fields

If a struct contains a field of another type, and that field is unexported, you cannot access the inner type's fields from outside. The visibility chain breaks at the first unexported link.

// config/config.go
package config

// Config holds application settings.
type Config struct {
    db DatabaseConfig
}

// DatabaseConfig holds database settings.
type DatabaseConfig struct {
    host string
    port int
}

External code can access config.db only if db is exported. If db is lowercase, the compiler blocks access. Even if DatabaseConfig and its fields are exported, the unexported field db prevents access. The compiler complains with cannot refer to unexported field db. Export the field or provide a method to access the nested data.

The internal directory: Module-level privacy

Go provides a special directory named internal to restrict visibility at the module level. Packages inside an internal directory are only importable by code in the parent directory tree. This allows you to share code between packages in your module without exposing it to the world.

Consider this directory structure:

myproject/
├── internal/
│   └── helper/
│       └── helper.go
├── cmd/
│   └── app/
│       └── main.go
└── pkg/
    └── api/
        └── handler.go

The package myproject/internal/helper can be imported by myproject/cmd/app and myproject/pkg/api. It cannot be imported by github.com/other/pkg. The compiler enforces this restriction. The internal directory acts as a firewall. It keeps shared utilities private to your module.

This is a powerful tool for library authors. You can organize code into logical packages without polluting the public API. Use internal when you have code that multiple packages in your module need, but external users should never see.

The internal directory is a compiler-enforced convention. It is not just a folder name. The tooling respects the boundary.

Testing unexported code

Test files ending in _test.go are part of the same package by default. This gives tests full access to unexported names. You can test internal functions and fields without exposing them.

// mypkg/mypkg_test.go
package mypkg

import "testing"

func TestPrivateFunc(t *testing.T) {
    // privateFunc is unexported but accessible here.
    // The test file is in package mypkg.
    result := privateFunc()
    if result != "Secret logic" {
        t.Errorf("unexpected result: %s", result)
    }
}

If you use package mypkg_test in the test file, the test runs as an external package. It can only access exported names. This is useful for testing the public API as a user would see it. Most projects use package mypkg for unit tests to access internals, and package mypkg_test for integration tests.

Tests are special. They live inside the package boundary, so they can see everything.

Decision: Visibility strategies

Use an exported name when you want other packages to call a function or read a variable. Use an unexported name when the identifier is an implementation detail that should not be part of the public API. Use an exported struct with unexported fields when you need to control how external code modifies the data. Use an interface with exported methods when you want to define a contract that other packages can implement. Use the internal directory when you need to share code within a module but hide it from external imports. Use package mypkg in test files to access unexported identifiers for unit testing. Use a lowercase receiver name like (u *User) when defining methods, following the convention of short, matching names.

Export sparingly. Every exported name is a promise you have to keep.

Where to go next