Go Language Object Oriented Programming: Difference between revisions
Line 12: | Line 12: | ||
Go uses <code>struct</code> instead, which is the encapsulation of data and methods, so the <code>struct</code> ends being equivalent to a class. To associate data with methods, we use Go functions and we give the a '''receiver type''', which is the type that function - now becoming a method - is associated with. When calling a method on an instance of the receiver type, the standard dot notation is used. | Go uses <code>struct</code> instead, which is the encapsulation of data and methods, so the <code>struct</code> ends being equivalent to a class. To associate data with methods, we use Go functions and we give the a '''receiver type''', which is the type that function - now becoming a method - is associated with. When calling a method on an instance of the receiver type, the standard dot notation is used. | ||
Go does not have inheritance, constructors or generics. Inheritance can be replaced to a certain extent by composition, embedding and interface, which support code reuse and [[Object-Oriented_Programming#Polymorphism|polymorphism]]. | Go does not have [[Object-Oriented_Programming#Inheritance|inheritance]], constructors or generics. Inheritance can be replaced to a certain extent by composition, embedding and interface, which support code reuse and [[Object-Oriented_Programming#Polymorphism|polymorphism]]. | ||
=Methods= | =Methods= |
Revision as of 17:07, 30 August 2023
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 Go. However, Go allows associating data with methods, which what a class in other programming languages really is.
Go uses struct
instead, which is the encapsulation of data and methods, so the struct
ends being equivalent to a class. To associate data with methods, we use Go functions and we give the a receiver type, which is the type that function - now becoming a method - is associated with. When calling a method on an instance of the receiver type, the standard dot notation is used.
Go does not have inheritance, constructors or generics. Inheritance can be replaced to a certain extent by composition, embedding and interface, which support code reuse and polymorphism.
Methods
Functions can be associated with a type, turning them into methods of that type, by declaring a receiver type on the function.
Receiver Type
Receiver types are declared with a special syntax on functions. This turns the function declared as such into methods of the type, and makes the receiver types analogous to "classes". Depending on whether we want to be able to modify the receiver type instance in the method, the receiver type can be declared as value receiver type or pointer receiver type.
Methods can only be associated with receiver types that are defined in the same package as the method. That is why a programmer cannot add new methods to built-in types.
Value Receiver Type
The syntax to declare a value receiver type for a method is:
func (<receiver_type_parameter_name> <receiver-type>) <function-name>(...) ... {
...
}
Example:
type Color struct {
color string
}
func (c Color) GetADarkerShade() 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 instance the method was invoked on.
The invocation of the method on the receiver type instance is done with the usual dot notation:
color := Color{"blue"}
result := color.GetADarkerShade() // result will be assigned with "dark blue"
The invocation with the dot notation makes the instance being invoked on (color
) an implicit argument (c
) of the method: there is no parameter in the GetADarkerShade
method signature, yet the method body has access to c
. This makes c
an implicit argument of the method.
The dot notation works with both values and pointers. The compiler knows how to handle that transparently.
In the example above, the receiver instance is passed by value. The instance the method is invoked on cannot be modified in the method, because the method gets a copy of the receiver type instance. To be able to modify the instance, it has to be passed as a pointer receiver type.
Pointer Receiver Type
To modify the receiver instance, use a pointer receiver type.
func (<receiver_type_parameter_name> *<receiver_type>) <function_name>(...) ... {
...
}
Example:
func (c *Color) Darken() {
c.color = "dark " + c.color
// (*c).color = "dark" + (*c).color is technically what we should have used, but the compiler can allow for the above, simpler, syntax
}
...
c := Color{"blue"}
c.Darken()
// equivalent (&c).Darken()
fmt.Println(c) // will print "{dark blue}"
Note that the Go compiler knows how to use the dot notation with both values and pointers, so in the above example, (*c).color
and c.color
are equivalent. In other words, there is no need to dereference the pointer inside the method. The compiler knows to dereference the pointer automatically. The reverse is also true, there is no need to reference the instance variable when used to invoke a method on it. Even though (&c).Darken()
would work in the example above, there is no need for it, c.Darken()
works equally well because the compiler knows what we mean.
The simpler syntax should be preferred.
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."
Structs as Receiver Types
It is very common to use structs as receiver types. You can take a struct, define it as a type, associate methods with that type, by declaring the type as receiver type on functions, and you would get what you normally think as a class in other programming languages.
Encapsulation
Encapsulation in this context is giving access to data, but intermediated by the public methods that are defined with the data. This way, access to data is controlled.
Encapsulation can be implemented with package data.
Encapsulation can also be implemented 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, 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) GetColor() string {
return (*c).color
}
func (c *Color) SetColor(color string) {
(*c).color = color
}
...
var c Color
c.Init("blue")
fmt.Println(c.GetColor()) // prints blue
c.SetColor("red")
fmt.Println(c.GetColor()) // prints red
Polymorphism
Interfaces
Overriding
Inheritance
TO DEPLETE
Read and merge into document: