skip to content

Exported and Unexported Names

Go has exactly two visibility levels — exported and not — decided by the first letter of the name, and the boundary they apply to is the package rather than the type. That surprises people from Java, where private means private to a class, and it changes how you design a package's surface.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

In Go, what makes an identifier visible to code in another package?

level: juniorimportance: must knowfreq 82%

answer

  1. no keyword does this job in Go
  2. the decision is made by spelling
  3. look at the very first character
  4. upper case reaches across the import

basics

~20 s

The case of its first letter. A name beginning with an upper-case letter is exported and can be used by importing packages; a name beginning with a lower-case letter is usable only inside the package that declares it.

solid answer

~40 s

Go has no `public`, `private` or `protected` keyword: the first character of the identifier is the entire access modifier. If it is an upper-case letter, the name is exported and any package that imports yours can write `pkg.Name`. If it is lower-case, an underscore or a digit-led name, it is unexported and invisible outside the package. The rule applies to package-level types, functions, variables and constants, and equally to struct field names and method names, so `User.Email` is reachable from another package while `User.verified` is not. Local variables inside a function are never exported no matter how you spell them, because export only applies to package-level declarations plus fields and methods. `go doc net/http` lists exactly the exported surface; `go doc -u` adds the unexported names.

code

go · 10 lines
go
type User struct {
	ID       int64  // exported: importers may read and set it
	Email    string // exported
	verified bool   // unexported: only this package can touch it
}

// Verified is exported; markVerified is not.
func (u *User) Verified() bool { return u.verified }

func (u *User) markVerified() { u.verified = true }

go deeper

for a junior

Be ready to state the rule in one sentence and apply it to a struct: which fields can an importer set, which cannot. Know that Go has no visibility keywords at all.

for a middle

Expect to explain what the rule covers beyond functions, including fields, methods and constants, and why a local variable is unaffected. Be able to show what an importer's compile error looks like.

for a senior

Demonstrate that you treat capitalisation as a contract decision, not a naming style: know that lower-casing a shipped name breaks importers at compile time and that you review the exported surface before release.

for a principal

Own the consequence that one character is your whole encapsulation policy. Be ready to say how your teams keep exported surfaces deliberate when the language offers no intermediate visibility level.

## The rule in one line An identifier in Go is **exported** — visible to code in other packages — if and only if its first character is an upper-case Unicode letter, and it is either declared at package level or is a struct field name or a method name. Everything else is **unexported**. That is the whole access-control system. There is no `public`, `private`, `protected`, `internal` or `friend` keyword in the language, and there is no way to declare finer-grained access such as "visible to this one other package". You choose between two states, and you choose by how you spell the name. ## What the rule applies to ```go package user type User struct { ID int64 // exported field Email string // exported field verified bool // unexported field } func New(id int64) *User { return &User{ID: id} } // exported function func normalize(s string) string { return s } // unexported function const MaxNameLen = 64 // exported constant var defaultDomain = "example.com" // unexported package variable func (u *User) Verified() bool { return u.verified } // exported method func (u *User) markVerified() { u.verified = true } // unexported method ``` An importer writing `import "example.com/acct/user"` can refer to `user.User`, `user.New`, `user.MaxNameLen`, the fields `ID` and `Email`, and the method `Verified`. It cannot name `user.normalize`, `user.defaultDomain`, `u.verified` or `u.markVerified()` — each of those is a compile-time error, not a run-time one. Two cases surprise newcomers: - **Local names are never exported.** A variable declared inside a function body is scoped to that block; capitalising it changes nothing about visibility, it just looks odd. - **The blank identifier and underscore-led names are not exported.** `_config` starts with an underscore, which is not an upper-case letter, so it is unexported. Go does not use a leading underscore as a privacy convention the way some other languages do; the underscore has no visibility meaning at all. ## Why the case of one letter The design puts the access decision at the point of *use*: when you read a call site, `user.Lookup(id)` is immediately identifiable as another package's supported API, and `lookupCached(id)` as something local. You do not have to open the declaration to know whether you are touching a package's contract. It also makes the exported surface mechanically extractable, which is what `go doc` and pkg.go.dev rely on. The cost is that renaming a name changes its visibility. Lower-casing an exported name is a breaking change for every importer, and upper-casing an unexported one silently adds to your contract. That is why the exported surface deserves the same review attention as behaviour. ## Reading the surface `go doc <package>` prints exactly the exported names, in the form an importer sees them, which makes it the fastest audit of what you have committed to. `go doc -all <package>` adds the full documentation for every exported name, and `go doc -u` includes unexported ones when you are studying an implementation. ## Common misconceptions - *"Lower-case means private to the file or to the type."* No: unexported names are visible to every file in the same package, and to every type in it. The package is the boundary. - *"An exported field is read-only from outside."* No: an importing package can both read and assign an exported field. - *"Capitalisation is only a naming convention."* It is enforced by the compiler; referring to an unexported name from another package does not build. - *"A nested directory package inherits access."* Directory nesting has no effect on visibility; a package in a subdirectory is just another package.

  • Does the rule apply to struct fields and methods, or only to top-level declarations?
    Both. Field names and method names follow the same first-letter rule, which is why an exported struct can mix `ID` (settable by importers) with `verified` (invisible to them). The only names the rule does not reach are locals inside a function body, which are block-scoped regardless of spelling.
  • Is a name like `_internalCache` unexported because of the underscore?
    It is unexported, but not because of the underscore. Go requires an upper-case first letter to export, and an underscore is not one. Unlike Python, Go attaches no meaning at all to a leading underscore in a name; `_internalCache` and `internalCache` are equally invisible outside the package.
  • Can an exported function return an unexported type?
    Yes, and it compiles: callers can hold the value with `:=` and call its exported methods. But they cannot write the type's name, declare a variable or field of it, or use it in their own signatures. Do it deliberately, as an opaque handle, or not at all.

The capital letter is the sign on the door. Names written in capitals are in the shop window where any customer can point at them; everything else is stockroom, visible only to staff of that shop.

saying these in an interview costs you the question

  • Says Go has public and private keywords
  • Thinks a lower-case name is hidden from other files in the same package
  • Believes the rule applies only to functions, not to struct fields
  • Claims a leading underscore marks a name private in Go
  • Thinks capitalisation only affects generated documentation
open as a page

Can code in one file of a Go package read an unexported field of a struct declared in another file of that package?

level: middleimportance: must knowfreq 58%

basics

~20 s

Yes. In Go the package is the unit of encapsulation, not the file and not the type. Every file of a package shares one scope, so any file can read and assign the unexported fields of any type declared in that package.

open as a page

Why would a Go package keep a struct's fields unexported and hand callers an exported constructor instead?

level: middleimportance: should knowfreq 50%

basics

~20 s

To control how values are built and how they are represented. With the fields unexported, importers cannot set them, so the constructor becomes the only place invariants can be established, and the field layout can change later without breaking callers.

open as a page

Importers already set a struct field your Go package exported by accident — how do you take it back?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Lower-casing the name breaks every importer at compile time, so do it in place only when you can rebuild them all in one change. Otherwise add the replacement API, mark the field deprecated, and remove it at the next major version.

open as a page

How do you decide a Go package's exported surface when many teams import it and unexporting later breaks them?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Export the smallest set of names that lets consumers do their job, and treat every capital letter as a promise you cannot withdraw cheaply. Start names unexported, export on a concrete request, and review the exported diff at every release.

open as a page