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

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
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
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
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
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 happensdiv(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
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
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
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)
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
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
// 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
func main() {
add := func(a, b int) int {
return a + b
}
fmt.Println(add(2, 3)) //5
}
we can can provoke them directly
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)
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
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
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
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
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
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
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
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
start
end
third defer
second defer
first defer
defer could be closure like
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

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
func getFile(name string) (*os.File, func(), error) { // utility function to open the file only
file, err := os.Open(name) // tries to open the file
if err != nil { // check if there is an error
return nil, nil, err // return the error
}
return file, func() { // if we got here means there is no error and we will return file pointer, closer function, and nil for error
file.Close()
}, nil
}
func main() { // here we will do what we need with opened resource
f, closer, err := getFile(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer closer()
data := make([]byte, 2048)
for {
counter, err := f.Read(data)
os.Stdout.Write(data[:counter])
if err != nil {
if err != io.EOF {
log.Fatal(err)
}
break
}
}
}
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
