Hi everyone! this is Jimmy , and this is the third article in my series “Breaking Things with Go.” In this series, I document my journey through Jon Bodner’s Second Edition: Learning Go – An Idiomatic Approach to Real-World Go Programming and explore how to use Go in the most practical way I can

in this series the resources are the book itself, go documentation, and any AI model to clarify some things

lets Jump into it Jump Into A Hole Stickers - Find & Share on GIPHY


in Today’s break we’ll be going through Functions in GO

Basic Declaration

I’m not gonna yap about what is functions and why it exist so I will assume that everyone knows what function is and we’ll go from there let’s start with Basic Function declaration in Go

  • func func_name(Param_name param_type) return_type {//code}
  • if you will return something you have to specify its type and no return needed if you won’t
  • if you need to exit function before its last line (if it doesn’t return anything) you can use return (known as naked-return in Go)
  • if the function returns data you can pass it around directly or you can assign it to variable first then pass it using that variable’s name

so function of simple division should look like

go
func div(num int, denom int) int{ // takes two integers, and returns int type 
    if denom == 0 {
        return 0
    }
    return num / denom 
}

BTW if there is two parameters of two types declared after each other you can write the type once func div(num, deno int) int {}

and you can just call it div(3, 5) or assign it to variable result:=div(3,5) or pass it around fmt.Println(div(3,5))

Named and Optional Parameters

Go doesn’t support named or optional parameters but we can simulate it using structs with the fields that matches the desired Parameters

struct must be declared on package level

go
package main

type MyFuncOpts struct {
    FirstName string
    LastName  string
    Age       int
}

func MyFunc(opts MyFuncOpts) error {
    // do something here 
}
func main() {
    MyFunc(MyFuncOpts{ // others are set to their zero value
        LastName: "Patel",
        Age:      50,
    })
    MyFunc(MyFuncOpts{
        FirstName: "Joe",
        LastName:  "Smith",
    })
}

if you found yourself using too much optional and name parameters refactor your functions

Variadic Input Parameters and Slices

variadic input means a function can accept a variable number of arguments of the same type

we indicate variadic input Parameters using three dots ... before the type and it has to be the last parameter

go
func addTo(base int, vals ...int) []int {
    out := make([]int, 0, len(vals))
    for _, v := range vals {
        out = append(out, base+v)
    }
    return out
}

func main() {
    fmt.Println(addTo(3))             // doesn't loop so it returns out slice empty []
    fmt.Println(addTo(3, 2))          // 5
    fmt.Println(addTo(3, 2, 4, 6, 8)) // 5 7 9 11
    a := []int{2, 4, 6, 8}
    fmt.Println(addTo(3, a...))                 // 5 7 9 11
    fmt.Println(addTo(3, []int{2, 4, 6, 8}...)) // 5 7 9 11
}

Multiple return values

  • go supports multiple return values unlike some programming languages
  • if you return multiple values you have to specify the return type for all values in a parentheses separated by a comma and you must return all of them
  • don't put parentheses around the returned values

returning an error in function → returns error if something goes wrong other wise it will return nil for the error value

go
func div(num, denom int) (int, int, error) {
    if denom == 0 {
        return 0, 0, errors.New("Can't divide by zero")
    }
    return num / denom, num % denom, nil // return nil if no error returned above
}

func main() {
    result, remainder, err := div(5, 2) // assigned the returned values to variables
    if err != nil { // checks if there is error returned 
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println(result, remainder)
}

Note that Multiple returns are Values meaning :

  • in python we can return multiple values but they are returned in a tuple but go returns actual multiple values that each of them need a variable to be assigned to
    • if function returns three values you need to assign them to 3 variables, otherwise you’ll get an error
    • if you wanna ignore value use _ and it will be ignored
    • you can’t use _ and ignore all variables but you can drop the assignment in the first place and nothing happens div(4,2) without assignment but you’ll lose those values

Named return values

we can also name the returned values

  • they are pre-declared variables that we use within the function to hold the return values
  • written comma-separated list within parentheses (even if it's a single value)
  • initialized to their zero value on creation
  • if you don't need to name some of the variables use _ for its name
  • those names are local to the function only and doesn't enforce naming to the returned values assignment on calling
  • you can shadow them if you re-declared them inside the same function again (so if you don't wanna shadow them use = not :=)

here is an example

go
func div(num, denom int) (result, remainder int, err error) { // same as parameters same types can be combined using comma and write the type once 
    if denom == 0 {
        err = errors.New("Can't divide by zero")
        return result, remainder, err
    }
    result, remainder = num/denom, num%denom // gave a value but not declaration
    return result, remainder, err
}

func main() {
    x, y, z := div(5, 2)
    fmt.Println(x, y, z)
}

those variables are created to pre-declare the variables instead of declaring them inside the function but it is totally fine not to return them because you don't have to

  • you can name variables and then return the values directly without assigning them to variable like this
go
func div(num, denom int) (result, remainder int, err error) {
    err = nil
    result, remainder = 10, 20
    if denom == 0 {
        return 0, 0, errors.New("Can't divide by zero")
    }
    return num/denom, num%denom, nil
}

func main() {
    x, y, z := div(5, 2)
    fmt.Println(x, y, z) // 2, 1, <nil>
}

don’t get me wrong → don’t have to return them doesn’t mean you can leave them unused (this is an error remember! )

Blank/Naked return

and it is a return statement without specified values, used in a function that has named parameters and it returns the value of the current named variables for example

go
func sum(a, b int) (result int){
    result = a+b
    return //returns the current value of result which is the summation 
}

func summ(a, b int) (result int){
    return // returns the current value of result which is its zero value of int 0
}
  • don't use them cause they aren't good for readability and will leave developers confused

Functions are values

the function type is combination of func(parameters_types) (return types) so the next function type is func(string) (int, bool)

go
func parse(s string) (n int, ok bool) {
    if s == "" {
        return
    }
    n = len(s)
    ok = true
}

function’s type is also called functions signature → any function with same parameter number and type and return values has the same signature

go
func sum(a, b int) (result int) { // signature func(int, int) (int)
    result = a + b
    return result
}

func main() {
    var function_variable func(int, int) int // created function with same signatrue
    function_variable = sum                  // now i can assign that function variable to it and treat it as a function
    fmt.Println(function_variable(2, 3))     // 5
}

zero value for a function is nil and the attempt to run a nil function results a panic

we can declare a function type for example if i need to store a function as a value in a map so i have to specify its type but instead of typing its signature every time i can just declare it as type

go
// method 1 
var opMap = map[string]func(int, int)int {
    // map functions here 
}

// method 2 
type funcType func(int, int) int
var opMap = map[string] funcType{
   // map functions as funcType type here
}

Anonymous functions

go doesn't support nested functions if it isn't an anonymous function but you can declare an anonymous function inside another function and assign it to value or provoke it directly

assign it to a variable

go
func main() {
    add := func(a, b int) int {
        return a + b
    }
    fmt.Println(add(2, 3)) //5
}

we can can provoke them directly

go
func main() {
    fmt.Println(func(a, b int) int {
        return a + b
    }(2, 3)) //5
}

this isn't idiomatic but it is valid

you could also provoke directly and store in a variables (cause it returns something)

go
sum, prod := func(a, b int) (int, int) {
    return a + b, a * b
}(3, 4)

fmt.Println(sum, prod) // 7 12

you can declare variables that are assigned to anonymous functions at package scope and call them

go
var (
    add = func(i, j int) int {return i+j}
    sub = func(i, j int) int {return i-j}
)
func changeAdd(){
    add = func(i, j int) int {return i+j+j}
}
func main(){
    x := add(2, 3)
    fmt.Println(x) // 5
    changeAdd()
    y := add(2, 3) // 2+3+3
    fmt.Println(y) // 8

}

it isn't good to declare something on package level and then reassign it cause package level variables are supposed to be immutable which make is harder to understand if you changed it later

and i don't even know why would i do something like that if i can just define two separate functions both of them doing whatever they are supposed to do

Closure

closures are functions that captures and uses variables from its surrounding scope the closure could be an anonymous function

go
func main() {
    a := 20
    f := func() {
        fmt.Println(a)
        a = 30
    }
    f()
    fmt.Println(a)
}

for example f is an anonymous function and closure at the same time (cause it accesses a from the outside scope) you can shadow the variable a also because it is a new block

using := instead of = will create new a that cease to exist after the function ends

when to use closures ?

  • if there is a function used only inside one other function and called multiple times inside it so there is no need to declare it at package level for example here the helper function needed only for process function
go
func process(nums []int) []int {
    helper := func(x int) int {
        return x * 2
    }

    out := make([]int, 0, len(nums))
    for _, n := range nums {
        out = append(out, helper(n))
    }
    return out
}

Functions as Parameters

functions are values and its type as we said is its signature meaning we can pass it as a parameter to other functions for example the function sort.Slice it takes a slice and a function that tells how will it be sorted

go
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    people := []Person{
        {"Pat", "Patterson", 32},
        {"Lebron", "James", 40},
        {"bronny", "James", 23},
    }
    fmt.Println(people)
    sort.Slice(people, func(i, j int) bool {
        return people[i].LastName < people[j].LastName
    })
    fmt.Println(people)
}

and this sorts the slice based on the last name of the struct type it will take two indices i, j which are pair of integers and sort them then change the i, j to other values like it tries 0,1 1,2 0,2 and so on until it is sorted

return functions

we can return closure from a function

go
func makeMult(base int) func(int) int {
    return func(factor int) int {
        return base * factor
    }
}

func main() {
    twoBase := makeMult(2)   // return a functions that has base of 2
    threeBase := makeMult(3) // return a function that has base of 3
    for i := 0; i < 3; i++ {
        fmt.Println(twoBase(i), threeBase(i)) // iteration that will try each factor in the loop with the base
    }
}

high-order functions are functions that take functions as parameters or a return value

Defer

programs often create a temporary resources like files, network connections that need to be cleaned up this clean up has to happen no matter how many exit points we can leave it unclosed but it will create other issues

  • Your program runs out of file descriptors
  • You see "too many open files" errors
  • Connection limits are hit
  • Memory usage grows unexpectedly

the good thing about defer statement that it run the cleanup function after the function exists not matter how it exists normally, or panic, or error

go
func main() {
    if len(os.Args) < 2 { // os.Args returns a slice first item is the app name, what after that is arguments each as item
        log.Fatal("No specified file")
    }
    f, err := os.Open(os.Args[1]) // os.Open returns pointer to file object (descriptor) and error if failed nil if successful
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close() // will be stored in defer stack which will be executed only when the surrounding function exists (in this case main is the function)
    data := make([]byte, 2048) // create a buffer
    for {
        count, err := f.Read(data) // reads upto count bytes and stores in data buffer
        os.Stdout.Write(data[:count]) // read the data till count index
        if err != nil { 
            if err != io.EOF { // if error not end of file log it 
                log.Fatal(err)
            }
            break // break it if is an end of file 
        }
    }
}
  • defer can accept function, method or closures
  • defer has defer stack so they run LIFO last defer runs first

for example look at this function

go
func demonstrateDefer() {
    fmt.Println("Start")

    defer fmt.Println("First defer")
    defer fmt.Println("Second defer")
    defer fmt.Println("Third defer")

    fmt.Println("End")
}

func main() {
    demonstrateDefer() // runs the function
}

so it will go through the code normally and if it finds a defer it will stack them then call them after the demonstartedefer function ends output will be

go
start
end
third defer
second defer
first defer

defer could be closure like

go
defer func() int {
    return 2
}()

but there is no way to read the values returned by defer but it can modify the values of its surrounding function, look at this book example Screenshot 2026-01-14 040740.png

this tries to insert some values into database but after cleanup we will check if it caused error we will rollback otherwise we will commit

using defer with a function needs parentheses after the function braces or it will cause compile-time error

best practice is to separate the function by creating a utility that will open the file and return closure and we will get its value in another function

it looks longer but it is better for reusable code maybe i need to open a file and do something rather than reading it so it will come handy

Go is call by value

meaning supplying a variable to a function, GO takes a copy of that variable and doesn't affect the actual variable

this doesn't happen with slices and maps cause they have backing array because they are implemented with pointers they will change maps and slices are values in go but the value is a pointer not actual value and we’ll get to Pointers in the next break

Takes

  • Functions are first-class values in Go with types based on their signature and can be assigned to variables, passed as parameters, or returned from other functions
  • Closures are anonymous functions that capture variables from their surrounding scope and are useful when a helper function is only needed within one other function
  • The defer keyword schedules cleanup functions to run after the surrounding function exits in LIFO order which prevents resource leaks like open files or connections
  • Higher-order functions that accept or return other functions enable flexible design patterns like custom sorting behavior or dependency injection
  • Go passes function arguments by value meaning variables are copied but slices and maps appear to modify originals because they're implemented with pointers internally
  • Functions are first-class values in Go with types based on their signature and can be assigned to variables, passed as parameters, or returned from other functions
  • Closures are anonymous functions that capture variables from their surrounding scope and are useful when a helper function is only needed within one other function
  • The defer keyword schedules cleanup functions to run after the surrounding function exits in LIFO order which prevents resource leaks like open files or connections
  • Higher-order functions that accept or return other functions enable flexible design patterns like custom sorting behavior or dependency injection
  • Go passes function arguments by value meaning variables are copied but slices and maps appear to modify originals because they're implemented with pointers internally

Coming Next

the next break will dive more about Pointers and memory stuff so stick around for next break where we will break more stuff break is over, time to get back to work

feel free to reach out to me Peace Out GIFs | Tenor