A variable or method of an object is said to be encapsulated if it is inaccessible to clients of the object. Encapsulation, sometimes called information hiding, is a key aspect of object-oriented programming.
Go has only one mechanism to control the visibility of names: capitalized identifiers are exported from the package in which they are defined, and uncapitalized names are not. The same mechanism that limits access to members of a package also limits access to the fields of a struct or the methods of a type. As a consequence, to encapsulate an object, we must make it a struct.
That’s the reason the IntSet
type from the previous section was
declared as a struct type even though it has only a single field:
type IntSet struct { words []uint64 }
We could instead define IntSet
as a slice type as follows,
though of course we’d have to replace each occurrence of
s.words
by *s
in its methods:
type IntSet []uint64
Although this version of IntSet
would be essentially
equivalent, it would allow clients from other packages to read and
modify the slice directly.
Put another way, whereas the expression *s
could be used in any
package, s.words
may appear only in the package that defines
IntSet
.
Another consequence of this name-based mechanism is that the unit of encapsulation is the package, not the type as in many other languages. The fields of a struct type are visible to all code within the same package. Whether the code appears in a function or a method makes no difference.
Encapsulation provides three benefits. First, because clients cannot directly modify the object’s variables, one need inspect fewer statements to understand the possible values of those variables.
Second, hiding implementation details prevents clients from depending on things that might change, which gives the designer greater freedom to evolve the implementation without breaking API compatibility.
As an example, consider the bytes.Buffer
type.
It is frequently used to accumulate very short strings, so it is a
profitable optimization to reserve a little extra space in the object
to avoid memory allocation in this common case.
Since Buffer
is a struct type, this space takes the form of an
extra field of type [64]byte
with an uncapitalized name.
When this field was added, because it was not exported, clients of
Buffer
outside the bytes
package were unaware of any
change except improved performance.
Buffer
and its Grow
method are shown below, simplified
for clarity:
type Buffer struct { buf []byte initial [64]byte /* ... */ } // Grow expands the buffer's capacity, if necessary, // to guarantee space for another n bytes. [...] func (b *Buffer) Grow(n int) { if b.buf == nil { b.buf = b.initial[:0] // use preallocated space initially } if len(b.buf)+n > cap(b.buf) { buf := make([]byte, b.Len(), 2*cap(b.buf) + n) copy(buf, b.buf) b.buf = buf } }
The third benefit of encapsulation, and in many cases the most
important, is that it prevents clients from setting an object’s
variables arbitrarily.
Because the object’s variables can be set only by functions in the
same package, the author of that package can ensure that all those
functions maintain the object’s internal invariants.
For example, the Counter
type below permits clients to
increment the counter or to reset it to zero, but not to set it to some
arbitrary value:
type Counter struct { n int } func (c *Counter) N() int { return c.n } func (c *Counter) Increment() { c.n++ } func (c *Counter) Reset() { c.n = 0 }
Functions that merely access or modify internal values of a type, such
as the methods of the Logger
type from log
package,
below, are called getters and setters.
However, when naming a getter method, we usually omit the Get
prefix.
This preference for brevity extends to all methods, not just field
accessors, and to other redundant prefixes as well, such as
Fetch
, Find
, and Lookup
.
package log type Logger struct { flags int prefix string // ... } func (l *Logger) Flags() int func (l *Logger) SetFlags(flag int) func (l *Logger) Prefix() string func (l *Logger) SetPrefix(prefix string)
Go style does not forbid exported fields. Of course, once exported, a field cannot be unexported without an incompatible change to the API, so the initial choice should be deliberate and should consider the complexity of the invariants that must be maintained, the likelihood of future changes, and the quantity of client code that would be affected by a change.
Encapsulation is not always desirable.
By revealing its representation as an int64
number of
nanoseconds, time.Duration
lets us use all the usual
arithmetic and comparison operations with durations, and even to define
constants of this type:
const day = 24 * time.Hour fmt.Println(day.Seconds()) // "86400"
As another example, contrast IntSet
with the
geometry.Path
type from the beginning of this chapter.
Path
was defined as a slice type, allowing its clients to
construct instances using the slice literal syntax, to iterate over
its points using a range loop, and so on, whereas these operations are
denied to clients of IntSet
.
Here’s the crucial difference: geometry.Path
is intrinsically a
sequence of points, no more and no less, and we don’t foresee adding
new fields to it, so it makes sense for the geometry
package to
reveal that Path
is a slice.
In contrast, an IntSet
merely happens to be represented
as a []uint64
slice.
It could have been represented using []uint
, or
something completely different for sets that are sparse or very small,
and it might perhaps benefit from additional features like an extra
field to record the number of elements in the set. For these reasons,
it makes sense for IntSet
to be opaque.
In this chapter, we learned how to associate methods with named types, and how to call those methods. Although methods are crucial to object-oriented programming, they’re only half the picture. To complete it, we need interfaces, the subject of the next chapter.
18.118.144.69