Go Language Object Oriented Programming
External
Internal
Overview
Go is an object-oriented language, but the object orientation programming model is relatively simple, compared with other object-oriented languages. It has been said about Go that is "weakly" object-oriented. Go does not use the term "class", there is no class
keyword in the language. However, Go allows associating data with methods, which what a class in other programming languages really is.
Go provides syntax to associate an arbitrary local type with arbitrary functions, turning them into methods of that type. The local type can be a type alias, a struct
or a function. In this context, "local" means a type that is defined in the same package as the function. That is why a programmer cannot add new methods to built-in types.
The most common pattern for implementing object-orientation is to use a struct
as data encapsulation mechanism, optionally declaring fields as unexported, thus preventing direct access to them from outside the package, and then associating the struct with functions, by declaring the struct type as the receiver type for those functions. The functions become methods of the type. This construct is the closest equivalent to a class in other programming languages. The standard dot notation is then used to call a method on the instance of the receiver type.
Go does offer inheritance, overriding and polymorphism in the language syntax the same way other OOP languages do, but these features are available and can be implemented with a combination of struct field embedding and interfaces. See:
Methods
Receiver Type
func function_name(implicit_receiver_parameter receiver_type|receiver_type_pointer, ...) ... { ... }
We call that receiver parameter. It is, loosely, the logical equivalent of the self
function parameter in Python and the implicit method parameter this
in Java. The receiver parameter can be simply thought of as the first parameter of the function. In the underlying implementation, it is indeed not much else indeed.
Receiver Parameter Naming
As we write idiomatic Go code, it is common to use the first letter or a short two-letter abbreviation of the type as the name of the receiver parameter. A short name makes sense because these parameters typically appear on almost every line of the method body. Receiver names should be consistent cross a type's methods, do not use one name in one and other name in another.
func (b *Buffer) Read(p []byte) (n int, err error)
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request)
In our case, if the name of the type is Color
, the name of the parameter is c
or even color
. We might even consider naming the receiver parameters with abbreviations of the interfaces they represent. See this article for arguments against naming that parameter "this" or "self".
Alos see:
Value Receiver Type
The syntax to declare a value receiver type for a function is:
func (receiver_parameter_name receiver_type) function_name(...) ... { ... }
Example:
type Color struct {
color string
}
func (c Color) SomeMethod() string {
return "dark " + c.color
}
In the example above, Color
is the receiver type, and c
is the variable that refers to the particular receiver type value the method was invoked on. The invocation target is passed as value inside the function.
The method is invoked on the receiver type instances with the usual dot notation syntax:
color := Color{"blue"}
result := color.SomeMethod() // the method will return "dark blue"
The invocation copies the color
value into in the c
variable inside the function, making c
an implicit parameter of the method. There is no formal c
function parameter in the signature, yet the method body has access to c
, which behaves similarly to any other function parameter. That is why the following syntax makes sense, and it also works. The syntax confirms that SomeMethod()
is a method associated with the type Color
, and when invoked with the instance of the type as its first (implicit) parameter, it behaves exactly as expected:
color := Color{"blue"}
Color.SomeMethod(color)
Because the receiver instance is passed by value, hence a copy of that value is passed inside the function, the method cannot modify the invocation target instance. A pointer receiver type must be used if instead to modify the target.
Type Method Set
The specification defines the method set of a type as the method set associated with the values of the type. Also see Method Sets above and Pointer Type Method Set below.
Pointer Receiver Type
The syntax to declare a pointer receiver type for a function is:
func (receiver_parameter_name *receiver_type) function_name(...) ... { ... }
Example:
func (c *Color) SomeOtherMethod() {
c.color = "dark " + c.color
// (*c).color = "dark" + (*c).color is an equivalent, longer
// syntax. The compiler can handle the simpler syntax.
}
...
color := Color{"blue"}
color.SomeOtherMethod() // equivalent with (&color).SomeOtherMethod()
fmt.Println(color) // will print "{dark blue}"
In the example above, a pointer to a Color
instance is passed inside the method, so the method can mutate the target instance it was invoked on. The invocation copies the pointer to the Color{"blue"}
value into in the c
variable inside the function, making c
an implicit parameter of the method. Similarly to the value receiver type syntax, there is no formal c
function parameter in the signature, yet the method body has access to c
, which behaves similarly to any other function parameter. That is why the following syntax makes sense, and it also works. The syntax confirms that SomeMethod()
is a method associated with the type *Color
(note that it is a pointer type), and when invoked with the pointer to a type instance as its first (implicit) parameter, it behaves exactly as expected:
color := Color{"blue"}
(*Color).SomeOtherMethod(&color)
Pointer Type Method Set
The method set associated with the pointers of a type consists of both all methods declared with a pointer receiver for that type and all methods declared with a value receiver of that type. The method set associated with the pointers of a type always includes the method set associated with the values of the type. This is because given a pointer, we can always infer the value pointed by that address, so the methods associated with the value will naturally "work". The reverse is not true: given a value, not always we can get an address for it, and because we are not able to get an pointer, we cannot assume that the methods associated with the pointer will work.
Invoking on nil Pointers
Since pointers are involved, methods declared with pointer receiver types may be invoked on a nil
pointers, which will lead to a "invalid memory address or nil pointer dereference" panic:
panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xbe79030] goroutine 1 [running]: main.(*Color).SomeOtherMethod(...) .../cmd/gotest/main.go:8 main.main() .../cmd/gotest/main.go:14 +0x10 Process finished with the exit code 2
If the implementation semantics allows it, the method may guard against nil
pointers:
func (c *Color) SomeOtherMethod() {
if c == nil {
return
}
c.color = "dark " + c.color
}
Dot Notation works with Both Values and Pointers
The Go compiler knows how to use the dot notation with both values and pointers.
There is no need to invoke a method defined with a pointer receiver type with:
(&variable).SomeMethod()
This will work:
variable.SomeMethod()
Similarly, inside the method, there is no need to dereference the pointer receiver type parameter:
func (v *SomeType) SomeMethod(...) {
(*v).someField // not necessary
}
This will work:
func (v *SomeType) SomeMethod(...) {
v.someField
}
Mixing Value and Pointer Receiver Types
When using pointer receivers, it is good programming practice to have all method use pointer receivers, or none of them use pointer receivers. If we mix value and pointer receivers, the IDE would detect that as static check warning:
Struct Color has methods on both value and pointer receivers. Such usage is not recommended by the Go Documentation.
Functions as Receiver Types
TODO: https://go.dev/blog/error-handling-and-go#simplifying-repetitive-error-handling
Encapsulation
Encapsulation in this context is giving access to private data, such as package-internal data or private struct fields, via public methods. This way, access to data is controlled.
Encapsulation can be implemented with package data and with structs, where the "private" fields are declared using identifiers that start with lower cap letters, so they are not visible outside the package. Controlled access to them is given via accessor and mutator methods, or getters and setters, which are functions that have been declared to use the struct as a receiver type:
type Color struct {
color string
}
func (c *Color) Init(color string) {
c.SetColor(color)
}
func (c *Color) Color() string {
return (*c).color
}
func (c *Color) SetColor(color string) {
(*c).color = color
}
...
var c Color
c.Init("blue")
fmt.Println(c.Color()) // prints blue
c.SetColor("red")
fmt.Println(c.Color()) // prints red