skip to content

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%

answer

  1. ask what the unit of privacy is
  2. files are not a boundary here
  3. there is no per-type private in Go
  4. one scope shared by every file
  5. to hide it, move it to its own package

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.

solid answer

~50 s

Yes, and this is the point people coming from class-based languages get wrong. Unexported names are private to the *package*; splitting a package across ten files hides nothing, because all ten files compile into one scope. There is also no per-type privacy: a helper type in the same package can reach straight into another type's lower-case fields. Two consequences follow. First, getters and setters inside the same package buy you no protection, since neighbouring code can bypass them by touching the field directly; they are worth writing only for invariants at the package boundary or to satisfy an interface. Second, the only way to genuinely hide something from a piece of your own code is to move it into a separate package. Directory nesting is irrelevant: a package in a subdirectory is simply a different package and gets no special access to its parent's unexported names.

code

go · 18 lines
go
// file: user/user.go
package user

type User struct {
	id    int64
	email string
}

// file: user/store.go
package user

type store struct {
	byID map[int64]*User
}

func (s *store) put(u *User) {
	s.byID[u.id] = u // legal: same package scope
}

go deeper

for a junior

Know that the package, not the file, is what unexported names are private to, and that adding files never changes who can see what.

for a middle

Be ready to explain why there is no per-type privacy, what that means for accessors inside a package, and why a subdirectory package gets no special access.

for a senior

Show that you use package size as the encapsulation knob: when a package grows until everything touches everything, the answer is to carve out a smaller package with a stated exported surface.

for a principal

Be able to defend a package layout as a trust boundary across teams, and to argue when a large cohesive package is right versus when it has quietly stopped encapsulating anything.

## The boundary is the package Go draws exactly one encapsulation line, and it is around the package. An unexported identifier is visible to every declaration in every file of the package that declares it, and to nothing else. That differs from the model most engineers arrive with. In a class-based language, `private` usually means "visible to this class", so two classes in the same file or the same namespace still cannot reach into each other. In Go, two types declared in the same package can read and write each other's lower-case fields freely. ```go // file: user/user.go type User struct { id int64 email string } // file: user/store.go type store struct { byID map[int64]*User } func (s *store) put(u *User) { s.byID[u.id] = u // legal: same package, different file, different type } ``` Both files declare `package user`, so they compile into one package scope. `store.put` names `u.id` with no ceremony. ## What follows from that **Files are an organising tool, not a security tool.** Moving a type's methods into a second file, or splitting a large file in half, changes nothing about who can touch what. If a review comment says "move this into its own file so nothing else uses it", the comment is mistaken; the change to make is a new package. **Accessors do not protect within a package.** A `func (u *User) Email() string` method is useful for importers, who cannot see `u.email` at all. For code in the same package it is a convenience at best: the neighbouring function can assign `u.email` directly and skip whatever the setter would have validated. If an invariant must hold, it holds because the package's own code is small enough to read and review, not because the compiler enforces it internally. **Nesting confers nothing.** A common wrong model is that `user/internal-ish-subdir` or `user/store` is "inside" `user` and therefore privileged. Go has no such relationship. Every directory is an independent package, and `user/store` sees only the *exported* names of `user`, exactly as an unrelated repository would. **Package size is a design decision, not a filing decision.** Because package scope is the only scope you get, the size of your package is the size of your trust boundary. A 40-file package where everything reaches into everything is a package that has given up on encapsulation; the fix is to carve out a smaller package whose exported surface states what the rest of the code is allowed to do. This is the main reason Go codebases end up with many small, sharply named packages rather than a handful of large ones. **Test code in the same package sees everything.** A test file declaring `package user` is part of the package and can reach unexported names, which is how Go supports white-box tests without any special mechanism. That is a property of the same rule, not an exception to it. ## Diagnosing it When you are trying to work out what is really encapsulated, `go doc <package>` shows only what leaves the package, and `go doc -u <package>` shows the rest. If the second list is where all the interesting behaviour lives and the package is enormous, the encapsulation is nominal. ## Misconceptions to avoid - "Each file has its own scope." It does not; only *imports* are file-scoped in Go, which is a separate rule about import names. - "Unexported means private to the type." There is no per-type privacy level in the language. - "A subpackage can see its parent's internals." It cannot; there is no parent-child visibility relationship between directories.

  • Does a package in a subdirectory get access to the unexported names of the package above it?
    No. Directory nesting has no visibility meaning in Go. `user/store` is an entirely separate package that sees only `user`'s exported names, exactly as a package in another repository would. Anything you want the subdirectory package to use has to be exported, which is itself a design signal worth noticing.
  • If package scope is the only granularity, how do you hide something from most of your own code?
    By making the package smaller. Move the type and its invariants into a package of their own and export only the operations you want the rest of the codebase to perform. Package size is the knob: a package is exactly as encapsulated as it is small, because everything inside it is mutually visible.
  • Are getters and setters worth writing for code in the same package?
    Not for access control, since a neighbouring function can assign the field directly and bypass them. They earn their place when they serve importers, when they satisfy an interface, or when they compute something rather than just returning a field. A same-package setter that only assigns is noise.

saying these in an interview costs you the question

  • Says each file in a package has its own private scope
  • Believes Go has a per-type private like a class-based language
  • Thinks a subdirectory package can see its parent's unexported names
  • Suggests splitting a file to hide a field from other code
  • Claims same-package getters enforce invariants