Lesson 14
Creating Methods

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.

WORKING WITH METHODS

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.

DEFINING A METHOD

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.

USING POINTERS WITH METHODS

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.

NAMING METHODS

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}

WORKING WITH VALUE RECEIVERS AND ARGUMENTS

Here's another difference between methods and functions:

  • If a function accepts a value argument, it can only accept a value argument.
  • If a method accepts a value receiver, it can accept either a value receiver or a pointer receiver.

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

WORKING WITH POINTER RECEIVERS AND ARGUMENTS

The reverse is also true in regard to working with pointer receivers and pointer arguments:

  • If a function accepts pointer arguments, it will only accept pointer arguments.
  • If a method accepts a pointer receiver, it will accept both pointer and value receivers.

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.

SUMMARY

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:

  • If a function accepts a value argument, it can only accept a value argument.
  • If a method accepts a value receiver, it can accept either a value receiver or a pointer receiver.
  • If a function accepts pointer arguments, it will only accept pointer arguments.
  • If a method accepts a pointer receiver, it will accept both pointer and value receivers.

EXERCISES

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:

Exercise 14.1: Functioning with Integers

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).

  • Compute the max of an array of int.
  • Compute the index of the max of an array of int.
  • Compute the min of an array of int.
  • Compute the index of the min of an array of int.
  • Sort an array of int in descending order and return the new sorted array in a separate array.
  • Sort an array of int in ascending order and return the new sorted array in a separate array.
  • Compute the mean of an array.
  • Compute the median of an array.
  • Identify all positive numbers in the array and return the numbers in a slice.
  • Identify all negative numbers in the array and return the numbers in a slice.
  • Compute the longest sequence of sorted numbers (in descending or ascending order) in the array and return in a new array. For example, with input of [1 45 67 87 6 57 0], the output should be [1 45 67 87].
  • Remove duplicates from an array of ints and return the unique elements in a slice.

Exercise 14.2: Methods with Integers

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.

Exercise 14.3: Volume of a Solid

Create a program that will calculate the volume of a solid. Start with the following structs:

  • Cube: Represents a cube with one attribute: length: float64
  • Box: Represents a box with three attributes: length, width, height as float64
  • Sphere: Represents a sphere with one attribute: radius: 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.

Exercise 14.4: Banking Terminal

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.

Bank Account

Create a struct to represent the accounts. Include the following information:

  • Account number: string
  • Account owner: struct of type entity that includes:
    • ID
    • Address
    • Entity type: string (Individual or Business)
  • Balance: float64
  • Interest rate: float64
  • Account type: string (checking, savings, or investment)

Wallet

Create a struct to group accounts by owner:

  • Wallet ID: string
  • Accounts: The different accounts in the wallet (choose appropriate data structure)
  • Wallet owner: struct of type entity

Define the Methods

Implement the following methods for the account struct:

  • withdraw method: Implement necessary logic to validate balance before performing a withdrawal.
    • Check that balance is greater than the amount to be withdrawn and that balance is not negative.
    • The withdraw method should take as input the amount to be withdrawn.
  • deposit method: This method should take as input the amount to be deposited.
  • apply interest: This method should apply an interest rate to the balance of the account as follows:
    • For individual accounts:
      • 1% APR for checking accounts
      • 2% APR for investment accounts
      • 5% APR for savings accounts
    • For business accounts:
      • 0.5% APR for checking accounts
      • 1% APR for investment accounts
      • 2% APR for savings accounts
  • wire: This method should mimic wiring money to another account.
    • The accounts can be owned by the same entity or by different entities.
    • The method will need the source account and the target account.
    • The method will need the amount to be wired.
    • The method will deduct the amount from the source (after checking the validity of the amount) and add it to the target.

Implement the following method for the entity account:

  • change address: Changes the address of the entity

Implement the following methods for the wallet struct:

  • display accounts: Iterate and display the information from each account in the wallet.
    • The method should display the accounts in the following order:
      • The checking accounts first
      • The investment accounts second
      • The saving accounts last
  • 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.
    • The source account must be in the wallet.
    • The target account can be in the wallet or external.
    • If the balance in the account is too low, display an error message.

main Function

Create a main function that can:

  • Create account, entity, and wallet types.
  • Showcase the different methods implemented, based on the user interaction with the accounts.

Challenge

After you have the basic program working as expected, add the following updates:

  • Create a nice banking terminal that allows users to:
    • View accounts
    • Interact with accounts (deposit, withdraw, etc.)
    • View the wallet
    • Interact with the wallet
  • Identify at least one design change that will make the structs and your program more elegant and efficient, such as:
    • Adding new attributes
    • Adding new structs
    • Implementing new methods
  • Instead of an error message, the wire method should recommend another account in the wallet that has enough of a balance to perform the transfer.
  • Can you change the account and wallet structs so that you are able to compute the overall interest rate paid to all the accounts in the wallet?
    • You will need to store the interest amount somewhere each time you apply interest to a particular account.
    • Use appropriate data structures and logic to implement it.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset