Exported and Unexported Names in Go

How Go uses capital letters for public and private names, what exported means for structs, fields and methods, and the errors you see when you get it wrong.

Go has no public or private keywords. Visibility is decided by the first letter of a name. A name that starts with a capital letter is exported and usable from other packages. A lowercase name is unexported and only visible inside its own package. People coming from Java or C# often trip on this at first, and then come to like how readable it is.

The Capital Letter Rule

// file: price/price.go
package price

const TaxRate = 0.18 // exported

func WithTax(p float64) float64 { // exported
	return round(p * (1 + TaxRate))
}

func round(p float64) float64 { // unexported
	return float64(int(p*100+0.5)) / 100
}

From another package, price.WithTax(100) and price.TaxRate work. price.round(1.234) fails to compile:

./main.go:8:20: undefined: price.round

Older Go versions said cannot refer to unexported name price.round. Either way, the fix is to export the name (rename it Round) if other packages should use it, or keep it private and call it only inside price.

The rule applies to every kind of top-level name: functions, types, constants, variables. Inside a package, case does not matter for access: WithTax can call round freely.

Struct Fields and Methods

Each field and method has its own visibility, independent of the struct type:

package main

import (
	"encoding/json"
	"fmt"
)

type User struct {
	Name     string // exported field
	Email    string // exported field
	password string // unexported field
}

func main() {
	u := User{Name: "asha", Email: "a@x.in", password: "secret"}
	b, _ := json.Marshal(u)
	fmt.Println(string(b))
}

Output:

{"Name":"asha","Email":"a@x.in"}

encoding/json lives in another package, so it can only see exported fields. password is silently skipped. This catches many beginners: a struct with all-lowercase fields marshals to {}.

Lowercase fields and JSON:

If json.Marshal gives {} or json.Unmarshal leaves fields empty, check that the fields start with a capital letter. Use struct tags such as json:"name" to control the JSON key while keeping the Go field exported.

Exported Type, Unexported Fields

A common pattern exports the type and a constructor but keeps fields private, so callers must go through methods that keep the data valid:

package main

import (
	"errors"
	"fmt"
)

type Account struct {
	balance int
}

func (a *Account) Deposit(n int) error {
	if n <= 0 {
		return errors.New("deposit must be positive")
	}
	a.balance += n
	return nil
}

func (a *Account) Balance() int { return a.balance }

func main() {
	var acc Account
	acc.Deposit(50)
	fmt.Println(acc.Deposit(-5), acc.Balance())
}

This prints deposit must be positive 50. In a separate package, callers could not write acc.balance = -1000, because balance is unexported. Only Deposit can change it.

Naming Exported Things Well

Callers always see an exported name together with its package name, so avoid repeating it:

price.PriceWithTax   ->  price.WithTax
user.UserRepository  ->  user.Repository
http.HTTPServer      ->  http.Server

This repetition is called stutter, and linters like revive flag it.

Exporting is a promise:

Every exported name becomes something other code can depend on. Removing or changing it later breaks those callers. Start with names unexported and export them only when another package actually needs them.

A clean package API:

If the exported names of a package read naturally with the package name in front, such as price.WithTax and user.New, and everything else is lowercase, your package has a small, clear API.

Summary

Names starting with a capital letter are exported and usable from other packages, lowercase names are private to their package. The rule applies separately to types, fields and methods, which is why packages like encoding/json ignore lowercase fields. Keep fields unexported when data needs protecting, avoid repeating the package name in exported names, and export only what other packages need.