Go Tutorial: Go Functions and Methods

Go programs are built from small pieces of reusable behavior. Two of the most important pieces are:

  • Functions
  • Methods

At first they look almost identical.

A function:

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

A method:

func (c Calculator) Add(a int, b int) int {
    return a + b
}

The important difference is this part:

(c Calculator)

It is called the receiver.

A method is therefore essentially a function associated with a receiver type. In Go’s specification, a method is defined as a function with a receiver.


1. The Big Picture

Think about functions like this:

Input
  |
  v
Function
  |
  v
Output

Example:

result := add(10, 20)

The function doesn’t need to belong to anything.

Methods are different:

        Object / Value
              |
              v
          +--------+
          | Method |
          +--------+
              |
              v
           Result

Example:

account.Deposit(500)

Deposit represents behavior associated with an account.

The easiest mental model is:

FUNCTION
-------
Do something.

METHOD
------
Ask a particular type/value to do something.

2. Function vs Method in One Example

Consider calculating a rectangle’s area.

Using a function

func area(width float64, height float64) float64 {
    return width * height
}

Call it:

result := area(10, 5)

Using a method

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

Call it:

rect := Rectangle{
    Width:  10,
    Height: 5,
}

result := rect.Area()

Both approaches work.

But they communicate different ideas.

The function says:

Calculate an area using these two numbers.

The method says:

Rectangle, calculate your area.

That distinction becomes extremely useful as programs become larger.


PART I — GO FUNCTIONS

3. What Is a Function?

A function is a reusable block of code designed to perform a particular task.

Basic syntax:

func functionName(parameters) returnType {
    // statements
}

Example:

func greet() {
    fmt.Println("Hello!")
}

Call it:

greet()

4. Your First Go Function

Complete program:

package main

import "fmt"

func greet() {
    fmt.Println("Hello, Go!")
}

func main() {
    greet()
}

Output:

Hello, Go!

Line-by-line explanation

Line 1

package main

Every Go source file belongs to a package.

main is special because it is used for executable programs.


Line 2

import "fmt"

Imports Go’s fmt package.

We need it because we use:

fmt.Println()

Line 3

