What is a method?
What is a receiver?
How to create a method.
How to call a method.
What is a pointer receiver, a value receiver?
What is a method set?
How to name your receivers.
Receiver
Method
Parameter
Value receiver
Pointer type
Pointer receiver
A method is a function with a receiver.1.
The receiver of a method is a special parameter.
The receiver is not listed in the parameter list but before the method name
A method can have only one receiver.
The receiver has a type T or T*
\nWhen the receiver has a type T, we say that it’s a “value receiver”
When it has a type T*, we say that it’s a “pointer receiver”
We say that the base type is T
Example :
\n// methods/first-example/main.go\npackage main\n\nimport (\n "os/user"\n "time"\n\n "github.com/Rhymond/go-money"\n)\n\ntype Item struct {\n ID string\n}\n\ntype Cart struct {\n ID string\n CreatedAt time.Time\n UpdatedAt time.Time\n lockedAt time.Time\n user.User\n Items []Item\n CurrencyCode string\n isLocked bool\n}\n\nfunc (c *Cart) TotalPrice() (*money.Money, error) {\n // ...\n return nil, nil\n}\n\nfunc (c *Cart) Lock() error {\n // ...\n return nil\n}
\nWe have defined the type Cart
.
This type has two methods: TotalPrice
Lock
Those two methods are functions.
They are bound to the type Cart
.
The receiver for the method TotalPrice
is called c
and is of type *Cart
The receiver for the method Lock
is called c
and is of type *Cart
With methods, you can give additional capabilities to the cart Type. In the previous example, we add the capability for somebody that manipulate a Cart to :
\nLock the cart
Compute the total price
Method names should be unique inside a method set.
What is a method set?
\nThe method set of a type T is the group of all methods with a receiver T
The method set of a type *T is the group of all methods with a receiver T and *T
It means that you CANNOT have two methods with the same name, even if the first one has a receiver type and the second one has a value type :
\n// Forbidden :\n\n\nfunc (c *Cart) TotalPrice() (*money.Money, error) {\n //...\n}\n\nfunc (c Cart) TotalPrice() (*money.Money, error) {\n //...\n}
\nThe previous snippet will not compile :
\n# maximilien-andile.com/methods/methodSet\n./main.go:52:6: method redeclared: Cart.TotalPrice\n method(*Cart) func() (*money.Money, error)\n method(Cart) func() (*money.Money, error)\n\nCompilation finished with exit code 2
\n\nMethods are called with the “dot notation”. The receiver argument is passed to the method with a dot.
\npackage main\n\nimport (\n "call/cart"\n "log"\n)\n\nfunc main() {\n // load the cart... into variable cart\n newCart := cart.Cart{}\n\n totalPrice, err := newCart.TotalPrice()\n if err != nil {\n log.Printf("impossible to compute price of the cart: %s", err)\n return\n }\n log.Println("Total Price", totalPrice.Display())\n\n err = newCart.Lock()\n if err != nil {\n log.Printf("impossible to lock the cart: %s", err)\n return\n }\n\n}
\nIn the previous example, the methods TotalPrice
and Lock
are called (with the dot notation)
totalPrice, err := cart.TotalPrice()
, here we pass the variable cart
to the method TotalPrice
err = cart.Lock()
, here we pass the variable cart
to the method Lock
Those two methods are bound to the type Cart
from the current package (main)
We also call the method Display
bound to the type Money
from the package money
Note that the package cart belongs to the module “call”
When you use a value receiver the data will be copied internally before the method is executed. The method will use a copy of the variable.
\nThis has two consequences :
\nA method with a value receiver cannot modify the data passed to it
The internal copy process might impact your program’s performance (most of the time, it’s negligible, except for heavy type structs).
With a pointer receiver, the data passed to it can be modified by the method.
\n\nThe receiver is an additional function parameter.
\nMethods receivers are either pointer receivers or value receivers.
\nIn this method, the receiver has the type *Cart
(a pointer to Cart
).
func (c *Cart) TotalPrice() (*money.Money, error) {\n // ...\n return total, nil\n}
\nWhen we call this method, we use the following notation :
\nnewCart := cart.Cart{}\ntotalPrice, err := newCart.TotalPrice()\n//...
\nDid you notice something weird?
\nWe have learned that a function parameter has a type. This type should be respected (you cannot give a function an uint8
if it expects a string).
The type of newCart
is Cart
(from cart
package)
The type of the receiver is *Cart
.
Types are not the same!
Should it trigger an error, no? It does not. Why?
\nnewCart
automatically to a pointer.Methods with pointer receivers can take a pointer OR a value as receiver
Methods with value receivers can take a pointer OR a value as receiver
Methods like functions have a visibility.
\nWhen the first letter of the method name is capitalized, the method is exported.
\nIn the previous example, we put all our code into the main package; consequently, method visibility did not matter much. However, when you create a package, you must consider methods’ visibility.
\nAn exported method is callable outside the package.
A non exported method is NOT callable outside the package.
Let’s consider a new organization (see figure).
\nWe have the go.mod, go.sum, and main at the root.go (the application)
We have three directories; each directory contains source files for a package.
We have three packages :
\ncart
product
user
Here is the content of the cart package :
\n// methods/example-project/cart/cart.go\npackage cart\n\nimport (\n "methods/example-project/product"\n "os/user"\n "time"\n\n "github.com/Rhymond/go-money"\n)\n\ntype Cart struct {\n ID string\n CreatedAt time.Time\n UpdatedAt time.Time\n lockedAt time.Time\n user.User\n Items []Item\n CurrencyCode string\n isLocked bool\n}\n\ntype Item struct {\n product.Product\n Quantity uint8\n}\n\nfunc (c *Cart) TotalPrice() (*money.Money, error) {\n //...\n return nil, nil\n}\n\nfunc (c *Cart) Lock() error {\n //...\n return nil\n}\n\nfunc (c *Cart) delete() error {\n // to implement\n return nil\n}
\nThe product package :
\n// methods/example-project/product/product.go\npackage product\n\nimport "github.com/Rhymond/go-money"\n\ntype Product struct {\n ID string\n Name string\n Price *money.Money\n}
\nThe user package :
\n// methods/example-project/user/user.go\npackage user\n\ntype User struct {\n ID string\n Firstname string\n Lastname string\n}
\nAnd the main package (program starting point) :
\n// methods/example-project/main.go\npackage main\n\nimport (\n "log"\n "methods/example-project/cart"\n)\n\nfunc main() {\n newCart := cart.Cart{}\n\n totalPrice, err := newCart.TotalPrice()\n if err != nil {\n log.Printf("impossible to compute price of the cart: %s", err)\n return\n }\n log.Println("Total Price", totalPrice.Display())\n\n err = newCart.Lock()\n if err != nil {\n log.Printf("impossible to lock the cart: %s", err)\n return\n }\n\n}
\n\nThe methods TotalPrice
and Lock
(bound to Cart
) are exported
However the method delete
bound to Cart
is not exported
It means that delete
cannot be called from other packages (main, user, product)
But TotalPrice
and Lock
can be called in other packages.
We can choose receiver names freely. However, two practices are adopted among the community :
\nThe receiver name is usually a single letter
Generally the first letter of the base type
\nBase type : Cart
, receiver name c
Base type : User
, receiver name u
...
When you chose a receiver name stick with it in all your methods.
\nc
for all methods of the base type Cart
Create a new module on your computer
Copy and paste the code provided in the previous section.
Implement the two methods : TotalPrice
and Lock
The method TotalPrice
computes the total price of the cart and return it. You should use the module github.com/Rhymond/go-money
The method Lock
will... lock the cart to avoid modifications after confirmation
The method should update the field isLocked
to true
It should also update the field lockedAt
to the current time
If the cart is already locked, it should return an error
You will need to take a look at the documentation of the github.com/Rhymond/go-money
module.
The reflex is to go to https://pkg.go.dev/
Type “go-money” in the search bar and click on the module:
\nClick on “Expand” you will see the README.md file of the repository that gives you an example usage
TotalPrice
Let’s begin by writing the unit test. Note that we will cover more in detail unit tests in a dedicated chapter. For the moment if you are not familiar with this, just assum that this is a tool for testing that a method behave as expected.
\npackage cart\n\n// imports...\n\nfunc TestTotalPrice(t *testing.T) {\n items := []Item{\n {\n Product: product.Product{\n ID: "p-1254",\n Name: "Product test",\n Price: money.New(1000, "EUR"),\n },\n Quantity: 2,\n },\n {\n Product: product.Product{\n ID: "p-1255",\n Name: "Product test 2",\n Price: money.New(2000, "EUR"),\n },\n Quantity: 1,\n },\n }\n c := Cart{\n ID: "1254",\n CreatedAt: time.Now(),\n UpdatedAt: time.Now(),\n User: user.User{},\n Items: items,\n CurrencyCode: "EUR",\n }\n actual, err := c.TotalPrice()\n assert.NoError(t, err)\n assert.Equal(t, money.New(4000, "EUR"), actual)\n}
\nThe unit test function is named TotalPrice
,
We begin by creating a slice of 2 fake Item
elements : items
The ID
and the Name
fields are filled with test data
To create a price, simply call money.New(1000, \"EUR\")
The two items have different prices: 10.00 EUR for the first and 20.00 for the second (2000 divided by 100 = 20)
Then a new variable c
of type Cart
is created.
The field Items
is set to the value items
The field CurrencyCode
is set to “EUR” (the euro currency code)
The method is then called on c
Then we check that there is no error
\nassert.NoError(t, err)
money.New(4000, \"EUR\")
(40 Euros)\n// methods/application/cart/cart.go \n//...\n\nfunc (c *Cart) TotalPrice() (*money.Money, error) {\n total := money.New(0, c.CurrencyCode)\n var err error\n for _, v := range c.Items {\n itemSubtotal := v.Product.Price.Multiply(int64(v.Quantity))\n total, err = total.Add(itemSubtotal)\n if err != nil {\n return nil, err\n }\n }\n return total, nil\n}
\nWe create a variable total
initialized with 0 EUR (with the function money.New
that will create a variable of type *money.Money
We create a variable err of type error
Then for each item in the basket
\nWe compute the total price for the item : itemSubtotal
.
We call the method Multiply
from the type *money.Money
The receiver parameter is in that case v.Product.Price
(which is the price of the current product)
The method takes the quantity as parameter (we convert it from uint8
to int64
)
Then this subtotal is added to the grand total
\nWe call the method Add
(defined on the type *money.Money
)
The receiver parameter is total
The result is then reassigned to the variable total
Lock
// methods/application/cart/cart_test.go \n//...\n\nfunc TestLock(t *testing.T) {\n c := Cart{\n ID: "1254",\n }\n err := c.Lock()\n assert.NoError(t, err)\n assert.True(t, c.isLocked)\n assert.True(t, c.lockedAt.Unix() > 0)\n}\n\nfunc TestLockAlreadyLocked(t *testing.T) {\n c := Cart{\n ID: "1254",\n isLocked: true,\n }\n err := c.Lock()\n assert.Error(t, err)\n}
\nFirst we test that when a cart is locked :
\nthe field isLocked
is equal to true
the field lockedAt
is set with a time corresponding to a UNIX epoch greater than 0
To do that, we get the UNIX epoch with the method Unix
defined on the type time.Time
This is not optimal
A better test would have checked that the time is correctly set.
How to check that the time is correctly set? Maybe we could modify the method signature by adding a lock time as parameter. That way, we can check that lockedAt
is correctly set.
The second function will check that the function behaves as expected when the cart is already locked
\nTo do so, we set isLocked
to true
Then we call Lock
to check that an error occurred.
// methods/application/cart/cart.go \n//...\n\nfunc (c *Cart) Lock() error {\n if c.isLocked {\n return errors.New("cart is already locked")\n }\n c.isLocked = true\n c.lockedAt = time.Now()\n return nil\n}
\nFirst, we check that the cart is not already locked.
\nerrors.New
)Then we set the value of is isLocked
to true
And the value of lockedAt
to the current time (time.Now()
) .
What is a method receiver?
Give an example of a method with a value receiver.
Give an example of a method with a pointer receiver.
Fill in the blanks. A method with a _______ receiver can modify their receiver.
True or False? Given a type User
, we can have a method named Logout
with a value receiver User
and a method named Logout
with a pointer receiver *User
.
How to control method visibility?
What is a method receiver?
\nA method receiver is an additional parameter
It is specified in a special parameter section that precedes the method name.
func (c *Cart) Lock() error
\nc
and is of type *Cart
Give an example of a method with a value receiver.
\nfunc (t T) MethodExampleName() error
Give an example of a method with a pointer receiver.
\nfunc (t *T) MethodExampleName2() error
Fill the blanks. A method with a _______ receiver can modify their receiver.
\nTrue or False ? Given a type User
, we can have a method named Logout
with a value receiver User
and a method named Logout
with a pointer receiver *User
.
False
The type *User
has a method set composed of all the methods with a receiver of type *User
and methods with a receiver of type User
The name of methods inside a method set should be unique.
In other words you cannot have a method with the same name, even if the receiver is a pointer receiver or a value receiver.
How to control method visibility?
\nTo make it visible outside the package where it’s defined, name the method with a first letter capitalized
To make it invisible outside the package where it’s defined, name the method with a first letter NOT capitalized
Methods are functions that is attached to a type
Each method has a receiver
The receiver is an additional parameter specified before the name of the function
func (t T) MethodExampleName() error
\nThe receiver can be a pointer to a type T
(denoted *T
)
The receiver can have a type T
When your method has a pointer receiver, you allow the method to modify the receiver’s value.
\nfunc (c *Cart) Lock() error
\nGenerally, the receiver has a one letter name, that is generally the first letter of the receiver type
\nfunc (c *Cart) Lock() error
(package time, standard library)
func (m *Money) Multiply(mul int64) *Money
(package money, module github.com/Rhymond/go-money)
The first letter of the method name control its visibility
\nFirst letter capitalized: Visible
First letter not capitalized: Impossible to call it from outside the package.
Method names should be unique in a method set
https://golang.org/ref/spec#Method_declarations↩︎
Previous
\n\t\t\t\t\t\t\t\t\tTypes
\n\t\t\t\t\t\t\t\tNext
\n\t\t\t\t\t\t\t\t\tPointer type
\n\t\t\t\t\t\t\t\t