Lesson 11
Organizing with Structs

Up to this point, you've used a variety of individual variables. When it has come to grouping or relating variables, you've learned about arrays, which group a number of values of the same type under a single variable name. There are also times when you want to associate a number of different variables to a single group. For instance, you can create a bank account item with fields that represent information about a bank account, such as an account holder, an account number, and a balance.

A struct is a data structure that includes a list of fields. Structs are an important aspect of any programming language in that they allow you to organize a group of variables into a related unit such as an account. This lesson takes a close look at the use of structs in Go.

DECLARING AND INITIALIZING A STRUCT

Structs allow you to create complex data types. A struct in Go can contain one or more fields. Each field in the struct must have a data type. You can use the struct keyword to create a struct using syntax like this:

type structName struct {
   field1 string
   field2 float64
[…]
}

Listing 11.1 creates a struct named account with two fields, accountNumber and balance, and assigns a value to each of those fields.

Let's look at the code. You define a struct by using the type keyword followed by the name of the struct and then the keyword struct. You use the keyword type because you're creating a new data type that you can use in the listing. The data type you're creating in this case is an account struct, named account. In the account struct, accountNumber represents the account number, defined as a string, and balance represents the current account balance, using float64.

In the main function of this listing, you define a variable using the account type you've defined. You declare a variable named a in the same way you would any other variable; however, in this case you use account as the type:

var a account

To access the individual fields of the new struct you created, you use the . operator (a period). More specifically, you use the name of the struct followed by the period, followed by the name of the field. In order to access and assign values to the fields in your account struct called a, you do the following:

a.balance = 140
a.accountNumber = "C14235345354"

Finally, you print the struct, which displays the values of the fields within the struct:

{C14235345354 140}

Retrieving Values from a Struct

You could've also printed the individual fields in the struct by using the same format you used to assign values. This is illustrated in Listing 11.2, which adds two print statements to the previous listing.

In this example, you use a.number to retrieve only the account number, whereas a.balance retrieves only the account balance. The output is as follows:

{C14235345354 140}
The account number is C14235345354
The current balance is 140

Initializing a Struct When Declaring

As with other variables, you can declare and initialize a struct variable in a single statement. Listing 11.3 uses the struct literal to assign values to the fields in the struct. In a struct literal, you simply assign values inside curly brackets ({}) when you declare the variable.

In this example, the values in the curly brackets are assigned to the struct's field in the order they appear in the struct itself. As such, C14235345354 will be assigned to the accountNumber string and 140 will be assigned to balance. When this listing is executed, the output looks like this:

(C14235345354 140}

Using the Short Assignment Operator

Another option is to use the short assignment operator (:=) with the struct literal. You've used this operator in the same manner when declaring basic variables of basic data types as well. Listing 11.4 shows the short assignment operator being used with a struct.

This listing again creates the account struct with a string and float64 value. The struct is then used in the main function to declare two new variables. Using the short assignment operator, the first variable called a is defined to be an account and the values C13242524 and 140.78 are assigned to its fields. You see these values printed in the first line of the output.

The second variable called b is also created using the short assignment operator, which declares and initializes the struct. You should notice that there are, however, no values between the curly brackets. This means that default values will be assigned to the fields. By default, these fields are assigned 0, which is a blank for a string. You can see this also from the second line of the output:

{C13242524 140.78}
{ 0}

USING KEY-VALUE PAIRS WITH STRUCTS

When initializing values in a struct by passing a value between curly brackets, you must make sure that the values are listed in the same order as the struct fields. Additionally, all fields must be initialized. Consider your previous struct for an account:

type account struct {
   accountNumber string
   balance float64
}

When you initialize this, let's see what happens if you pass the balance first and the account number second:

a := account{140.78, "C13242524"}

In this case, a number is being assigned to a string and a string to a number, so errors will occur:

# command-line-arguments
.listing.go:11:17: cannot use 140.78 (type untyped float) as type string in field value
.listing.go:11:25: cannot use "C13242524" (type untyped string) as type float64 in field value

