Go Functions: Difference between revisions
(→Naming) |
|||
Line 105: | Line 105: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=<span id=' | =<span id='No_Java-Style_Getters_and_Setters'></span><span id='Idiomatic_Function_Naming_Conventions'><span>Naming= | ||
< | |||
When consumed from other packages, the function references include package names, so in general, the function names should not be prefixed with the package name. See [[Go_Packages#Try_to_Make_Package_Names_and_Exported_Names_Work_Together|Try to Make Package Names and Exported Names Work Together]]. Go does not encourage Java-style getters and setters. For naming conventions around that, see: {{Go_Style#Getters_and_Setters|Go Style | Getters and Setters}} | |||
Also see: | |||
{{Internal|Go_Style#Naming|Go Style}} | {{Internal|Go_Style#Naming|Go Style}} | ||
Revision as of 22:40, 5 July 2024
External
Internal
Overview
A function is a block of instructions, grouped together, and that optionally have a name.
Functions exist for code reusability reasons: the function is declared once and then can be invoked any number of times. The functions can be reused from within the project, or from other projects, if the function is declared as part of a package that is imported in the project that needs to use the function.
Function exist for abstraction reasons: they hide details into a compact "packaging" and improve the understandably of the code.
Declaration
A statical function declaration starts with the func
keyword followed by the function name and a mandatory parentheses pair. Note that functions can also be created dynamically.
func <function_name>([parameters]) [(return_declaration)] {
// body
[return [return_values]]
}
func someFunction(color string, size int) (float64, error) {
//
// body
//
var result float64
var err error
// ...
return result, err
}
Parameters
The parentheses optionally enclose function parameters. A function may not have any parameters, but in this situation, the parentheses must still be provided. The parameters, when exist, are vehicles for the input data the function needs to operate on. The parameter declaration syntax consists in a set of variables listed after the function name, between parentheses. Parameters become local variables to the function, scoped to the function body.
...(<par_name_1> <type>, <par_name_2> <type>, ...)
Example:
func blue(x int, s string) {
...
}
If there are multiple parameters of the same type, they can be provided as a comma separated list postfixed by the type:
func blue(x, y int, s string) {
...
}
Also see:
Return Declaration
The function output must have a type (or types), which are listed in the function declaration after the parameter list.
func ...(...) <return-type> {
...
}
If the function has more than one return values, their types must be enclosed in parentheses.
func ...(...) (<return-type-1>, <return-type-2>, ....) {
...
}
Function Body
Parameters are local variables visible inside the function body.
Go functions allow new local variables to be declared, inside the function, with the short variable declaration. The short variable declaration is not allowed anywhere else, except a function body.
The return Statement
The function returns its output value(s) with the return
keyword:
{
...
return someVar
}
More than one values can be returned at the same time, and such a function can be used with the multi-value short variable declaration form.
{
...
return someVar1, someVar2
}
Normally, one or more variable names are specified after the return
statement. The function will return the values associated with those variables, with the implication that the variables have been initialized before the return
statement. However, the value can be initialized in the return statement itself, like in this commonly-used pattern for constructor functions:
func NewBox(color string, ...) *Box {
return &Box{
color: color,
...
}
}
Naked return
A return
statement without any arguments returns the named return values. It is known as a "naked return":
func someFunc(...) (result string, err error) {
result = "..."
return // will return (result, err)
}
Naming
When consumed from other packages, the function references include package names, so in general, the function names should not be prefixed with the package name. See Try to Make Package Names and Exported Names Work Together. Go does not encourage Java-style getters and setters. For naming conventions around that, see: Template:Go Style Also see:
Invocation
All functions, except main()
must be invoked explicitly from the program to execute.
A function is invoked, or called, by specifying the function name, mandatory followed by open parentheses, optionally followed by arguments, if the function has parameters, then mandatory followed by closing parenthesis.
result, err := someFunction("blue", 3)
Arguments
The arguments consist of the data supplied to the function as part of the invocation.
Pass-by-value vs. pass-by-pointer vs. pass-by-reference
“Passing” in this context refer to how arguments are passed to parameters during a function call.
When a function is invoked, the function arguments are copied on the call stack.
In languages other than Go, the availability of reference variables allows for a “pass-by-reference” semantics, which means that even though a reference variable provided as argument is copied on the stack, the copy will still refer to the same target instance as the original function argument. The copied variable becomes just one more alias variable. The function is now in the position to the target instance directly, should it choose so.
This semantics is not available in Go. In Go, function arguments are always passed by value, there is no other choice, and the fact that Go does not have reference variables makes it impossible for the function to modify the original values. When the call arguments are copied on the function’s stack, the function always deals with copies, no reference variables exist, and this makes the function unable to change the values present outside the function.
This approach promotes encapsulation: the function cannot modify the original data. The called function cannot changed the variables inside the calling function. On the flip-side, the approach comes at the cost of the argument copy time, which for large pieces of data, can be non-trivial. In the particular case of arrays, they are passed by value, like everybody else, their data is always copied, and for large arrays this can be a problem. This is the reason array should not be used directly, but slices should be used instead.
A manual alternative to “pass-by-reference” can be implemented by passing a pointer to a variable as argument. This mechanism is called pass-by-pointer. Go materials refer to it as “pass-by-reference”, but this is not technically correct. This is how pass-by-pointer is implemented:
func passByPointerExample(i *int) {
*i = *i + 1
}
func callerFunction() {
x := 2
passByPointerExample(&x)
fmt.Println(x) // will print 3
}
The same considerations apply when returning results from a function. The result can be returned by value or by pointer. One practical advantage of returning by pointer is that the function can return nil
instead of allocating an "empty" value and returning it when the function errors out.
Also see:
main()
An executable program in Go must have a main()
function, where the program execution starts. The main()
function must be declared in the main
package. You never call this function. When a program is executed, the main()
gets called automatically.
package main
import "fmt"
func main() {
fmt.Printf("hello\n")
}
main()
is an implicit goroutine. For more details on main()
behavior as goroutine, and the interaction of other goroutines with main()
, see:
For more details on the main
package and Go executables, see:
init()
Variadic Functions
Variadic function are functions with a variable number of parameters. They are declared with an ellipsis ...
that precedes the type of the parameter. Inside the function, the parameter is treated like a slice. There could be just one variadic argument per function, which must be the final argument in the list.
func SomeVariadicFunc(s string, i ...int) {
...
for _, v := range i {
...
}
}
When invoked, such a variadic function can get a list of arguments, separated by commas:
SomeVariadicFunc("blue", 3, 5, 7, 11, 13, 17)
An equivalent invocation uses a slice and the ellipsis ...
to allow using the slice as argument:
si := []int{3, 5, 7, 11, 13, 17}
SomeVariadicFunc("blue", si...)
defer - Deferred Function Calls
Go syntax allows specifying that a function invocation should be deferred after the function that invokes it finishes executing. This behavior is achieved using the defer
keyword. This mechanism is usually used to clean things up after the enclosing function completes, close files, etc.
Deferred invocation without arguments:
defer func() {
fmt.Printf("do something\n")
}()
Deferred invocation with arguments:
defer closeDatabase(ctx, conn)
It is important to remember is that the deferred function arguments will be evaluated when the defer
statement is executed, NOT when the deferred function is executed.
More than one defer
statement can be specified in a function, and the order in which the deferred functions are executed is the inverse of the order in which the defer
statements have been declared (LIFO): the function declared with the last defer
statement is executed first, etc.
The following example prints:
EnclosingFunction 30
DeferredFunction 20
The example prints "EnclosingFunction 11" first because even if fmt.Println("EnclosingFunction", i)
statement is executed last in EnclosingFunction()
, DeferredFunction()
is executed after EnclosingFunction()
completes.
The example prints "DeferredFunction 20" last because first because DeferredFunction()
is executed after EnclosingFunction()
completes. However the integer value is the result of the argument evaluation at the time the defer
statement is executed (10 + 10), and not at the time DeferredFunction()
is executed, when it should have been 10 + 20 + 10 = 40.
func EnclosingFunction() {
i := 10
defer DeferredFunction(i + 10)
i += 20
fmt.Println("EnclosingFunction", i)
}
func DeferredFunction(i int) {
fmt.Println("DeferredFunction", i)
}
func main() {
EnclosingFunction()
}
defer and Error Handling
If the function being deferred returns an error value, not handing it is a mistake. Usually, IDE static analysis flags it as a strong warning. The error returned by deferred functions can be handled in a closure:
...
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
// handle the error here
log.Printf("%v\n", err)
}
}(resp.Body)
defer and panic
The function invoked with defer
execute even in case of panic
. More research required.
Built-in Functions
Built-in functions are available by default, without importing any package. Their names are predeclared function identifiers. They give access to Go's internal data structures. Their semantics depends on the arguments.
Length and Capacity
len()
len()
returns string length, array length, slice length and map size.
cap()
cap()
returns slice capacity.
Complex Number Manipulation
make()
make(()
is a built-in function that can be used to initialize slices, maps and channels.
TO DO: Continue to Distribute These Built-in Functions
Allocation: new()
Appending to and copying slices: append(), copy()
Deletion of map elements delete()
Handling panics panic(), recover()
Built-in functions for type conversions.
Functions as First-Class Values
Go implements features of a functional programming style. Function are treated as first-class value, which means functions are treated like any other type. Variables can be declared to be of a function type.
Function instances can be created dynamically at runtime, as opposite to declaring them statically in the program with the func
keyword. Functions can be passed as arguments into a function invocation and they can be returned as values of a function invocation. Functions can be stored into a data structure.
Declaring a Function Variable
The variable name acts as an alias, another name for the function.
var <var_name> <func_type>
A function type is declared using the following syntax:
func(<parameter_1_type, parameter_2_type, ...) (return_1_type, return_2_type, ...)
Example:
func SomeFunction(s string, i int) (int, error) {
...
}
...
var f func(string, int) (int, error)
f = SomeFunction
f("10", 20)
Passing a Function as Argument into a Function
func <func_name>(<func_var> <func_type_declaration>, ...) ... {
...
}
Example:
func Invoker(f func(string) int, s string) int {
result := f(s)
return result
}
func SomeFunction(s string) int {
i, _ := strconv.Atoi(s)
return i
}
...
result := Invoker(SomeFunction, "10")
...
Anonymous Functions (Lambdas)
These are lambdas.
In the example for Passing a Function as Argument into a Function, we can simply create the function passed as argument on the fly, without giving it a name:
result := Invoker(func(s string) int {
i, _ := strconv.Atoi(s)
return i
}, "10")
Returning Function as Result of Function Invocations
func FunctionMaker() func(int) int {
// we are creating an anonymous function
f := func(i int) int {
return i + 1
}
return f
}
Environment of a Function
The concept of environment of a function is important for understanding closures.
The environment of a function is the set of all names that are accessible from inside the function. Variables are a subset of those names. Given that Go is lexically scoped, the environment of a function includes the names defined locally in the function's own block and the enclosing blocks. A variable's scope is a somewhat similar concept, and scope and environment are some times used interchangeably when it comes to functions, but they are technically not the same. Functions have environments, variables have scope.
Closures
A closure is a function and its environment, which, as described in the Environment of a Function section, includes all the names declared within the function and all its enclosing blocks.
When a function is passed as an argument, the environment goes along with the function. When the function passed as argument is executed, the function has access to its original environment.
In the following example, we show that passing a function of an argument it passes the closure of the function: the function and its environment. The anonymous function that is passed as argument has access to its own localVar
local variable, the functionMakerVar
, declared in the block that also defines the anonymous function, and the package-level packageLevelVar
. When the anonymous function is invoked from within FunctionInvoker()
, whose environment does not have access to functionMakerVar
variable, the anonymous function still has access to all the variables it had access to while declared, and returns 10 (the argument) + 20 (localVar
in the anonymous function) + 30 (functionMakerVar
variable in the FunctionMaker()
block) + 40 (packageLevelVar
) = 100:
...
var packageLevelVar int = 40
func FunctionMaker() func(int) int {
functionMakerVar := 30
f := func(i int) int {
localVar := 20
return i + localVar + functionMakerVar + packageLevelVar
}
return f
}
func FunctionInvoker(f func(int) int, arg int) int {
return f(arg)
}
func main() {
f := FunctionMaker()
r := FunctionInvoker(f, 10)
fmt.Println(r) // will print 100
}
Elements of Style
Strive to write your functions so it enhances the understandability of your code. A code is understandable if, when you are in the position to find a feature, you can find it quickly. In general, you should be able to answer fast to question of type "Where is the code that does something?"
Avoid global variables. Without global variables, data is local to function.
Name functions and variables meaningfully. You don't want the names to be too long. "If you want to work with people, naming is really important".
Limit the number of parameters. A large number of parameter is a symptom of bad functional cohesion.
Functional cohesion: a function should perform only one operation.
Functional complexity: don't make function too complex: lines of code, control-flow complexity.
To reduce control-flow complexity, separate conditionals in different functions.
Methods and Support for Object Orientation
Overloading
Go does not have function or method overloading. Go Language FAQ says that "Method dispatch is simplified if it doesn't need to do type matching as well. Experience with other languages told us that having a variety of methods with the same name but different signatures was occasionally useful but that it could also be confusing and fragile in practice. Matching only by name and requiring consistency in the types was a major simplifying decision in Go's type system."
The most useful feature of overloading, the call of function with optional arguments and inferring defaults for those omitted can be simulated using variadic functions.