skip to content

In a Go struct, what does capitalising a field name like Name instead of name control?

level: juniorimportance: must knowfreq 72%

answer

  1. visibility is spelled, not declared
  2. no keyword does this in Go
  3. look at the first letter
  4. upper case crosses the package line

basics

~20 s

A field whose name starts with an upper-case letter is exported: any package that imports the declaring package can read and write it. A lower-case first letter keeps the field usable only inside the package that declares the struct.

solid answer

~40 s

Go has no `public` or `private` keywords; the first character of an identifier decides visibility, and struct fields follow that rule like everything else. `Name` is exported, so `u.Name` compiles in any package that imports the one declaring the type; `name` is unexported, and only code compiled into that same package can select it. The boundary is the package, not the struct and not the file, so a sibling file in the same package has full access to the unexported field. Unexported fields still exist at run time, still occupy their place in the layout, and are still copied when the struct value is assigned or passed — what they lose is external access. Keeping fields unexported behind a constructor and a few accessor methods is the normal way a package protects an invariant.

code

go · 8 lines
go
package schema

type Schema struct {
	Name string // exported: any importing package may read or set it
	id   int    // unexported: only files in package schema may name it
}

func (s Schema) ID() int { return s.id } // read-only access for outsiders

go deeper

for a junior

Be ready to state the rule in one sentence and apply it on sight: an upper-case first letter means other packages may use the field, a lower-case one means they may not.

for a middle

Explain that the boundary is the package rather than the file or the struct, that unexported fields still occupy space and are still copied, and how a constructor plus accessor methods protect an invariant.

for a senior

Show judgment about what to export on a type other teams import: exporting a field freezes its name and type in your API and lets callers assign to it without passing any validation you wrote.

for a principal

Own the convention across a codebase — which packages expose plain data structs and which hide state behind constructors — and be able to say what breaks downstream when a previously unexported field is exported to unblock one caller.

## The rule Go does not have access modifiers. There is no `public`, `private`, `protected` or `internal` keyword anywhere in the language. Instead, **one rule covers every identifier**: if the first character of the name is an upper-case letter, the identifier is *exported*; otherwise it is *unexported*. That applies to package-level functions, types, constants and variables — and, exactly the same way, to the fields and methods of a struct. ```go type Schema struct { Name string // exported id int // unexported } ``` Any package that imports this one can write `s.Name`. Writing `s.id` from outside the package is a compile error: the selector simply does not resolve there. ## The unit of privacy is the package This is the part that trips up people arriving from Java, C# or C++, where privacy is per class. In Go the unit is the **package**, which may be many files in one directory. Every file compiled into that package sees the same identifiers, so a helper in another file of the same package can read and assign `id` freely. There is no way to hide a field from other code in its own package, and no notion of a field being private to the struct itself. A practical consequence appears in tests. A test file declared `package schema` is part of the package and may assert on unexported fields; a test file declared `package schema_test` is a separate package and may not — it sees only the exported surface, which is exactly the point of writing tests that way when you want to exercise the public API. ## What being unexported does *not* mean An unexported field is not removed, hidden at run time, or excluded from the value: - It occupies its position in the struct's memory layout and contributes to the struct's size and alignment. - It is copied along with everything else when the struct is assigned, passed to a function, returned, or stored in a slice. - It participates in equality when the struct is compared, even from a package that cannot name it. What it loses is *nameability from outside*. Code in another package cannot select it, and generic library code that inspects types at run time can read type information about it but cannot set it. That is why data-driven libraries in Go universally require exported fields for anything they must populate. ## Choosing what to export Exporting a field is a bigger commitment than exporting a method. It publishes the field's **name** and its **type**, and it lets callers assign to it without going through any validation you wrote. Once other packages depend on `cfg.Retries`, you cannot rename it, change its type, or start deriving it from something else without breaking them. The usual patterns: - **Plain data carriers** — a request or a row of a report — export their fields. There is no invariant to protect, and callers need to build the value with a composite literal. - **Types with invariants** — a connection, a counter, a validated identifier — keep the state unexported and expose a constructor such as `NewClient(...)` plus methods. Callers cannot put the value into an inconsistent state because they cannot reach the state. When you do want read-only access, add an accessor. Go convention omits the `Get` prefix, so the method reading `name` is written `func (s Schema) Name() string`. A field and a method cannot share a name on the same type, which is why the field is usually the lower-case one. Be aware that returning a slice, map or pointer field from an accessor hands the caller a reference into your value — copy it if the invariant matters. ## Common mistakes - Assuming the rule is per file. It is per package. - Assuming an unexported field is dropped from the struct, or that its bytes are somehow protected. It is ordinary memory; only the compiler's name resolution enforces the boundary. - Trying to export just for one caller. That is a permanent widening of the API; a method or a constructor argument is usually the smaller change. - Forgetting that a package's own tests, benchmarks and examples in the same package can reach everything, so a green in-package test proves nothing about what an importer can actually do.

  • Does an unexported field disappear when the struct is used from another package?
    No. The field still exists, still takes up space in the layout, and is still copied whenever the value is assigned or passed. Outside code simply cannot name it, and library code driven by run-time type inspection cannot set it. Size, alignment and equality behaviour are unchanged.
  • Can one file read an unexported field declared in another file of the same package?
    Yes. Privacy is per package, not per file, so every file in the directory sees the same identifiers. That is also why an in-package test file can assert on unexported fields while an external test file declared as package foo_test cannot.
  • How do you let callers read a field without letting them change it?
    Keep the field unexported and add an exported method that returns it, for example func (s Schema) Name() string { return s.name }. Go convention drops the Get prefix. If the field is a slice, map or pointer, return a copy, otherwise the caller can mutate your value through the reference you handed out.

The capital letter is the field's passport: without it the field can move freely at home, inside its own package, but it cannot cross the package border.

saying these in an interview costs you the question

  • Says Go has public and private keywords for fields
  • Thinks the visibility boundary is the file, not the package
  • Believes an unexported field is stripped from the struct's layout
  • Claims another package can reach an unexported field somehow
  • Assumes lower-case fields are hidden from same-package code