If you were to create an account with an account number but not assign a balance, you might be tempted to do the following:

a := account{"C13242524"}

In this case the account number is provided, but there is no balance. While you might assume that this would default the balance to 0, that is not the case. Instead, this produces an error indicating there are too few elements:

# command-line-arguments
.listing.go:11:17: too few values in account{…}

In a small struct with two fields, it is easy to make sure things are in order and all fields are assigned; however, as programs get more complex and structs get larger, this might not be the case. Go provides a way to get around these two restrictions by using key-value pairs.

When you use key-value pairs to initialize a struct, fields can be initialized in any order and not all fields have to be initialized. A key-value pair is simply the name of a field followed by a colon (:) and then the value being used to initialize the field:

field : value 

Each key-value pair is separated by a comma. Listing 11.5 once again presents the account struct to illustrate the use of key-value pairs.

The unique part of this listing is the creation and initialization of the account structs called a and b. In the first declaration, a is initialized using key-value pairs. You can see that the field names are listed, followed by a colon, then by the value you want to assign. The order of the assignments in this case is reversed so that the balance is passed first and the account number second. When the a struct is printed, you can see that the fields are initialized correctly:

{C13242524 140.78}

In the creation of the struct called b, only the account number is assigned. You can see that there is no initialization of the balance. As such, by default the balance will be set to 0. This can also be seen when the b struct is printed:

{S12212321 0}

USING THE NEW KEYWORD

It is also possible to create a struct using the new keyword. Listing 11.6 shows the use of the new keyword to declare the same account struct you've been using.

In this listing, you can see that the new keyword is used to create a new struct to assign to a variable. The type of struct to be created (the struct's name) is passed within parentheses after the new keywords. In this case, a new account struct is being created and assigned to the variable named a. Everything else about the listing, including how values are assigned, is done in the same manner shown before.

It is important to note that the new keyword is returning a pointer to the struct that is being created. This means that if the struct a in Listing 11.6 were to be passed to a function, the function would not be using a copy of the struct, but rather would be referencing the original object that was created. Listing 11.7 illustrates this by making a change to the account number within a function.

In this listing, you're again using the same account struct within your main function. You're creating a new account called a, which is assigned an account number and balance. The account number is printed and looks just like you've seen before:

C13242524

This listing also includes a function called closeAccount that receives an account struct as an argument. This struct is called CurrentAccount within the function. What you can see, however, is that CurrentAccount is a pointer that points to an account.

Within the function, CurrentAccount is used in the same way that you've been using the account struct objects up to this point. The account number is being assigned a new value, which is the original value with the text "CLOSED-" concatenated to the front.

Program flow returns to the main function after calling closeAccount passing the struct called a. When the value of a.accountNumber is printed, you can see that the struct was indeed passed by reference and not by value, because the account number has been updated:

CLOSED-C13242524

POINTERS AND STRUCTS

In the previous section you saw that the new keyword can be used to create a pointer to a struct. It is possible to create a pointer in the same way you did in Lesson 10, “Working with Pointers.” Listing 11.8 creates a pointer that can access and update values in the fields in an account struct.

In this listing, you create a struct called a and assign values to it. You then declare a variable called p and assign it the address of a, which means p will be created as a pointer to a struct, or more specifically a pointer to an account struct. You then use the rest of the listing to assign a value to the balance of your struct, which is then printed.

Note that both of the following statements work to assign a value to the field:

(*p).balance = 220

and:

p.balance = 320

In the first version, you explicitly represent p as a pointer, using *. However, because you have already defined p as a pointer, you can simply reference it in the same way you would reference the struct itself. The second version uses simpler code to perform the same task. When this listing is executed, you see the balance was indeed assigned and printed in both ways:

{C21345345345355 220}
{C21345345345355 320}

NESTED STRUCTS

Go supports nested structs, which allow you to create structs and then use those structs as data type fields for other structs. In the example presented in Listing 11.9, you create a new entity struct that represents a person or business that owns an account. You then include an entity struct in your original account struct.

Within the main function of this listing, you create an entity struct called e, which is initialized with values when declared. You then create an account struct called a. You assign values to the fields in a individually. Note that when you assign the owner value to the account struct, you simply reference the variable that represents the entity struct:

a.owner = e

The output from printing the struct called a includes the nested struct:

{C21345345345355 140609.09 {000-00-0000 123 Main Street}}

It is worth noting that if you wanted to print just the id that is stored in the entity struct within your account, you would need to include the account (called a in the listing), owner (which is the name of the entity struct within the account struct), and the field name, which is called id. To print the value, you would use this:

fmt.Println(a.owner.id)

ADDING METHODS TO A STRUCT

Up to this point, we've shown structs that contain fields that tend to define the properties of an object created by a struct. A struct can also contain functions, which can also be referred to as methods.

Listing 11.10 adds a method to your account class. You create the HaveEnoughBalance method, which you can use in the main function to check if the account has enough money to be withdrawn.

In this listing, the account struct is again being created and includes the entity struct you saw in the previous listing. Where this listing differs is in the inclusion of the HaveEnoughBalance method. You know this is a function because of the use of the func keyword. Overall, the method should look very similar to what was seen in Lesson 8, “Using Functions.”

What is different is that within the function's declaration, there is a receiver type of account included in the method declaration:

func(acct account) HaveEnoughBalance(value float64) bool{
   if acct.balance>= value{
      return true
   }
   return false
}

This tells Go that the method is bound to the account struct and thus will be available to any account structs that are created. Within the method, you can then have access to the fields in the struct that have been created. In this case, the method compares the provided value called value (which contains 150 in the main function) to the current value in the account struct (acct.balance) to determine the outcome. In this case, because the account balance is greater than 150, the output is:

true
{C21345345345355 140609.09 {000-00-0000 123 Main Street}}

Within the main function, the HaveEnoughBalance method is called by using the name of the struct followed by the dot operator and then the method name. The appropriate parameters are also passed, in this case, simply a value of 150:

a.HaveEnoughBalance(150)

Note that just as 150 is associated and passed to the value argument in your HaveEnoughBalance method, the struct called a is associated to the acct receiver type in your method.

TYPE AND VALUE

By importing the Go reflect package, you can check the type and value of a struct using the TypeOf and ValueOf functions. The code in Listing 11.11 shows these functions in use.

Remember that the struct keyword is used to create structs, which are considered new types. In this listing, you update the previous listing and add calls to the TypeOf and ValueOf functions from the reflect package. You use these to print the type and value of the variables a and e that were defined in the listing. You can see that these print the type and value of both structs. The output is:

Type and value of a:
main.account
{C21345345345355 140609.09 {000-00-0000 123 Main Street}}
 
Type and value of e:
main.entity
{000-00-0000 123 Main Street}

COMPARING STRUCTS

You can use the == operator to compare two structs. Using this operator, each element within a struct will be compared to verify if they are the same. In Listing 11.12, three different variables are created using the entity struct, and then they are compared to each other.

Because both values assigned to e1 and e2 are the same, the first comparison statement returns true:

{000-00-0000 123 Main Street}
{000-00-0000 123 Main Street}
true

However, you have a slightly different address in e3, so e1 is different from e3:

{000-00-0000 124 Main Street}
false

SUMMARY

In this lesson you've learned how to associate a group of fields and methods into a package that can then be used to create new types. Using structs, you should be able to better organize the information used within your programs.

You should now be able to create your own structs (including nested structs) as well as use them within the code you create. You should be able to add, update, and display values from a struct as well as determine their types, compare them, and even create pointers to access them.

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 11.1: Addressing an Envelope

Create an address struct that contains the following items:

  • First name
  • Last name
  • Address line 1
  • Address line 2
  • City
  • State
  • Country
  • Zip code/postal code

Write a program that creates and initializes a variable using the struct. Assign values and then use fmt.Print and fmt.Println to display the address in a manner that would look good on an envelope. Include logic to avoid printing address line 2 if it is blank and to add a comma between the city and state. For example:

John Doe
123 My Street
Chicago, IL  12345

Exercise 11.2: Name

Create a struct to hold a name. It should include a first name, last name, and middle name. Rewrite your solution from Exercise 11.1 to use this name struct (nested) instead of the individual fields.

Exercise 11.3: Full Name

Update the struct you created in the previous listing that holds a name. Add a method to your name struct called GetFullName that doesn't take any parameters but returns a string from the struct that contains the full name of the person in the current name struct. The full name should be formatted as first name, middle initial, a period, then last name. If there is no middle name, then the period should not be included.

Exercise 11.4: Address Book

Using the address struct you've created in the previous listings, create an array of addresses called AddressBook. Assign at least four addresses to the array. Print the resulting addresses in a format that would look appropriate for an envelope.

Exercise 11.5: Passing a Struct

Compare Listing 11.13 to Listing 11.7 within the lesson. What are the differences? Before running Listing 11.13, predict what the final value of a.accountNumber will be. Enter the code and see if you are correct.

Exercise 11.6: Nesting Practice

Consider a different scenario where it makes sense to nest a struct in another struct. Using that scenario, create a struct that contains at least one nested struct. Using the structs, write a program that creates and displays at least two different variables using the structs.

Examples include:

  • A bank customer with multiple accounts
  • A student taking multiple courses
  • A course with multiple students

Exercise 11.7: Burger Shop

In this activity, you will create an online ordering system for a burger shop. This exercise will require using what you've learned in the book up to this point. The program will perform the following tasks:

  • Take an order from a user, including a burger, a drink, a side, or a combo.
  • Customize selected items, including toppings for the burger.
  • Display the completed order to the user.
  • Calculate and display the order total.
  • Allow the user to cancel the order and exit the program at any prompt.
  • If the user enters an unexpected value, provide appropriate feedback and prompt them to try again.

The program must include the following features:

  1. Create a struct that represents a burger with the following attributes:
    • Bun: Boolean (True if you want the burger with bun, False otherwise)
    • Price: float64
    • Dressed: Boolean (True if you want the burger fully dressed, False otherwise)
    • Do a basic Boolean option for the first pass. You can add other options after the main program works as expected.
  2. Create a struct that represents a drink with the following attributes:
    • Price: float64
    • Type: string
  3. Create a struct that represents a side with the following attributes:
    • Price: float64
    • Type: string
  4. Create a nested struct called Combo that represents a burger, side, and drink combo:
    • Burger
    • Side
    • Drink
    • Price: The price of the combo is the price of the three items with a 20% discount.
  5. Next, implement the following functions:
    • A function called user_input_burger that asks the user how they want their burger and stores it in a struct type. The price of the burger is computed as follows:
      • Burger with bun: 7
      • Burger without bun: 6
      • Dressed has no impact on price.
    • A function for user_input_drink, which asks the user for their drink. Provide the user with a limited number of drink options to choose from (include 3–4 choices).
      • The price of water is $1.
      • The price of any other drink is $2.
    • A function for user_input_side, which asks the user for their side. Provide the user with a limited number of side options to choose from, such as fries, onion rings, a salad, and coleslaw.
    • The price of fries is $2.
    • The price of any other option is $3.
    • A function for user_input_combo, which asks the user for their combo preferences.
    • A function called take_order_from_user
      • Ask the user for a name for the order.
      • Repeat taking the order until the user is done.
      • Display order details.
      • Display a thank you message.

Challenges

Once the basic program outlined here works as expected, consider refactoring it to improve it. For example:

  • Allow the user to choose specific toppings on the burger, including items such as cheese, lettuce, tomato, onion, pickles, mustard, and ketchup, and/or offer a choice of buns.
  • Allow non-burger options, such as a fish sandwich, tacos, or personal pizza.
  • Allow the user to choose a drink size, with appropriate pricing.
  • Allow user-specific customization, such as low-salt or extra pickles.
  • Add a dessert menu.
..................Content has been hidden....................

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