func greet() {

func tells Go that we’re declaring a function.

greet is the function’s name.

The empty parentheses:

()

mean the function receives no parameters.

{ begins the function body.


Line 4

fmt.Println("Hello, Go!")

Prints text.


Line 5

}

Ends the greet function.


Line 6

func main() {

Every executable Go program starts execution from the main function in package main.


Line 7

greet()

Calls our function.


5. Function Parameters

Functions become much more useful when we give them input.

Example:

func greet(name string) {
    fmt.Println("Hello,", name)
}

Call it:

greet("Alice")

Output:

Hello, Alice

Understanding the parameter

name string

means:

parameter name : name
parameter type : string

The value:

"Alice"

is called an argument when we call the function.

Technically:

Parameter = variable declared by function

Argument = actual value supplied when calling it

6. Multiple Parameters

func add(a int, b int) {
    fmt.Println(a + b)
}

Call:

add(10, 20)

Output:

30

Go also allows parameters of the same type to be shortened:

func add(a, b int) {
    fmt.Println(a + b)
}

Both mean exactly the same thing.


7. Returning a Value

Instead of printing the result inside a function, it is usually more reusable to return it.

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

Call it:

result := add(10, 20)

fmt.Println(result)

Output:

30

Line-by-line

func add(a, b int) int {

The first int describes the parameters:

a, b int

The final int describes the returned value:

func add(a, b int) int
                       ^
                    return type

Then:

return a + b

calculates the value and gives it back to the caller.


8. Why Returning Is Usually Better Than Printing

Compare these functions.

Version A

func add(a, b int) {
    fmt.Println(a + b)
}

Version B

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

Version B is much more reusable.

You can:

total := add(10, 20)

or:

fmt.Println(add(10, 20))

or:

finalPrice := add(10, 20) * 2

or:

if add(10, 20) > 25 {
    fmt.Println("Large result")
}

A useful design principle is:

Functions that calculate values should generally return those values rather than deciding how they are displayed.


9. Multiple Return Values

Go functions can return multiple values.

This feature is used extensively in idiomatic Go.

Example:

func divide(a, b float64) (float64, bool) {
    if b == 0 {
        return 0, false
    }

    return a / b, true
}

Call:

result, ok := divide(10, 2)

fmt.Println(result)
fmt.Println(ok)

Output:

5
true

10. Multiple Returns and Errors

One of the most important Go patterns is:

value, err := function()

Example:

package main

import (
    "errors"
    "fmt"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }

    return a / b, nil
}

func main() {
    result, err := divide(10, 2)

    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Println("Result:", result)
}

Output:

Result: 5

11. Understanding the Error Pattern

Look carefully at:

func divide(a, b float64) (float64, error)

The function returns two things:

  1. float64
  2. error

On failure:

return 0, errors.New("cannot divide by zero")

On success:

return a / b, nil

nil means there is no error.

The caller checks:

if err != nil {

This pattern appears everywhere in Go.

Examples include:

file, err := os.Open(...)
data, err := os.ReadFile(...)
value, err := strconv.Atoi(...)

12. Ignoring a Return Value

Suppose:

value, err := divide(10, 2)

but you don’t need value.

Use the blank identifier:

_, err := divide(10, 2)

Similarly:

value, _ := divide(10, 2)

is syntactically possible, although ignoring errors without a reason is usually a bad practice.


13. Named Return Values

Go allows returned values to have names.

Example:

func calculate(a, b int) (sum int, product int) {
    sum = a + b
    product = a * b

    return
}

Call:

s, p := calculate(5, 3)

Result:

s = 8
p = 15

Because the results were declared as:

(sum int, product int)

the function can use:

return

without explicitly listing them.

This is called a naked return.

Recommendation

Named returns are useful when they make a function’s meaning clearer.

For short functions:

func dimensions() (width int, height int)

can be helpful.

But avoid large functions full of naked returns because they become difficult to understand.


14. Functions Are Passed Values

An important Go concept is:

Function arguments are passed by value.

Consider:

func changeNumber(n int) {
    n = 100
}

Then:

number := 10

changeNumber(number)

fmt.Println(number)

Output:

10

Why?

The function receives its own copy of number.

Conceptually:

main:

number = 10

        copy
         |
         v

changeNumber:

n = 10
n = 100

The original number remains 10.


15. Functions with Pointer Parameters

If you want a function to modify the caller’s value, you can pass a pointer.

func changeNumber(n *int) {
    *n = 100
}

Call:

number := 10

changeNumber(&number)

fmt.Println(number)

Output:

100

Important symbols

&number

means:

Give me the address of number.

And:

*n

means:

Access the value stored at the address contained in n.

This same concept becomes extremely important when we study pointer receiver methods.


16. Variadic Functions

Sometimes you don’t know how many arguments a function will receive.

Use:

...

Example:

func sum(numbers ...int) int {
    total := 0

    for _, number := range numbers {
        total += number
    }

    return total
}

Calls:

fmt.Println(sum())
fmt.Println(sum(10))
fmt.Println(sum(10, 20))
fmt.Println(sum(10, 20, 30, 40))

Outputs:

0
10
30
100

The specification defines a function whose final parameter uses ... as a variadic function.

Inside the function:

numbers

behaves like a slice:

[]int

17. Passing a Slice to a Variadic Function

Suppose:

values := []int{10, 20, 30}

You cannot simply write:

sum(values)

Instead:

sum(values...)

The trailing:

...

expands the slice into arguments.

Conceptually:

sum(values...)

becomes:

sum(10, 20, 30)

18. Functions Are Values

This is where Go functions become much more powerful.

A function can be stored in a variable.

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

func main() {
    operation := add

    result := operation(10, 20)

    fmt.Println(result)
}

Output:

30

The official Go documentation describes Go as supporting first-class functions, including function values, higher-order functions, function literals, and closures.


19. Function Types

A function has a type.

For:

func add(a, b int) int

the function type is:

func(int, int) int

So this is legal:

var operation func(int, int) int

operation = add

Then:

result := operation(10, 20)

20. Passing a Function to Another Function

Functions can receive other functions.

This is called a higher-order function.

Example:

package main

import "fmt"

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

func multiply(a, b int) int {
    return a * b
}

func calculate(a, b int, operation func(int, int) int) int {
    return operation(a, b)
}

func main() {
    fmt.Println(calculate(10, 5, add))
    fmt.Println(calculate(10, 5, multiply))
}

Output:

15
50

21. Understanding the Higher-Order Function

Look at:

func calculate(
    a, b int,
    operation func(int, int) int,
) int

operation is a parameter.

But instead of being:

int

or:

string

its type is:

func(int, int) int

Therefore calculate can execute:

operation(a, b)

The caller decides what operation means.

This pattern is useful for:

  • callbacks
  • filtering
  • sorting
  • middleware
  • event handlers
  • retry logic
  • testing
  • configurable algorithms

22. Anonymous Functions

A function does not always need a name.

Example:

func() {
    fmt.Println("Hello")
}()

This declares and immediately executes an anonymous function.

More commonly:

greet := func(name string) {
    fmt.Println("Hello,", name)
}

greet("Alice")

Output:

Hello, Alice

23. Anonymous Functions as Callbacks

Instead of:

func multiply(a, b int) int {
    return a * b
}

calculate(10, 5, multiply)

you can write:

result := calculate(
    10,
    5,
    func(a, b int) int {
        return a * b
    },
)

This is useful when the behavior is small and only needed in one place.


24. Closures

An anonymous function can remember variables from its surrounding scope.

Example:

func makeCounter() func() int {
    count := 0

    return func() int {
        count++
        return count
    }
}

Use:

counter := makeCounter()

fmt.Println(counter())
fmt.Println(counter())
fmt.Println(counter())

Output:

1
2
3

What’s happening?

When:

makeCounter()

runs, it creates:

count := 0

Then it returns another function.

That returned function continues to access count.

Conceptually:

counter
   |
   v
returned function
   |
   +---- remembers ----> count

Each call modifies the remembered variable.

Closures are useful for:

  • counters
  • configuration
  • middleware
  • stateful callbacks
  • function factories
  • test helpers

25. Functions Returning Functions

You can also deliberately create functions.

Example:

func multiplier(factor int) func(int) int {
    return func(value int) int {
        return value * factor
    }
}

Use:

double := multiplier(2)
triple := multiplier(3)

fmt.Println(double(10))
fmt.Println(triple(10))

Output:

20
30

The double closure remembers:

factor = 2

while triple remembers:

factor = 3

26. Recursion

A recursive function calls itself.

Example:

func factorial(n int) int {
    if n <= 1 {
        return 1
    }

    return n * factorial(n-1)
}

Call:

fmt.Println(factorial(5))

Conceptually:

factorial(5)
= 5 * factorial(4)

= 5 * 4 * factorial(3)

= 5 * 4 * 3 * factorial(2)

= 5 * 4 * 3 * 2 * factorial(1)

= 5 * 4 * 3 * 2 * 1

= 120

The condition:

if n <= 1

is the base case.

Without a correct base case, recursion continues until the program runs out of resources.


27. defer and Function Calls

Go’s defer schedules a function call to execute when the surrounding function returns.

Example:

func example() {
    defer fmt.Println("Second")

    fmt.Println("First")
}

Output:

First
Second

A common real-world pattern is resource cleanup:

file, err := os.Open("data.txt")
if err != nil {
    return err
}

defer file.Close()

This keeps acquisition and cleanup close together.


28. Generic Functions

Modern Go supports type parameters for generic functions. The current language specification explicitly includes type parameters in function declarations.

Suppose we want a function that returns the first element of any slice.

Without generics, we might need separate functions.

With generics:

func first[T any](values []T) T {
    return values[0]
}

Use:

numbers := []int{10, 20, 30}
names := []string{"Alice", "Bob"}

fmt.Println(first(numbers))
fmt.Println(first(names))

Output:

10
Alice

29. Understanding the Generic Function

func first[T any](values []T) T

Break it apart.

func

Declares a function.

first

Function name.

[T any]

Declares type parameter T.

any means T can represent any permitted type.

values []T

means the parameter is a slice containing T.

T

after the parentheses means the function returns a value of type T.


30. Generic Function with a Constraint

Suppose you want to add values.

You cannot necessarily add arbitrary any values.

Define a constraint:

type Number interface {
    ~int | ~int64 | ~float64
}

func addNumbers[T Number](a, b T) T {
    return a + b
}

Now:

fmt.Println(addNumbers(10, 20))
fmt.Println(addNumbers(2.5, 3.5))

works with the allowed numeric types.

Generics are particularly useful for reusable algorithms and containers.


PART II — GO METHODS

31. What Is a Method?

A method is a function associated with a type through a receiver.

Example:

type Person struct {
    Name string
}

func (p Person) Greet() {
    fmt.Println("Hello, my name is", p.Name)
}

Call:

person := Person{Name: "Alice"}

person.Greet()

Output:

Hello, my name is Alice

32. Line-by-Line Method Explanation

Start with:

type Person struct {
    Name string
}

This creates a new type named:

Person

It contains one field:

Name

Then:

func (p Person) Greet() {

Break this apart.

func

declares a function or method.

(p Person)

is the receiver.

p

is the variable through which the method accesses the receiver value.

Person

is the receiver’s type.

Greet

is the method name.

Then:

fmt.Println("Hello, my name is", p.Name)

accesses:

p.Name

Finally:

person.Greet()

calls the method using normal selector syntax.


33. Method Syntax

General form:

func (receiver ReceiverType) MethodName(parameters) returnType {
    // body
}

Example:

func (p Person) FullName() string {
    return p.Name
}

The receiver appears:

between func and method name

Compare:

func FullName(p Person) string

with:

func (p Person) FullName() string

34. Function and Method Doing the Same Thing

Consider:

type Rectangle struct {
    Width  float64
    Height float64
}

Function

func rectangleArea(r Rectangle) float64 {
    return r.Width * r.Height
}

Call:

rectangleArea(rect)

Method

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

Call:

rect.Area()

Both are valid.

But:

rect.Area()

usually makes more conceptual sense because area is naturally behavior associated with a rectangle.


35. Methods Can Have Parameters

Receivers do not replace normal parameters.

Example:

type Calculator struct{}

func (c Calculator) Add(a, b int) int {
    return a + b
}

Call:

calculator := Calculator{}

result := calculator.Add(10, 20)

The receiver is:

c Calculator

The normal parameters are:

a, b int

36. Methods Can Return Values

Example:

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

Call:

circle := Circle{Radius: 5}

fmt.Println(circle.Area())

37. Methods Are Not Limited to Structs

This surprises many beginners.

Methods can be associated with appropriate defined types; they are not limited to structs. The receiver base type has to satisfy Go’s receiver rules and must be defined in the same package as the method.

Example:

type Celsius float64

func (c Celsius) Fahrenheit() float64 {
    return float64(c)*9/5 + 32
}

Call:

temperature := Celsius(25)

fmt.Println(temperature.Fahrenheit())

Output:

77

This is extremely useful for creating domain-specific types.


38. You Cannot Add Methods to Arbitrary External Types

Suppose another package defines some type.

You generally cannot simply write:

func (x SomeExternalType) MyMethod() {
}

for that external receiver type.

The receiver base type must be defined in the same package as the method.

If you need custom behavior, commonly you:

  • create your own type
  • wrap the external type
  • embed it where appropriate
  • use a standalone function

PART III — VALUE RECEIVERS AND POINTER RECEIVERS

39. The Most Important Method Topic

Methods can use two main receiver styles.

Value receiver

func (p Person) Greet() {
}

Pointer receiver

func (p *Person) Rename(name string) {
}

The * makes a huge difference.


40. Value Receiver

Example:

package main

import "fmt"

type Person struct {
    Name string
}

func (p Person) Rename(name string) {
    p.Name = name
}

func main() {
    person := Person{Name: "Alice"}

    person.Rename("Bob")

    fmt.Println(person.Name)
}

What will it print?

Alice

Not:

Bob

Why?

The method receives a copy of the Person value.

Conceptually:

Original:

person.Name = "Alice"

       copy
        |
        v

Method receiver:

p.Name = "Alice"

then:

p.Name = "Bob"

The copy changes.

The original does not.


41. Pointer Receiver

Change the method to:

func (p *Person) Rename(name string) {
    p.Name = name
}

Now:

person := Person{Name: "Alice"}

person.Rename("Bob")

fmt.Println(person.Name)

outputs:

Bob

Why?

p is a pointer to the original Person.

Changes affect the original value.


42. Pointer Receiver Line by Line

func (p *Person) Rename(name string) {

p is the receiver variable.

*Person

means p points to a Person.

Then:

p.Name = name

updates the original Person.

You could think about it conceptually as:

(*p).Name = name

Go allows the simpler:

p.Name

syntax.


43. When Should You Use a Pointer Receiver?

Use a pointer receiver when the method needs to modify receiver state.

Example:

type BankAccount struct {
    Balance float64
}

func (a *BankAccount) Deposit(amount float64) {
    a.Balance += amount
}

Calling:

account.Deposit(500)

should change the account.

So a pointer receiver is appropriate.


44. Another Reason for Pointer Receivers

Imagine:

type HugeData struct {
    // lots of fields
}

A value receiver conceptually works with a value copy.

A pointer receiver passes the pointer value rather than requiring the method receiver to represent an independent copy of the entire struct.

For larger mutable types, pointer receivers are often appropriate.


45. When Should You Use a Value Receiver?

Value receivers work well when:

  • the type is small
  • the method doesn’t need to modify it
  • the type naturally behaves like a value
  • copying is expected
  • immutable-style semantics make sense

Example:

type Point struct {
    X float64
    Y float64
}

func (p Point) DistanceFromOrigin() float64 {
    return math.Sqrt(p.X*p.X + p.Y*p.Y)
}

The method calculates information.

It doesn’t need to modify the point.


46. Go Often Automatically Takes the Address

Suppose:

func (p *Person) Rename(name string) {
    p.Name = name
}

and:

person := Person{Name: "Alice"}

You can normally write:

person.Rename("Bob")

instead of:

(&person).Rename("Bob")

For an addressable value, Go can automatically take the address for the pointer-receiver method call. The language specification also distinguishes this call convenience from the actual method-set rules.

This convenience is important because beginners sometimes conclude:

“If person.Rename() works, Person must contain the pointer method in its method set.”

That conclusion is incorrect.

Which leads us to the next advanced concept.


PART IV — METHOD SETS

47. What Is a Method Set?

A method set is the collection of methods associated with a type for purposes including interface implementation.

For a defined type T, its method set contains methods declared with receiver T.

For *T, its method set contains methods declared with receiver T and *T.

Simplified:

Receiver declaration        Method belongs to method set of

func (t T) A()              T and *T
func (t *T) B()             *T

48. Method Set Example

type User struct{}

func (u User) Read() {
}

func (u *User) Write() {
}

Think of the method sets as:

User
----
Read

*User
-----
Read
Write

This becomes extremely important when interfaces are involved.


PART V — METHODS AND INTERFACES

49. Why Methods Become So Powerful

Interfaces are one of the main reasons methods matter so much in Go.

Consider:

type Speaker interface {
    Speak() string
}

This interface says:

Anything that has a Speak() string method can behave as a Speaker.

Now:

type Person struct {
    Name string
}

func (p Person) Speak() string {
    return "Hello, I am " + p.Name
}

Person satisfies Speaker.

You don’t explicitly write:

Person implements Speaker

The relationship is determined by the methods provided by the type.


50. Complete Interface Example

package main

import "fmt"

type Speaker interface {
    Speak() string
}

type Person struct {
    Name string
}

func (p Person) Speak() string {
    return "Hello, I am " + p.Name
}

func introduce(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    person := Person{Name: "Alice"}

    introduce(person)
}

Output:

Hello, I am Alice

51. Line-by-Line Interface Explanation

type Speaker interface {

Declares an interface.

Speak() string

specifies required behavior.

Anything whose method set satisfies that requirement can be used as a Speaker. Go interfaces specify behavior through methods.

Then:

func introduce(s Speaker)

doesn’t care whether s is:

  • Person
  • Robot
  • Dog
  • AI
  • another future type

It only cares that:

s.Speak()

exists with the required signature.

That is one of Go’s most important forms of polymorphism.


52. Pointer Receivers and Interfaces

Consider:

type Notifier interface {
    Notify()
}

type User struct {
    Email string
}

func (u *User) Notify() {
    fmt.Println("Notifying:", u.Email)
}

This works:

user := &User{Email: "alice@example.com"}

var notifier Notifier = user

because *User has the Notify method.

But a plain User value does not have that pointer-receiver method in its method set.

So this conceptually fails:

var notifier Notifier = User{
    Email: "alice@example.com",
}

This distinction is one of the most common sources of confusion for new Go programmers.


53. Compile-Time Interface Checks

A common Go technique is:

var _ Notifier = (*User)(nil)

This doesn’t create a useful runtime value.

Instead, it asks the compiler:

Does *User satisfy Notifier?

If not, compilation fails.

This is useful in larger codebases.


PART VI — METHODS AS FUNCTION VALUES

54. Method Values

Methods can themselves become function values.

Suppose:

type User struct {
    Name string
}

func (u User) Greet() string {
    return "Hello, " + u.Name
}

Then:

user := User{Name: "Alice"}

greet := user.Greet

Now:

fmt.Println(greet())

works.

user.Greet is called a method value. The receiver is captured as part of that function value.


55. Understanding Method Values

Normally:

user.Greet()

But:

greet := user.Greet

binds user to the method.

So greet effectively behaves like:

func() string

You don’t need to pass user again.

This is useful for:

  • callbacks
  • handlers
  • dependency injection
  • event processing
  • scheduling work

56. Method Expressions

A related advanced concept is a method expression.

Given:

type User struct {
    Name string
}

func (u User) Greet() string {
    return "Hello, " + u.Name
}

you can write:

greet := User.Greet

Now the receiver is not captured.

You provide it explicitly:

user := User{Name: "Alice"}

fmt.Println(greet(user))

The specification describes a method expression such as T.M as producing a function whose first argument represents the receiver.


57. Method Value vs Method Expression

Method value:

greet := user.Greet
greet()

Receiver already selected.

Method expression:

greet := User.Greet
greet(user)

Receiver supplied later.

Think:

METHOD VALUE

user.Greet
^^^^
receiver already attached


METHOD EXPRESSION

User.Greet
     ^
receiver becomes function argument

PART VII — METHODS AND GENERICS

58. Methods on Generic Types

Methods can be defined on generic receiver types.

Example:

type Pair[A, B any] struct {
    First  A
    Second B
}

Then:

func (p Pair[A, B]) Swap() Pair[B, A] {
    return Pair[B, A]{
        First:  p.Second,
        Second: p.First,
    }
}

Use:

pair := Pair[string, int]{
    First:  "Age",
    Second: 30,
}

swapped := pair.Swap()

The current Go specification explicitly allows the receiver specification of a generic receiver base type to declare the corresponding receiver type parameters for the method to use.


59. Important Generic Method Rule

Consider:

type Box[T any] struct {
    Value T
}

A method can use the receiver’s T:

func (b Box[T]) Get() T {
    return b.Value
}

But Go method declaration syntax does not provide a separate independent type-parameter list after the method name.

In practical terms, prefer generic functions when you need an operation with its own independent type parameters.


PART VIII — EMBEDDING AND PROMOTED METHODS

60. Struct Embedding

Go uses composition rather than traditional class inheritance.

Example:

type Logger struct{}

func (Logger) Log(message string) {
    fmt.Println("LOG:", message)
}

Embed it:

type Service struct {
    Logger
}

Now:

service := Service{}

service.Log("Server started")

works through method promotion.

Conceptually:

Service
  |
  +-- Logger
        |
        +-- Log()

This is composition.


61. Why Embedding Matters

Embedding allows larger types to compose behavior from smaller types.

Example:

type Database struct{}

func (Database) Save() {
    fmt.Println("saving")
}

type UserService struct {
    Database
}

Then:

service.Save()

can use the promoted method.

Embedding should express a meaningful composition relationship—not merely be used to imitate inheritance from another language.


PART IX — FUNCTION VS METHOD COMPARISON

62. Side-by-Side Comparison

FeatureFunctionMethod
Declared with funcYesYes
Has parametersYesYes
Returns valuesYesYes
Can return errorsYesYes
Belongs to receiver typeNoYes
Has receiverNoYes
Called as name()YesUsually
Called as value.Name()NoYes
Can naturally modify state through pointerYes, with pointer parameterYes, with pointer receiver
Participates in type method setNoYes
Can satisfy interface methodsNoYes
Can be stored in variableYesYes
Can be callbackYesYes, via method value/expression
Can be genericYesReceiver type can be generic
Good for package-level operationsExcellentSometimes
Good for type-specific behaviorSometimesExcellent

63. Same Problem as Function and Method

Suppose:

type Account struct {
    Balance float64
}

Function version

func deposit(account *Account, amount float64) {
    account.Balance += amount
}

Call:

deposit(&account, 500)

Method version

func (account *Account) Deposit(amount float64) {
    account.Balance += amount
}

Call:

account.Deposit(500)

Neither approach is automatically “better.”

The question is:

Does this operation conceptually belong to Account?

For depositing money into an account, the answer is probably yes.

Therefore:

account.Deposit(500)

expresses the domain nicely.


PART X — WHEN TO USE FUNCTIONS

64. Use Function: Pure Calculation

Example:

func calculateTax(price, taxRate float64) float64 {
    return price * taxRate
}

This doesn’t necessarily belong to one particular object.

A function is natural.


65. Use Function: Conversion

func CelsiusToFahrenheit(c float64) float64 {
    return c*9/5 + 32
}

Excellent function use case.

If you’ve created a domain-specific Celsius type, a method could also make sense:

func (c Celsius) Fahrenheit() float64

Design depends on the abstraction you want.


66. Use Function: Constructor

Go frequently uses functions to construct values.

Example:

type Server struct {
    Host string
    Port int
}

func NewServer(host string, port int) *Server {
    return &Server{
        Host: host,
        Port: port,
    }
}

Call:

server := NewServer("localhost", 8080)

The convention:

NewType(...)

is common when initialization logic is needed.


67. Use Function: Operation Involving Multiple Types

Suppose:

func Transfer(
    source *Account,
    destination *Account,
    amount float64,
) error

Transfer involves two accounts.

Depending on your domain, a standalone function may express that relationship better than arbitrarily attaching the operation to one account.


68. Use Function: Utility

Examples:

func IsValidEmail(email string) bool
func ParseConfig(data []byte) (*Config, error)
func GenerateID() string
func HashPassword(password string) ([]byte, error)

These frequently make sense as package-level functions.


69. Use Function: Generic Algorithms

Generic reusable algorithms are particularly natural as functions.

Example:

func Map[T, R any](items []T, transform func(T) R) []R {
    result := make([]R, len(items))

    for i, item := range items {
        result[i] = transform(item)
    }

    return result
}

Use:

numbers := []int{1, 2, 3}

strings := Map(numbers, func(n int) string {
    return strconv.Itoa(n)
})

PART XI — WHEN TO USE METHODS

70. Use Method: Behavior Belongs to a Type

user.FullName()

is more expressive than:

calculateUserFullName(user)

when full-name behavior naturally belongs to User.


71. Use Method: Modify Object State

account.Deposit(100)
cart.AddItem(item)
player.Move(10, 5)
server.Start()

These operations naturally affect receiver state.


72. Use Method: Implement an Interface

Suppose:

type Stringer interface {
    String() string
}

Your type can provide the required behavior through a method.

Interfaces are fundamentally method-oriented in Go.


73. Use Method: Domain Modeling

Methods can create very readable domain code.

Instead of:

withdrawFromBankAccount(account, 100)

you can use:

account.Withdraw(100)

Instead of:

calculateInvoiceTotal(invoice)

you might use:

invoice.Total()

Instead of:

activateUser(user)

you might use:

user.Activate()

Good APIs often read naturally.


PART XII — COMPLETE REAL-WORLD EXAMPLE

74. Building a Bank Account API

Let’s combine functions and methods.

package main

import (
    "errors"
    "fmt"
)

type BankAccount struct {
    owner   string
    balance float64
}

func NewBankAccount(owner string) *BankAccount {
    return &BankAccount{
        owner:   owner,
        balance: 0,
    }
}

func (a *BankAccount) Deposit(amount float64) error {
    if amount <= 0 {
        return errors.New("deposit must be greater than zero")
    }

    a.balance += amount

    return nil
}

func (a *BankAccount) Withdraw(amount float64) error {
    if amount <= 0 {
        return errors.New("withdrawal must be greater than zero")
    }

    if amount > a.balance {
        return errors.New("insufficient balance")
    }

    a.balance -= amount

    return nil
}

func (a BankAccount) Balance() float64 {
    return a.balance
}

func (a BankAccount) Owner() string {
    return a.owner
}

func formatMoney(amount float64) string {
    return fmt.Sprintf("$%.2f", amount)
}

func main() {
    account := NewBankAccount("Alice")

    if err := account.Deposit(1000); err != nil {
        fmt.Println("Deposit error:", err)
        return
    }

    if err := account.Withdraw(250); err != nil {
        fmt.Println("Withdrawal error:", err)
        return
    }

    fmt.Println("Owner:", account.Owner())
    fmt.Println("Balance:", formatMoney(account.Balance()))
}

Output:

Owner: Alice
Balance: $750.00

75. Why NewBankAccount Is a Function

func NewBankAccount(owner string) *BankAccount

At creation time we don’t have an account receiver yet.

The purpose of the function is to create one.

Therefore a constructor-style function is natural:

account := NewBankAccount("Alice")

76. Why Deposit Is a Method

func (a *BankAccount) Deposit(amount float64) error

A deposit affects a specific account.

Therefore:

account.Deposit(1000)

is natural.

It uses a pointer receiver because the account’s balance must change.


77. Why Withdraw Is a Method

Same reasoning:

account.Withdraw(250)

The operation acts on one particular account and modifies it.

Therefore a pointer receiver is appropriate.


78. Why Balance Uses a Value Receiver

func (a BankAccount) Balance() float64 {
    return a.balance
}

Balance() only reads information.

It doesn’t mutate the account.

For a small value, a value receiver can be reasonable.

In real APIs, however, receiver consistency is also worth considering. Go’s official guidance recommends carefully considering pointer-versus-value receiver consistency when methods of a type need pointer receivers.

For example, a real production BankAccount might simply use pointer receivers consistently across its methods:

func (a *BankAccount) Balance() float64

79. Why formatMoney Is a Function

func formatMoney(amount float64) string

Formatting money isn’t necessarily intrinsic behavior of BankAccount.

It could be used for:

  • account balances
  • invoices
  • product prices
  • salaries
  • payments

So a standalone function can make more sense.

This illustrates an important principle:

Don’t make everything a method.


PART XIII — ADVANCED API DESIGN

80. Don’t Turn Structs into “Classes”

Developers coming from Java, C++, C#, or similar languages sometimes try to turn every Go struct into a class with dozens of methods.

That isn’t necessary.

Go encourages simple composition.

Use:

type User struct {
    Name string
}

Methods should exist when they add meaningful behavior.

You don’t need a method merely because a function happens to accept that type.


81. Functions Can Be More Flexible

Suppose:

func Distance(a, b Point) float64

This is symmetric.

Neither point is obviously more important.

A function communicates that nicely:

Distance(pointA, pointB)

You could technically design:

pointA.DistanceTo(pointB)

That can also be good.

The right choice depends on the domain and intended API.


82. Ask “Who Owns This Behavior?”

A useful design question is:

Does this behavior naturally belong to one specific type?

If yes:

Consider a method.

If no:

Consider a function.

Examples:

user.Activate()

Method.

account.Deposit()

Method.

file.Close()

Method.

ParseDate()

Function.

GenerateUUID()

Function.

SortUsers()

Potentially function.

invoice.Total()

Method.


PART XIV — COMMON BEGINNER MISTAKES

83. Mistake: Forgetting the Return Type

Wrong:

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

The function claims to return nothing.

But:

return a + b

returns a value.

Correct:

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

84. Mistake: Forgetting to Return

Wrong:

func add(a, b int) int {
    result := a + b
}

The function promises:

int

but doesn’t return one.

Correct:

func add(a, b int) int {
    result := a + b

    return result
}

85. Mistake: Expecting a Value Parameter to Change

func change(n int) {
    n = 100
}

Calling:

x := 10
change(x)

does not change x.

Use a pointer if mutation is intended:

func change(n *int) {
    *n = 100
}

86. Mistake: Using Value Receiver for Intended Mutation

Wrong:

func (a Account) Deposit(amount float64) {
    a.Balance += amount
}

If you expect the original account to change, use:

func (a *Account) Deposit(amount float64) {
    a.Balance += amount
}

87. Mistake: Confusing Call Convenience with Interface Method Sets

This may work:

user.Notify()

even when Notify has a pointer receiver and user is an addressable value.

But that does not mean the value type necessarily satisfies an interface requiring Notify.

Method-call shorthand and method sets are related but distinct rules.


88. Mistake: Making Everything a Pointer Receiver

Pointer receivers are useful, but not automatically correct.

For a small immutable-style type:

type Coordinate struct {
    X int
    Y int
}

something like:

func (c Coordinate) IsOrigin() bool {
    return c.X == 0 && c.Y == 0
}

can reasonably use a value receiver.


89. Mistake: Mixing Receivers Without Thinking

Avoid randomly creating:

func (u User) Name()
func (u *User) Save()
func (u User) Email()
func (u *User) Validate()

There may be valid reasons, but receiver choice affects method sets and interface implementation.

Choose receivers deliberately.


90. Mistake: Creating Huge Functions

Avoid:

func ProcessEverything() {
    // 500 lines
}

Functions should usually have a focused responsibility.

Instead:

func validateOrder(...)
func calculateTotal(...)
func saveOrder(...)
func sendConfirmation(...)

Then combine them in higher-level logic.

Small functions are easier to:

  • read
  • test
  • reuse
  • debug
  • maintain

91. Mistake: Ignoring Errors

Avoid:

result, _ := doSomething()

unless ignoring the error is genuinely intentional and safe.

Prefer:

result, err := doSomething()

if err != nil {
    return err
}

Error returns are a central part of Go function and method design.


PART XV — FUNCTION AND METHOD NAMING

92. Function Names

Use names that describe behavior.

Good:

ParseConfig
LoadUser
CalculateTax
ValidateEmail

Poor:

DoThing
ProcessStuff
HandleData

unless the surrounding context genuinely makes them precise.


93. Method Names

Methods already have receiver context.

Instead of:

user.GetUserName()

you can often use:

user.Name()

Instead of:

account.GetAccountBalance()

prefer:

account.Balance()

Official Go guidance specifically recommends concise getter names such as Owner() rather than GetOwner(), with setters such as SetOwner() where appropriate.


94. Exported Functions and Methods

Go uses capitalization for visibility outside a package.

Lowercase:

func calculateTax() {
}

is unexported.

Uppercase:

func CalculateTax() {
}

is exported.

Same with methods:

func (u User) name()

unexported.

func (u User) Name()

exported.


PART XVI — FUNCTIONS, METHODS, AND CONCURRENCY

95. Calling Functions in Goroutines

Any appropriate function call can be launched with:

go

Example:

func sendEmail() {
    fmt.Println("Sending email")
}

func main() {
    go sendEmail()
}

You can similarly launch a method call:

go worker.Process()

The Go documentation notes that prefixing a function or method call with go runs that call in a new goroutine.

Concurrency introduces synchronization and lifetime concerns beyond the scope of basic functions and methods, but syntactically both participate naturally.


PART XVII — TESTABILITY

96. Functions Are Easy to Unit Test

Function:

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

Test:

func TestAdd(t *testing.T) {
    result := add(2, 3)

    if result != 5 {
        t.Fatalf("expected 5, got %d", result)
    }
}

Pure functions are particularly easy to test because output depends only on input.


97. Methods Are Also Easy to Test

Method:

type Counter struct {
    Value int
}

func (c *Counter) Increment() {
    c.Value++
}

Test:

func TestIncrement(t *testing.T) {
    counter := Counter{}

    counter.Increment()

    if counter.Value != 1 {
        t.Fatalf("expected 1, got %d", counter.Value)
    }
}

Methods often require creating receiver state before testing.


PART XVIII — DESIGN PATTERNS USING FUNCTIONS

98. Functional Options

An advanced Go API pattern uses functions for configuration.

Example:

type Server struct {
    Host string
    Port int
}

type Option func(*Server)

Create an option:

func WithPort(port int) Option {
    return func(s *Server) {
        s.Port = port
    }
}

Constructor:

func NewServer(options ...Option) *Server {
    server := &Server{
        Host: "localhost",
        Port: 8080,
    }

    for _, option := range options {
        option(server)
    }

    return server
}

Use:

server := NewServer(
    WithPort(9000),
)

This combines:

  • function types
  • higher-order functions
  • closures
  • variadic functions
  • pointer mutation

It is a good example of how powerful functions become in real Go APIs.


PART XIX — A CLEANER REAL-WORLD SERVICE EXAMPLE

99. User Service

Consider:

type User struct {
    ID     int
    Name   string
    Active bool
}

Behavior naturally belonging to a user:

func (u *User) Activate() {
    u.Active = true
}

Behavior belonging to formatting:

func FormatUser(u User) string {
    return fmt.Sprintf("%d: %s", u.ID, u.Name)
}

Behavior belonging to data storage might instead live behind an abstraction:

type UserRepository interface {
    Save(user *User) error
}

This produces cleaner separation:

User
 |
 +-- domain behavior

Formatting functions
 |
 +-- representation

Repository
 |
 +-- persistence behavior

Do not simply attach every possible operation to User.


PART XX — QUICK DECISION TREE

100. Should I Create a Function or Method?

Start here:

Does the behavior naturally belong to one type?
                |
        +-------+-------+
        |               |
       YES              NO
        |               |
        v               v
    METHOD          FUNCTION

Then ask:

Does it need to modify the receiver?
                |
        +-------+-------+
        |               |
       YES              NO
        |               |
        v               v
Pointer receiver    Consider value
                    or pointer receiver

Then ask:

Must the type satisfy an interface?
                |
                v
Check the exact method set.

And:

Is it a generic reusable algorithm involving
multiple independent types?
                |
                v
A generic function is often a strong choice.

PART XXI — PRACTICAL COMPARISON

101. Example: User Validation

Possible function:

func ValidateUser(user User) error

Possible method:

func (u User) Validate() error

Which is better?

If validation rules fundamentally belong to User, this reads beautifully:

user.Validate()

If validation depends heavily on external policy:

validator.Validate(user)

or:

ValidateUser(user, policy)

may provide better separation.

The decision is architectural—not simply syntactic.


102. Example: Password Hashing

Prefer something like:

func HashPassword(password string) ([]byte, error)

rather than forcing the operation onto an unrelated object merely to make it a method.


103. Example: Shopping Cart

Methods are natural:

cart.Add(item)
cart.Remove(productID)
cart.Total()

because those behaviors naturally belong to the cart.


104. Example: Parsing JSON

A package function may make sense:

ParseUser(data)

because the operation converts external data into a user.

Alternatively, standard interfaces or package conventions may lead to methods depending on the API.

The important point is to choose based on the abstraction rather than blindly preferring one syntax.


PART XXII — BEST PRACTICES

105. Function Best Practices

1. Give functions one clear job

Prefer:

ValidateOrder()
CalculateTotal()
SaveOrder()

over:

DoEverythingWithOrder()

2. Keep inputs clear

Avoid unnecessary global state.

3. Return useful values

Prefer reusable functions over functions that always print results.

4. Return errors where callers can recover

Avoid using panic for normal application errors.

5. Use descriptive names

The caller should understand:

ParseConfig()

without reading its entire implementation.

6. Use higher-order functions when behavior genuinely needs to be configurable

Don’t introduce callbacks just to make simple code look sophisticated.

7. Use generics when they eliminate meaningful duplication

Don’t use generics when a concrete function would be clearer.


106. Method Best Practices

1. Associate methods with meaningful receiver behavior

Good:

account.Withdraw()

2. Use pointer receivers when mutation is required

func (a *Account) Deposit(...)

3. Understand interface method sets

Especially when pointer receivers are involved.

4. Keep receiver names short and consistent

Common:

func (u User) Name()

instead of:

func (theCurrentUser User) Name()

5. Don’t treat Go types like traditional classes

Use composition.

6. Don’t create methods simply to reduce function count

Architecture matters more than syntax.

7. Consider receiver consistency

Especially when the type contains mutating methods or implements interfaces.


PART XXIII — INTERVIEW-STYLE QUESTIONS

107. What Is a Function in Go?

A function is a reusable block of code declared using func that may accept parameters and return zero or more results.

Example:

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

108. What Is a Method in Go?

A method is a function with a receiver.

Example:

func (u User) Name() string {
    return u.name
}

109. What Is the Main Difference?

Function:

Calculate(user)

Method:

user.Calculate()

A method is associated with a receiver type and participates in that type’s method-set/interface behavior.


110. What Is a Receiver?

The receiver is the parameter appearing between func and the method name.

func (u User) Greet()
     ^^^^^^^^
     receiver

111. Value Receiver vs Pointer Receiver?

Value:

func (u User) Method()

works with a receiver value.

Pointer:

func (u *User) Method()

works through a pointer and can directly modify the receiver’s original state.


112. Can Functions Return Multiple Values?

Yes.

func divide(a, b int) (int, error)

This is commonly used for error handling.


113. Can Functions Be Passed Around?

Yes.

func execute(operation func()) {
    operation()
}

Functions are first-class values in Go.


114. Can Methods Be Passed Around?

Yes.

Using a method value:

f := user.Save

or a method expression:

f := User.Name

Method values and expressions are formally part of Go’s language semantics.


115. Can Methods Exist on Non-Struct Types?

Yes, methods are not limited to structs.

Example:

type Celsius float64

func (c Celsius) Fahrenheit() float64 {
    return float64(c)*9/5 + 32
}

The receiver still has to satisfy Go’s receiver declaration rules.


116. Does Go Support Function Overloading?

Go doesn’t use traditional function overloading where several package functions share the same name but differ only by parameter types.

Instead, Go favors:

  • distinct names
  • interfaces
  • variadic functions where appropriate
  • generic functions
  • methods on different receiver types

Different receiver types can, of course, have methods with the same method name.

Example:

dog.Speak()

and:

person.Speak()

PART XXIV — PRACTICE EXERCISES

117. Beginner Exercise 1

Create:

func subtract(a, b int) int

Expected:

subtract(10, 3)

returns:

7

118. Beginner Exercise 2

Create:

func fullName(firstName, lastName string) string

Calling:

fullName("Alice", "Smith")

should return:

Alice Smith

119. Beginner Exercise 3

Create a function:

divide(a, b float64)

that returns:

(float64, error)

Return an error when b == 0.


120. Method Exercise 1

Create:

type Rectangle struct {
    Width  float64
    Height float64
}

Add:

Area()

and:

Perimeter()

methods.

Expected:

rectangle.Area()
rectangle.Perimeter()

121. Method Exercise 2

Create:

type Counter struct {
    Value int
}

Add:

Increment()

using a pointer receiver.

Then:

counter.Increment()
counter.Increment()
counter.Increment()

should result in:

3

122. Intermediate Exercise

Create:

type BankAccount struct {
    balance float64
}

Methods:

Deposit(amount float64) error
Withdraw(amount float64) error
Balance() float64

Rules:

Deposit must be positive.
Withdrawal must be positive.
Withdrawal cannot exceed balance.

123. Interface Exercise

Create:

type Shape interface {
    Area() float64
}

Then implement Area() for:

Rectangle
Circle

Finally:

func printArea(shape Shape)

should work with either type.


124. Higher-Order Function Exercise

Create:

func calculate(a, b int, operation func(int, int) int) int

Then pass:

add
subtract
multiply

into it.


125. Closure Exercise

Create:

func makeCounter() func() int

so:

counter := makeCounter()

counter() // 1
counter() // 2
counter() // 3

126. Advanced Generic Exercise

Create:

func First[T any](values []T) (T, bool)

Requirements:

  • return the first element when the slice has data
  • return false when it is empty
  • avoid panicking on an empty slice

Hint:

var zero T

can produce the zero value of generic type T.


PART XXV — FINAL CHEAT SHEET

127. Basic Function

func greet() {
    fmt.Println("Hello")
}

128. Function with Parameter

func greet(name string) {
    fmt.Println("Hello", name)
}

129. Function Returning Value

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

130. Multiple Returns

func operation() (int, error) {
    return 10, nil
}

131. Variadic Function

func sum(values ...int) int {
    total := 0

    for _, value := range values {
        total += value
    }

    return total
}

132. Function Variable

operation := add

133. Anonymous Function

operation := func(a, b int) int {
    return a + b
}

134. Higher-Order Function

func execute(operation func()) {
    operation()
}

135. Generic Function

func First[T any](values []T) T {
    return values[0]
}

136. Basic Method

func (u User) Name() string {
    return u.name
}

137. Pointer Receiver

func (u *User) Rename(name string) {
    u.name = name
}

138. Method Value

f := user.Greet

f()

139. Method Expression

f := User.Greet

f(user)

140. Interface

type Speaker interface {
    Speak() string
}

141. Compile-Time Interface Check

var _ Speaker = (*Person)(nil)

142. Final Function vs Method Rule

When unsure, start with this:

FUNCTION
========

func CalculateTax(price float64) float64

Use when:
- behavior is standalone
- transforming/calculating data
- creating values
- parsing
- validating independent inputs
- working across multiple unrelated types
- writing generic algorithms
- creating callbacks or higher-order functions


METHOD
======

func (a *Account) Deposit(amount float64)

Use when:
- behavior naturally belongs to a type
- working with receiver state
- modifying receiver state
- implementing an interface
- building a domain-oriented API
- you want calls such as account.Deposit()

And for receiver selection:

VALUE RECEIVER

func (p Point) X()

Best starting point when:
- no mutation required
- value is small
- value semantics make sense


POINTER RECEIVER

func (u *User) Rename()

Best starting point when:
- receiver must be modified
- avoiding receiver copies matters
- identity/state matters
- interface/method-set design requires it

143. The Five Rules Worth Memorizing

If you remember only five things from this entire tutorial, remember these:

Rule 1

A function is standalone:

result := Add(10, 20)

Rule 2

A method has a receiver:

account.Deposit(100)

Rule 3

Use a pointer receiver when the method must modify receiver state:

func (a *Account) Deposit(amount float64)

Rule 4

Methods are the foundation of interface satisfaction:

type Writer interface {
    Write([]byte) (int, error)
}

Rule 5

Don’t ask:

"Can this be a method?"

Ask:

"Does this behavior naturally belong to this type?"

That question leads to much cleaner Go programs.


144. Final Mental Model

Think of your Go program as three layers:

                 GO PROGRAM
                     |
          +----------+----------+
          |                     |
      FUNCTIONS              TYPES
          |                     |
          |                 +---+---+
          |                 |       |
          |              Fields   Methods
          |                         |
          +------------+------------+
                       |
                   Interfaces

Functions give you:

Reusable operations
Transformations
Algorithms
Factories
Callbacks
Closures
Generic behavior

Methods give your types:

Behavior
State operations
Domain meaning
Interface implementation
Readable APIs

Used together:

user := NewUser("Alice")   // function

user.Activate()            // method

if err := Save(user); err != nil { // function
    return err
}

That combination—simple types, focused functions, meaningful methods, and small interfaces—is one of the foundations of clean, idiomatic Go design.

Related Posts

Go Tutorials: Go Methods

Go methods become much easier once you understand one idea: A method is simply a function attached to a type. And the important rule is: A receiver…

Read More

Go Tutorials: Modules, Packages, Subpackages, Submodules, and Workspaces

This tutorial gives you one complete mental model for: The most important idea is: And sometimes: These two designs are very different. 1. What is a Go…

Read More

Go Tutorials: Numeric Types Beginner Tutorial

Go has four main families of numeric types: The simplest way to remember them is: Type family Stores Example int Whole numbers, positive or negative -10, 0,…

Read More

Go Tutorials: Go Testing, Benchmarking, and Profiling

These three topics become much easier once you separate the questions they answer: Topic Main question Main Go tool Result Testing Does my code work correctly? go…

Read More

Go Tutorials: Generics

1. What are Generics? Generics let you write one piece of code that works with multiple types while keeping Go’s compile-time type safety. Suppose you want a…

Read More

Go Concurrency Management Made Simple

This tutorial covers: The goal is: One concept → one purpose → one complete runnable example. Every example is independent. Save any example as: and run: 0….

Read More