Go doesn't support classes like Java or C# does. However, Go offers some aspects of object-oriented programming (OOP) and ways to reuse code. For example, though Go doesn't support classes, it does support the concept of a method. You learned a little bit about methods in Lesson 8, “Using Functions.” In this lesson, we return to the topic of methods and dive a little deeper.
Methods in Go are functions defined on a particular data type. The only difference between a function and a method is that a method has a special receiver type that it can operate on. To understand the difference between functions and methods, consider the code in Listing 14.1.
In this code, you create a struct, account
. You then define a function, HaveEnoughBalance
, that takes as input a value and returns bool
. If you look closely, after the func
keyword, you see (acct account)
. This tells Go that the function is actually a method and that the receiver is an account
type. In the same code, you then define a traditional function, HaveEnoughBalance2
, that uses an account
value as input rather than being a receiver.
Both HaveEnoughBalance
and HaveEnoughBalance2
have the same functionality except that one is a method with an account type receiver and the other is a function. The output is the same for both:
Method result: false
Function result: false
The difference between the function and the method is that the method includes a receiver argument (acct account)
, which instructs the compiler to execute the method through account types, whereas the function requires the account to be passed as an argument. The advantage of the method using a receiver argument is that the HaveEnoughBalance
method can now be executed through the account type. You see this done in the print statement, where the method is called using the following syntax:
a.HaveEnoughBalance(150)
This is very similar to the object-oriented approach using classes (struct in this case) and methods. You can use methods in Go to create various routines that define the behavior of the receiver struct.
You are not limited to defining methods on structs. In fact, you can define them on any other type if you wish. In Go, it is possible to create new types based on a built-in type using this syntax:
type identifier builtin_datatype
The identifier
of a custom type must follow the same naming convention used for other identifiers. In the code in Listing 14.2, you use the keyword type
to create a new type called s
based on the existing string
type. You then use the type as a receiver in a method.
The output for this listing is:
type value: Hi
method value: false
Why would you create a new data type that acts like a string instead of simply passing a string type to the method? As a rule, in Go, the definition of the receiver type must be in the same package as the method. This means that you cannot use the string as the receiver type because it is not in the same package as the method you are creating.
To see what happens when you try to use a string as the receiver for a method, consider the code in Listing 14.3.
In this listing, you are creating a method with a receiver type of string
. When executed, the output generates an error:
# command-line-arguments
.main.go:6:5: cannot define new methods on nonlocal type string
.main.go:14:12: undefined: s
In this version, the receiver of the method IsEmpty
is of type string
. This code won't work because the string
type is not in the same package as the method IsEmpty
.
In the previous examples, you have used method receivers that are values (called value receivers) instead of pointers. When you pass values to methods, a copy of the value is made, and the method operates on a copy of the input. This could be an issue. To understand this problem, consider the code in Listing 14.4.
In this code, you create a struct, account
, and a method, withdraw
, which mimics a withdrawal from an account. The method receiver is a value receiver of type account. When we execute the code, the output is:
{C21345345345355 159}
true
{C21345345345355 159}
The Boolean part of the method executed correctly in that the original balance (159) is greater than the withdrawal amount (150), but the value of balance
in the struct did not change as expected. This is because the method operated on a copy of acct
, rather than using the values directly.
One work-around that will update the changes in the caller a
properly is to return the new value acct
and assign it to the old version of acct
, as shown in Listing 14.5.
In this version, you reassign the copy created by the withdraw
method to acct
. The result includes the updated balance, as can be seen in the output:
{C21345345345355 9}
True
{C21345345345355 9}
Although the code in Listing 14.5 overcomes the issue with updating acct
, there is a more elegant solution. This is accomplished by using pointers as receivers.
Instead of using value receivers and having to create work-arounds for the method's copy, you can use a pointer receiver. In other words, instead of passing the value as a receiver, you pass a reference to it (a pointer). This approach will directly change the value assigned to the caller acct
by changing the value assigned to that memory block. Listing 14.6 is the same as the earlier example, except that the receiver is a pointer receiver.
In this listing, you can see that the primary change is that a pointer (*account
) is being used in the receiver instead of just account
for the type. Using a pointer receiver means that the method accesses the memory location of the stored value, rather than accessing the value itself. When it updates the value, the update is stored to the same location, allowing the original variable to retrieve the updated value.
By using the pointer, the output is now:
{C21345345345355 159}
true
{C21345345345355 9}
The choice to use value receivers or pointer receivers depends on the situation. If you want the changes that the method performs to be visible to the caller, you should use a pointer receiver. Otherwise, you can use a value pointer.
In Go, you cannot create multiple functions with the same name. On the other hand, you can create several methods with the same name, using them to operate on different data types, an approach similar to the concept of overloading in object-oriented programming. This is another advantage of using methods over functions. Listing 14.7 illustrates this by creating a program with methods that mimic withdrawing funds from both checking and savings accounts.
In this listing, you create two struct types: one, called CheckingAccount
, is for a checking account and the other, called SavingsAccount
, is for a savings account. You then define the same method called Withdraw
twice. These methods use the same signature other than the receiver type. The method mimics a withdrawal from a savings account and a checking account.
In the main
function, you create a struct called acct
using the SavingsAccount
struct. You assign values and then call with the Withdraw
method. You repeat the process, creating a struct called acct2
, this time using the CheckingAccount
struct, but also calling its Withdraw
method. The output from the listing is as follows:
savings balance {S21345345345355 159}
withdraw from savings: true
new savings balance {S21345345345355 9}
checking balance {C218678678345345355 2000}
withdraw from checking: true
new checking balance {C218678678345345355 1850}
Here's another difference between methods and functions:
To understand this difference, let's consider the code in Listing 14.8, which uses a method defined with a value receiver. Note that this method is defined to take a value receiver, but the listing passes a pointer.
In this example, you use the withdraw
method, and you provide a pointer receiver. The method accepts the receiver, and the output is:
Before: &{C21345345345355 159}
After: &{C21345345345355 159}
Although this does not throw an error, note that the balance value does not change; the output still reflects a balance of 159, even after the method supposedly subtracted 100 from that value. Using a pointer receiver for a method that accepts value receivers works, but it is important to remember that the method still copies the passed pointer and operates on the copy, rather than on the value itself. In order to see the changes in the caller, you still need to use pointer receivers.
If you update the main
function to use a withdraw
function rather than the method, the program will not run, as you can see if you try to run Listing 14.9.
In this listing, withdraw
has been changed to a standard function. You can see that instead of using a receiver type, the account is passed as a parameter. As in the previous listing, you create a pointer (ptra
) that points to an account. This pointer is passed to the function. The output is an error:
.Listing1409.go:29:11: cannot use ptra (type *account) as type account in argument to withdraw
The reverse is also true in regard to working with pointer receivers and pointer arguments:
Listing 14.10 is similar to what you've been using; however, this time you use a method defined with a pointer receiver, but a value is defined and used in the main
function.
You can see that the withdraw method receives a float64
argument called value
. Because this is a value type, the expectation might be that a copy of the value would be received, and the original value would not be impacted. The result of the program shows that this is not the case:
Before: {C21345345345355 159}
After: {C21345345345355 59}
Because the method uses a pointer receiver, the account balance updates, even with a value provided.
Now let's look at the same example using a function with a pointer parameter instead of a value parameter. This is shown in Listing 14.11.
In this listing, the withdraw
function accepts a pointer to an account. In the main
function, you define a pointer, but you don't use it. Rather, you pass acct
, which is an account
struct, to withdraw
. When you run the code, it throws the following error:
.Listing1411.go:27:12: cannot use a (type account) as type *account in argument to withdraw
You can change the call to withdraw
to use a pointer:
withdraw(ptra,150)
In this case, the function gets the pointer it expects and thus works as expected:
0xc000006028
Before: {C21345345345355 159}
After: {C21345345345355 9}
The key to remember in this case, however, is that if the function expects a pointer, then a value type can't be sent.
In this lesson, we returned to the topic of methods as well as functions. Not only were the differences between methods and functions mentioned, but some aspects of object-oriented programming were also covered, such as ways to reuse code. We know that Go doesn't support classes, but it does support the concept of a method, and you learned how methods with the same name can be used.
We also covered the use of pointers versus value types for method receivers as well as arguments and parameters. It is important to understand receiver type in order to get the results you expect. Keep the following rules in mind as you build your own methods and functions:
The following exercises are provided to allow you to experiment with the tools and concepts presented in this lesson. For each exercise, write a program that meets the specified requirements and verify that the program runs as expected. The exercises are:
Create a function that takes as input an int
, n
, and returns an array of length n
with random integers between −100 and 100. Then implement a function for each of the following bullets. Test that each function will return an input array of 100 integers. Do not use the sort package or any built-in function (like min
or max
).
max
of an array of int
.max
of an array of int
.min
of an array of int
.min
of an array of int
.int
in descending order and return the new sorted array in a separate array.int
in ascending order and return the new sorted array in a separate array.[1 45 67 87 6 57 0]
, the output should be [1 45 67 87].
int
s and return the unique elements in a slice.Modify the code you wrote for Exercise 14.1 so that you add methods to the array instead of creating standalone functions. The receiver type of your methods should be the integer array you create.
Create a program that will calculate the volume of a solid. Start with the following structs:
float64
float64
float64
Implement a volume
method for each of the structs defined above. The volume
method returns the volume of a cube, box, or sphere. Use the main
function to create different shapes and compute their volume.
After verifying that the program meets the requirements and works correctly, add additional shapes. For example, you could add a cuboid, a cone, and a pyramid.
In this exercise, you will create a banking terminal that allows a user to manage their bank account. Start by creating the following structs. Use an appropriate name for each struct.
Create a struct to represent the accounts. Include the following information:
string
entity
that includes:
string
(Individual or Business)float64
float64
string
(checking, savings, or investment)Create a struct to group accounts by owner:
string
entity
Implement the following methods for the account
struct:
withdraw
method: Implement necessary logic to validate balance before performing a withdrawal.
balance
is greater than the amount to be withdrawn and that balance
is not negative.withdraw
method should take as input the amount to be withdrawn.deposit
method: This method should take as input the amount to be deposited.interest
: This method should apply an interest rate to the balance of the account as follows:
wire
: This method should mimic wiring money to another account.
Implement the following method for the entity
account:
change address
: Changes the address of the entityImplement the following methods for the wallet
struct:
display accounts
: Iterate and display the information from each account in the wallet.
balance
: Iterate through all accounts and return the overall balance in all accounts.wire
: This method will mimic a wire from a source account to a target account.
Create a main
function that can:
After you have the basic program working as expected, add the following updates:
wire
method should recommend another account in the wallet that has enough of a balance to perform the transfer.account
and wallet
structs so that you are able to compute the overall interest rate paid to all the accounts in the wallet?