Go Concepts - Functions

From NovaOrdis Knowledge Base
Revision as of 01:46, 19 December 2023 by Ovidiu (talk | contribs) (→‎TO DEPLETE)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

TO DEPLETE

Deplete to Go Functions

External

Function Definition

A function declaration starts with the func keyword, followed by the function name and its signature: input parameters and the results, followed by a series of statements in a block. If the function does not return anything, the result declaration can be missing.

The statements are applied on a number of inputs, resulting in a number of outputs. Same inputs will always result in the same outputs. Function do not depend on a type instance's state. From this perspective, functions are conceptually different from methods.

Function Signature

The parameters and the return types are known as the function's signature.

Syntax

func <name>(
    [parameter-identifier1] [type1], [parameter-identifier2] [type2], ...) 
    [(<return-type1>, [return-type2], ...)] {

   // function's body
   statement1
   statement2
   ...

   return <return-value1>, <return-value2>
}
func add(a int, b int) int {
   return a + b;
}

The return type declarations are optional, if the function does not return anything.

Multiple Return Values

A function may return one or more results:

func add(a int, b int) (int, int) {
   return a + b, a - b;
}

It is idiomatic for Go to return an error value among the return values, see:

Error Handling Idioms

Return Types can Have Names

func f() (r int) {
   r = 1
   return
}

Varidic Functions

Variadic parameters are declared using ellipsis (...). A function can only have one variadic parameter.

func f(args ...<type>) <return-type> {

    //
    // range can be used to iterate over the individual parameters
    //
   for _, arg := range args {
        // ...
   }
}

Also see:

range

A slice can be "converted" into its individual components which can be passed as individual arguments to a variadic function, by appending an ellipsis (...) to the slice identifier:

s := []int{1, 2, 3}
f(s...)

Passing Variadic Arguments from an Enclosing Function to an Enclosed Function

When we want to pass the variadic arguments of an enclosing function, we need to use this syntax:

func enclosing(args ...int) {
   ...
   enclosed(args ...)
}

func enclosed(args ...int) {
   ...
}

Note the use of ellipsis (...) when passing the args... variadic argument to enclosed().

Function Literals

There are cases when a function definition is needed in-line - to be passed as argument to another function, for example. In this case we use a function literal, which consists in the func keyword, followed by a signature, and the body. No name is required. Example:

func (i int, j int) int {
    return i + j
}

Also see:

Closures
Function Types

Anonymous Functions

An anonymous function is a function that is declared without a name. Anonymous functions are useful in implementing closures.

Anonymous function invocation example:


a := 10

func (i int) {
    fmt.Println(i)
}(a)

Functions and Variable Scopes

A function does not have access to variables declares in functions that call it.

A function does have access to package level variables declared in the function's package.

Also see:

Variable Scopes

Pass by Value vs. Pass by Reference

In Go, all variables are passed by value: when an argument is passed to a function, that argument is copied on the stack.

This is true for built-in types, including arrays, and for user defined types (structs). Because of that, if you want to change a struct inside a function, you should pass a pointer to that struct, not the struct itself.

Even for pointer variables, since the value of the pointer is a memory address, passing pointer variables is still considered pass by value.

Closures

Go Closures

defer

The defer keyword schedules the function it is invoked with to be executed immediately after the function it is invoked from exits, irrespective of how the function exits (even with a panic). The execution is done on the same thread.

defer is used in Go idioms where a resource needs to be closed after it was opened: it keeps the "close" call near the "open" call, so the code is easier to understand. The recommended way to do it is:

f, _ := os.Open(filename)
defer f.Close()

Special Functions

The init() Function

The init() function contains code executed on package initialization. All init() functions in any code file that is part of the program will be called before the main() function.

Each package has the ability to provide as many init() functions as necessary.

A way to trigger package initialization code to execute, even if none of the package identifiers is used in the source code, is to use the blank identifier _. For more details see using the blank identifier when importing a package.

An example of where init() is useful is with database drivers. They register themselves with the sql package with their init() function is executed at startup.

The main() Function

A main() function that takes no arguments and returns no value is the entry point in a Go program. Once the main() function exists, the program exits, regardless of whether there are other in-flight goroutines. More about the main() function, the main package and Go executables can be found here: compiling an executable.

Conversion between Types

Conversions between types look like function invocations, see Conversion between Types.

Methods

A method defines the behavior of a type, and it relies on the state of an instance of the type. The method will - and it is supposed to - change the state. From this point of view, the method is conceptually different from a function.

Lexically, methods are declared independently from the struct or the aliased type they are related to: they can declared in a different section of the file from where the type is declared, or in a different file altogether, as long as that file belongs to the same package.

A method is always [Go_Language_Modularization#Exported_Identifiers|exported]] by the package it is enclosed in.

Syntax

A method declaration is similar to a function's, except that in between the func keyword and the name of function we add an extra parameter named receiver. The receiver syntax is similar to a function's parameters - (name type):

func (<receiver_declaration>) <method-name> (...) {

    // the rest of declaration is similar to a function's
    ...
}

Example:

func (bag Bag) changeColor(color string) {
    ...
}
func (bagPtr *Bag) changeColor(color string) {
    ...
}

Receivers

A receiver is what turns a simple function into a method - the presence of the receiver with a function declaration indicates that the function becomes a method bound to the specified receiver type.

Value and Pointer Receivers

When declaring receivers, both a value or a pointer can be used in the declaration. The declarations are referred to as value receiver and pointer receiver respectively. For implications of using pointers vs. values, see Receiver Declaration and Changing the State of the Instance the Method is Invoked Upon.

Regardless of declaration, the method invocation will work on both target values and pointers. The compiler will reference or dereference as necessary:

type thing struct {
    color string
}

//
// accessor - in this case pointer/value does not matter
//
func (t thing) displayFromValue() {
    fmt.Println("color", t.color)
}

//
// accessor - in this case pointer/value does not matter
//
func (tPtr *thing) displayFromPointer() {
    fmt.Println("color", tPtr.color)
}

...

t := thing{"blue"}

//
//  all these work fine
//

t.displayFromValue()
t.displayFromPointer()
(&t).displayFromValue()
(&t).displayFromPointer()

Receiver Declaration and Changing the State of the Instance the Method is Invoked Upon

If the method is supposed to change the state of the instance is invoked upon, a pointer receiver declaration must be used.

Invocation itself can be made both into a pointer or the value, both options will work and the state will be changed - as long as a pointer receiver is used in the method definition.

If a value receiver is used, the mutator will execute without any compile-time or runtime error, but the state will not be changed. The explanation is that a copy of the receiver is transferred to the method. For a value receiver, a copy of the value is transferred to the method, and the method mutates its own private copy; the change is not reflected on the value the copy was made from. For a pointer receiver, a copy of the pointer is transferred to the method, so the method mutates the original value - via its (copy of the) pointer.

Good mutator:

//
// good mutator: pointer receiver
//
func (tPtr *thing) changeColor(newColor string) {
    tPtr.color=newColor
}

...

t := thing{"red"}

// will display "red"
fmt.Println(t.color)


//
// both invocations (on value and on pointer) are valid
//

t.changeColor("blue")

// will display "blue"
fmt.Println(t.color)

(&t).changeColor("green")

// will display "green"
fmt.Println(t.color)

Invalid mutator:

//
// invalid mutator: value receiver
//
func (t thing) invalidChangeColor(newColor string) {
    t.color = newColor
}

...

t := thing{"red"}

// will display "red"
fmt.Println(t.color)

//
// both invocations (on value and on pointer) will execute but the invocation won't change the state
//

t.invalidChangeColor("blue")

// will display "red"
fmt.Println(t.color)

(&t).invalidChangeColor("green")

// will display "red"
fmt.Println(t.color)

Receivers and Interfaces

Unlike when you call methods directly from values and pointers, when you call a method via an interface type value, the rules are different. Methods declared with pointer receivers can only be called by interface type pointers. Methods declared with value receivers can be called by interface type values and pointers. For an explanation on why this is see Method Set associated with the Pointers of a Type.

Method Sets

See:

Method Sets

Receiver Best Practice

The decision to use a value or pointer receiver should not be based on whether the method is mutating the receiving value, it should be based on the primitive vs. non-primitive nature of the receiver type.

An exception is when you need the flexibility that value type receivers provide when working with interface values (see Receivers and Interfaces above). In this case you may choose to use a value receiver even though the nature of the type is non-primitive.

Method Invocation

Once a method was associated to the type, the type's instances or pointers to the type's instances allow invocation of the method using the . operator. The . operator works with both the instance and the pointer. For the method declaration above, both invocations that follow are valid:

var b Bag
bPtr := &b

b.changeColor("red")
bPtr.changeColor("blue")