Go Language Object Oriented Programming: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
 
(66 intermediate revisions by the same user not shown)
Line 10: Line 10:
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 "[[Object-Oriented_Programming#Class|class]]", there is no <code>class</code> keyword in the language. However, Go allows associating data with methods, which what a class in other programming languages really is.  
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 "[[Object-Oriented_Programming#Class|class]]", there is no <code>class</code> keyword in the language. However, Go allows associating data with methods, which what a class in other programming languages really is.  


Go permits associating an arbitrary [[Go_Language#Local_Type|local type]] with arbitrary functions, turning them into methods of that type. The local type can be a [[Go_Language#Type_Alias|type alias]], a <code>[[Go_Structs#Overview|struct]]</code> or a <font color=darkkhaki>function</font>.
Go provides syntax to associate an arbitrary [[Go_Language#Local_Type|local type]] with arbitrary functions, turning them into [[#Method|methods]] of that type. The local type can be a [[Go_Language#Type_Alias|type alias]], a <code>[[Go_Structs#Overview|struct]]</code> or a [[#Functions_as_Receiver_Types|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 <code>struct</code> as data encapsulation mechanism, and then associate the struct with functions, by declaring the struct type as the [[#Receiver_Type|receiver type]] for the function, turning it into a [[#Method|method]] 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.
<span id='Structs_as_Receiver_Types'></span><span id='Structs_as_Receiver_Type'></span>The most common pattern for implementing object-orientation is to use a <code>struct</code> as data encapsulation mechanism, optionally declaring fields as [[Go_Packages#Unexported_Members|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|receiver type]] for the function and the functions into [[#Method|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.


<span id='Polymorphism'></span>Go does not have formal [[Object-Oriented_Programming#Inheritance|inheritance]], constructors or generics. Inheritance can be replaced to a certain extent by composition, [[Go_Structs#Embedded_Fields|struct field embedding]] and [[Go_Interfaces#Overview|interfaces]], which support code reuse and [[#Polymorphism|polymorphism]].
<span id='Polymorphism'></span>Go does not have formal [[Object-Oriented_Programming#Inheritance|inheritance]], constructors or generics. Inheritance can be replaced to a certain extent by composition, [[Go_Structs#Embedded_Fields|struct field embedding]] and [[Go_Interfaces#Overview|interfaces]], which support code reuse and [[#Polymorphism|polymorphism]].
Line 18: Line 18:
=<span id='Method'></span>Methods=
=<span id='Method'></span>Methods=


A '''method''' of a type is a function that was associated with the type, by declaring a [[#Receiver_Type|receiver type]] on the function, with the special syntax described below. If a method name starts with an uppercase character it is automatically [[Go_Language#Exported_Identifiers|exported]].
A '''method''' of a type is a function that was associated with the type, by declaring the type to be a [[#Receiver_Type|receiver type]] for that function, with the special syntax described below. If a method name starts with an uppercase character it is automatically [[Go_Language#Exported_Identifiers|exported]].


=Receiver Type=
=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|value receiver type]] or [[#Pointer_Receiver_Type|pointer receiver type]].
A '''receiver type''' for a function is a type that is associated with the function with dedicated syntax:


<span id='Implicit_Argument'></span>For both [[#Value_Receiver_Type|value receiver types]] and [[#Pointer_Receiver_Type|pointer receiver types]], the function gets an '''implicit argument'''. In case of a value receiver type, the implicit argument is a value, for a pointer receiver type, the implicit argument is a pointer.
<syntaxhighlight lang='go'>
func (<receiver_parameter_name> <receiver_type|receiver_type_pointer>) function_name(...) ... {
  ...
}
</syntaxhighlight>
 
Declaring a receiver type turns the function into a [[#Method|method]] of the type. Depending on whether we want to be able to modify the receiver instance in the method or not, the receiver type can be declared as a [[#Pointer_Receiver_Type|pointer receiver type]] or a [[#Value_Receiver_Type|value receiver type]].
 
<span id='Implicit_Parameter'></span>Regardless of whether we use a pointer or a value receiver type, the function gets an '''implicit parameter''', which is a pointer to the instance in case of a pointer receiver type, and a value of the type in case of a value recover 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:
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
func (<receiver_type_parameter_name> <receiver-type>) <function-name>(...) ... {
func function_name(<implicit_receiver_parameter> <receiver_type|receiver_type_pointer>, ...) ... {
  ...
}
</syntaxhighlight>
 
We call that '''receiver parameter'''. It is, loosely, the logical equivalent of the <code>[[Python_Language_OOP#The_self_Parameter|self]]</code> function parameter in Python and the implicit method parameter <code>this</code> 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.
 
As we write idiomatic Go code, it is common to use the first letter or a short abbreviation of the type as the name of the receiver. In our case, if the name of the type is <code>Color</code>, the name of the parameter is <code>c</code> or even <code>color</code>. We might even consider naming the receiver parameters with abbreviations of the interfaces they represent. See  [https://blog.heroku.com/neither-self-nor-this-receivers-in-go#naming-the-receiver this article] for arguments against naming that parameter "this" or "self".
 
==Value Receiver Type==
The syntax to declare a value receiver type for a function is:
<syntaxhighlight lang='go'>
func (<receiver_parameter_name> <receiver_type>) <function_name>(...) ... {
   ...
   ...
}
}
Line 40: Line 57:
}
}


func (c Color) GetADarkerShade() string {
func (c Color) SomeMethod() string {
   return "dark " + c.color  
   return "dark " + c.color  
}
}
</syntaxhighlight>
</syntaxhighlight>


In the example above, <code>Color</code> is the receiver type, and <code>c</code> is the variable that refers to the particular receiver type instance the method was invoked on.
In the example above, <code>Color</code> is the receiver type, and <code>c</code> 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 invocation of the method on the receiver type instance is done with the usual '''dot notation''':
The method is invoked on the receiver type instances with the usual dot notation syntax:
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
color := Color{"blue"}
color := Color{"blue"}
result := color.GetADarkerShade() // result will be assigned with "dark blue"
result := color.SomeMethod() // the method will return "dark blue"
</syntaxhighlight>
</syntaxhighlight>


The invocation with the dot notation makes the instance being invoked on (<code>color</code>) an '''implicit argument''' (<code>c</code>) of the method: there is no parameter in the <code>GetADarkerShade</code> method signature, yet the method body has access to <code>c</code>. This  makes <code>c</code> an implicit argument of the method.
The invocation copies the <code>color</code> value into in the <code>c</code> variable inside the function, making <code>c</code> an [[#Implicit_Parameter|implicit parameter]] of the method. There is no formal <code>c</code> function parameter in the signature, yet the method body has access to <code>c</code>, which behaves similarly to any other function parameter. That is why the following syntax makes sense, and it also works. The syntax confirms that <code>SomeMethod()</code> is a method associated with the type <code>Color</code>, and when invoked with the instance of the type as its first (implicit) parameter, it behaves exactly as expected:
 
<syntaxhighlight lang='go'>
The dot notation works with both values and pointers. The compiler knows how to handle that transparently.
color := Color{"blue"}
Color.SomeMethod(color)
</syntaxhighlight>


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]].
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|pointer receiver type]] must be used if instead to modify the target.


===Pointer Receiver Type===
==Pointer Receiver Type==
To modify the receiver instance, use a pointer receiver type.
The syntax to declare a pointer receiver type for a function is:


<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
func (<receiver_type_parameter_name> *<receiver_type>) <function_name>(...) ... {
func (<receiver_parameter_name> *<receiver_type>) <function_name>(...) ... {
   ...
   ...
}
}
Line 70: Line 89:
Example:  
Example:  
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
func (c *Color) Darken() {
func (c *Color) SomeOtherMethod() {
   c.color = "dark " + c.color
   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 = "dark" + (*c).color is an equivalent, longer syntax. The compiler can handle the simpler syntax
}
}


...
...
c := Color{"blue"}
 
c.Darken()
color := Color{"blue"}
// equivalent (&c).Darken()
color.SomeOtherMethod() // equivalent with (&color).SomeOtherMethod()
fmt.Println(c) // will print "{dark blue}"
fmt.Println(color) // will print "{dark blue}"
</syntaxhighlight>
</syntaxhighlight>


Note that the Go compiler knows how to use the dot notation with both values and pointers, so in the above example, <code>(*c).color</code> and <code>c.color</code> 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 <code>(&c).Darken()</code> would work in the example above, there is no need for it, <code>c.Darken()</code> works equally well because the compiler knows what we mean.
In the example above, a pointer to a <code>Color</code> 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 <code>Color{"blue"}</code> value into in the <code>c</code> variable inside the function, making <code>c</code> an [[#Implicit_Parameter|implicit parameter]] of the method. Similarly to the value receiver type syntax, there is no formal <code>c</code> function parameter in the signature, yet the method body has access to <code>c</code>, which behaves similarly to any other function parameter. That is why the following syntax makes sense, and it also works. The syntax confirms that <code>SomeMethod()</code> is a method associated with the type <code>*Color</code> (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:
<syntaxhighlight lang='go'>
color := Color{"blue"}
(*Color).SomeOtherMethod(&color)
</syntaxhighlight>
 
===Invoking on <tt>nil</tt> Pointers===
Since pointers are involved, methods declared with pointer receiver types may be invoked on a <code>nil</code> pointers, which will lead to a "invalid memory address or nil pointer dereference" panic:
<font size=-2>
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
</font>
 
If the implementation semantics allows it, the method may guard against <code>nil</code> pointers:
<syntaxhighlight lang='go'>
func (c *Color) SomeOtherMethod() {
  if c == nil {
    return
  }
  c.color = "dark " + c.color
}
</syntaxhighlight>


The simpler syntax should be preferred.
==Dot Notation works with Both Values and Pointers==
The Go compiler knows how to use the dot notation with both values and pointers.  


===Mixing Value and Pointer Receiver Types===
There is no need to invoke a method defined with a pointer receiver type with:
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."
<syntaxhighlight lang='go'>
(&variable).SomeMethod()
</syntaxhighlight>
This will work:
<syntaxhighlight lang='go'>
variable.SomeMethod()
</syntaxhighlight>
Similarly, inside the method, there is no need to dereference the pointer receiver type parameter:
<syntaxhighlight lang='go'>
func (v *SomeType) SomeMethod(...) {
  (*v).someField // not necessary
}
</syntaxhighlight>
This will work:
<syntaxhighlight lang='go'>
func (v *SomeType) SomeMethod(...) {
  v.someField
}
</syntaxhighlight>


=<span id='Structs_as_Receiver_Type='></span>Structs as Receiver Types=
==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:


It is very common to use [[Go_Structs#Overview|structs]] as [[#Receiver_Type|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.
<font size=-2>
  Struct Color has methods on both value and pointer receivers. Such usage is not recommended by the Go Documentation.
</font>


=Functions as Receiver Types=
==Functions as Receiver Types==


<font color=darkkhaki>TODO, example here: https://go.dev/blog/error-handling-and-go#simplifying-repetitive-error-handling</font>
<font color=darkkhaki>TODO: https://go.dev/blog/error-handling-and-go#simplifying-repetitive-error-handling</font>


=Encapsulation=
=Encapsulation=

Latest revision as of 00:21, 14 May 2024

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 the function and the functions into 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 not have formal inheritance, constructors or generics. Inheritance can be replaced to a certain extent by composition, struct field embedding and interfaces, which support code reuse and polymorphism.

Methods

A method of a type is a function that was associated with the type, by declaring the type to be a receiver type for that function, with the special syntax described below. If a method name starts with an uppercase character it is automatically exported.

Receiver Type

A receiver type for a function is a type that is associated with the function with dedicated syntax:

func (<receiver_parameter_name> <receiver_type|receiver_type_pointer>) function_name(...) ... {
  ...
}

Declaring a receiver type turns the function into a method of the type. Depending on whether we want to be able to modify the receiver instance in the method or not, the receiver type can be declared as a pointer receiver type or a value receiver type.

Regardless of whether we use a pointer or a value receiver type, the function gets an implicit parameter, which is a pointer to the instance in case of a pointer receiver type, and a value of the type in case of a value recover 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.

As we write idiomatic Go code, it is common to use the first letter or a short abbreviation of the type as the name of the receiver. 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".

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.

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)

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 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

Polymorphism is available in Go, but it is implemented a bit differently than in other object-oriented languages, in that it does not imply inheritance, which is not available in Go. Polymorphism in Go is implemented using interfaces.

For a generic discussion on polymorphism, see:

Object-Oriented Programming | Polymorphism

Interfaces

Go Interfaces

Overriding

Inheritance

TO DEPLETE

Read and merge into document: