Go Methods

From NovaOrdis Knowledge Base
Jump to navigation Jump to search

External

Internal

Overview

Go allows associating arbitrary behavior with built-in or custom types, which contributes to the object-oriented character of the language. Note that Go is not a fully object-oriented language, it misses type inheritance, for example.

Syntactically, the association of the behavior with the type is done by declaring a function that encapsulates the behavior we want to add to the type, and adding a receiver to its signature:

func (r ReceiverType) FunctionName(parameters, ...) (return_declaration) {
 ...
}

If the name of the method starts with an uppercase letter, the method is automatically exported by the package. As result of this association, the function becomes a method of the type and a member of the method set of that type.

The declaration is identical to that of a regular function, with the exception of the receiver parameter, which precedes the function name. The receiver parameter gives the method's body access to the instance of the associated type. Aside from its special syntactical position, all other aspects the receiver parameter is identical with the regular parameters of the function.

An aspect that has profound implications on the relationship between the method and the type instance is whether the receiver parameter is a value or a pointer. The deciding factor should be whether the method is intended to change the state of the receiver. Use pointer receivers if you intend to let the method modify the state of the receiver instance. See Deciding between Value or Pointer Receiver.

When naming a method, consider if the name clashes with well-known methods names. See Method Naming for more details.

Receiver

The receiver is the instance to invoke the method on. It is passed to the method as an implicit parameter, which does not differ in any way from the "regular" parameters. We call it the receiver parameter. Loosely, it is 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. This allows an alternative valid syntax where the instance being invoked on can be provided as the first parameter of the function, as shown here.

As with the regular parameters, the receiver parameter can be a value or a pointer, and it is called a value receiver or a pointer receiver, respectively.

Value Receiver

func (r ReceiverType) MethodName(parameters, ...) (return_declaration) {
   ...
}

A value receiver gives access to the copy of the value the method is invoked on, inside the method. As such, the original value outside the method cannot be modified by the method.

The method is invoked with the usual dot notation syntax:

func (r SomeType) SomeMethod() {
    ...
}

s := SomeType{}
s.SomeMethod()

The invocation copies the value of the s variable as the value of the r method variable.

There is no formal r function parameter in the signature, yet the method has access to r, which behaves similarly to other function parameters. That is why the following syntax makes sense, and it also works. The syntax confirms that SomeMethod() is a method associate with the type SomeType and when invoked with the instances of the type as the first (implicit) parameter, it behaves as expected:

s := SomeType{}
SomeType.SomeMethod(s)

Pointer Receiver

func (r *ReceiverType) MethodName(parameters, ...) (return_declaration) {
   ...
}

A pointer receiver gives access to the variable the method is invoked on, inside the method. As such, the method may modify the state of the original variable.

Similarly to the value receiver case, there is no formal r function parameter in the signature, yet the method has access to r, which behaves similarly to other function parameters. That is why the following syntax makes sense, and it also works. The syntax confirms that SomeMethod() is a method associate with the type *SomeType and when invoked with a pointer of the type as the first (implicit) parameter, it behaves as expected:

s := SomeType{}
(*SomeType).SomeMethod(&s)

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
}

Deciding between Value or Pointer Receiver

The deciding factor should be whether the method intends to change the state of the receiver.

If the method is intended to modify the state of the receiver instance, use a pointer receiver. Use a value receiver otherwise. Since in this case the receiver argument is passed by value, the method will not be able to modify the original receiver, intendedly or erroneously.

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.

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:

Go Style

Functions as Receivers

TODO: https://go.dev/blog/error-handling-and-go#simplifying-repetitive-error-handling

Method Set

https://go.dev/ref/spec#Method_sets

A method set of a non-interface type determines the methods that can be called on the instances of that type. The method set of the non-interface type T is formally defined as the set of all methods declared with a receiver type T. Every type has a possibly empty method set associated with it.

The method set of the pointer to a non-interface type is different from the method set of the type. The method set of a pointer to a type T is formally defined as the set of all method declared with a receiver *T and T. The method set of the pointer to the type T always includes the method set of the type T. 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 always true. Given a value, not always we can get an address for it, and because we are not able to get a pointer, we cannot assume that the methods associated with the pointer will work. This is a code example that makes this point:

Method Set for Type and Method Set for Pointer to Type

The method set of an interface type is the intersection of the method set of each type in the interface's type set. The resulting method set is usually just the set of declared methods in the interface.

In a method set, each method must have a unique, non-blank method name.

Method Naming

There is a number of well-known methods names like Read, Write, Close, Flush, String, etc. that have canonical signatures and meanings. To avoid confusion, do not give your methods one of these names, unless it has the same signature and meaning. If indeed the method you are developing has the same meaning as a method on a well-known type, give it the same name and signature: your string converter method should be called String, not ToString.

Go Style

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 (r *SomeType) SomeMethod(...) {
  (*r).someField // not necessary
}

This will work:

func (r *SomeType) SomeMethod(...) {
  r.someField 
}

TODO

Continue and deplete Go_Language_Object_Oriented_Programming#Methods.