In Go, what makes an identifier visible outside its package, and what does an unexported struct field mean for importing packages?
answer
- Go has no access keywords at all
- look at the first letter of the name
- upper case crosses the package line
- the boundary is the directory, not the file
- hidden fields force callers through a constructor
basics
~20 sCase decides it. An identifier whose name begins with an upper-case letter is exported and visible to packages that import it; a lower-case one is package-private. Importing packages cannot read, set, or even name an unexported struct field.
solid answer
~50 sGo has no `public`, `private` or `protected` keywords: the first letter of the name is the access control. Upper-case means exported — visible to any package that imports yours; lower-case means it is usable only inside the declaring package. The rule applies uniformly to top-level constants, variables, types and functions, and also to struct fields, methods and interface methods. Scope is the *package*, not the file, so two files in the same package see each other's lower-case names freely. For a struct, unexported fields mean an outside package cannot write `pkg.Runner{dir: "x"}` and cannot assign `r.dir` — it must go through whatever the package exports, which is normally a constructor such as `migrate.New(dir)` plus a few methods. That is the cheapest way to keep a package's promised surface small: everything starts lower-case, and you raise a letter only when a caller genuinely needs it.
code
go · 15 linespackage migrate
// Runner applies SQL migration files from a directory.
type Runner struct {
dir string // unexported: invisible outside package migrate
applied int
}
// New is how another package builds a usable Runner.
func New(dir string) *Runner {
return &Runner{dir: dir}
}
// Applied reports how many files have been applied.
func (r *Runner) Applied() int { return r.applied }go deeper
Be ready to state the rule in one sentence and name what it covers: top-level declarations, struct fields and methods. Then say what an importing package can and cannot do with a hidden field.
Explain that the scope is the package directory, not the file or the type, and show why unexported fields push callers through a constructor instead of a composite literal.
Show the habit behind the rule: start everything lower-case and raise a letter only for a caller with a real need, because a capital letter is a promise you cannot quietly withdraw.
Frame capitalisation as the cheapest API governance you have. Argue for reviewing the diff of newly capitalised names, since that diff is the only place your package's obligations grow.
## The rule Go spells access control with capitalisation. An identifier is **exported** — visible to any package that imports the declaring package — if and only if its name begins with an upper-case Unicode letter. Everything else is visible only inside the package that declares it. There is no `public`, `private`, `protected`, `friend`, `module` or `sealed` keyword; there is one rule and no modifiers on it. The rule applies to every named thing: - top-level `const`, `var`, `type` and `func` declarations, - **struct fields**, - **methods**, - methods listed inside an interface type. So `Runner` is exported, `runner` is not; `Runner.Applied` is exported, `Runner.applied` is not; and a package can perfectly well export a type whose fields are all unexported. ## The unit of privacy is the package, not the file or the type This surprises people arriving from Java or C#. A Go package is usually a directory of several `.go` files, and all of them share one scope. If `parse.go` declares `type sqlFile struct{...}`, then `run.go` in the same package uses it without ceremony. There is no file-private level, and no per-type privacy either: a lower-case field of one struct is readable by any other code in the same package, including a different type's methods. Privacy in Go is a boundary you draw around a *directory*, not around a class. ## What an unexported field actually forbids Suppose a schema-migration package declares: ```go package migrate type Runner struct { dir string applied int } ``` From another package you can name the type `migrate.Runner`, declare variables of it, pass it around, and store it in a slice. What you cannot do is: - read or assign `r.dir` — the compiler reports that `r.dir` is undefined and refers to an unexported field; - build one with a keyed composite literal `migrate.Runner{dir: "./sql"}` — `unknown field dir`; - build one with a positional literal `migrate.Runner{"./sql", 0}` either, since that would have to supply the unexported fields; - set the field through reflection: a reflected value obtained from an unexported field reports that it cannot be set, and assigning to it panics. What is left is exactly what the package chose to expose: exported methods, and any exported constructor. That is the point. The zero `migrate.Runner{}` may be useless — no directory, nothing to apply — so the package supplies `func New(dir string) *Runner` and documents it as the way in. Callers cannot construct a half-built value even by accident, because the parts they would have to fill in are not visible to them. ## Why this matters for surface size Every upper-case letter in your package is a promise. Someone else's code will compile against that name, and from then on the name, its shape and its behaviour are yours to keep working. Lower-case costs you nothing: you can rename a field, change its type, split it into three, or delete it, and no build outside your package notices. So the default is lower-case, and raising a letter is a deliberate decision with a caller behind it. A practical habit follows: write the whole package unexported first, then export only the identifiers your own example or test actually needs from outside. A helper type that exists so two of your functions can share a parse result should never be exported; a struct field that exists to cache a computed value should never be exported. ## Consequences worth knowing early - **Documentation follows the rule.** `go doc` shows exported identifiers by default, so the exported set is literally the documented API of your package. - **Tests inside the package see everything.** A test file declared as `package migrate` can touch unexported identifiers; one declared `package migrate_test` in the same directory sees only the exported surface, which is a useful way to feel how big that surface is. - **Some libraries do care about case.** Packages that inspect your structs through reflection can only see exported fields, which is why an unexported field is skipped when a struct is encoded, and why struct types meant to be serialised carry exported field names. The rule is small enough to state in one sentence, and most of Go's package-design advice is downstream of it: keep the letters low, and you keep your options open.
- Two files in the same package — can one of them use a lower-case name declared in the other?Yes. Unexported means package-private, not file-private. Every `.go` file in a directory contributes to one package scope, so any file can use any identifier declared in any of the others. Go has no file-level visibility.
- If a struct's fields are unexported, how does a caller get a usable value?Through whatever the package exports: usually a constructor like `New(...)` that fills the fields, plus methods that read or change them. That is deliberate — the caller cannot build a half-initialised value, because the parts they would have to supply are not visible to them.
- Can a package export a function whose return type is unexported?It compiles. A caller can hold the value with `:=` and call its exported methods, but cannot write the type's name, so they cannot declare a variable or field of that type. It is legal and occasionally deliberate, but it is usually a sign that the return type should be exported or the function should return something the caller can name.
The capital letter is the door to the street; everything lower-case is a room inside the house that only housemates walk through.
saying these in an interview costs you the question
- Says Go has public, private and protected keywords
- Thinks unexported means file-private rather than package-private
- Claims reflection lets an outside package assign to an unexported field
- Assumes an exported struct type means all its fields are exported
- Believes exporting a name only affects documentation, not compilation