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:
float64error
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,Personmust 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() stringmethod 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:
PersonRobotDogAI- 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
*UsersatisfyNotifier?
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
| Feature | Function | Method |
|---|---|---|
Declared with func | Yes | Yes |
| Has parameters | Yes | Yes |
| Returns values | Yes | Yes |
| Can return errors | Yes | Yes |
| Belongs to receiver type | No | Yes |
| Has receiver | No | Yes |
Called as name() | Yes | Usually |
Called as value.Name() | No | Yes |
| Can naturally modify state through pointer | Yes, with pointer parameter | Yes, with pointer receiver |
| Participates in type method set | No | Yes |
| Can satisfy interface methods | No | Yes |
| Can be stored in variable | Yes | Yes |
| Can be callback | Yes | Yes, via method value/expression |
| Can be generic | Yes | Receiver type can be generic |
| Good for package-level operations | Excellent | Sometimes |
| Good for type-specific behavior | Sometimes | Excellent |
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
falsewhen 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.