- Variables
- Functions
- Arrays & Slices
- Types & Receiver functions
- Writing To File
- Reading From File
- Unit Test
In this review, we are implementing a system of cards and deck, and review the following concepts:
- Variables
- Functions
- Arrays & Slices
- Iteration with
for-loops - Types & Receiver Functions
- Writing To File
- Reading From File
- Error Handling
- Unit Test
- Variables can be initialized inside or outside of a function
- But can only be assigned a value inside a function
- Go uses the
varkeyword to declare variables - Variables are typed
- Go is a statically-typed language
- Every declared variables must be used
// Declare
// var <name> <type>
var myCard string
// Initialize
// <name> = <value>
myCard = "Ace of Spade"
// Declare and Initialize
// var <name> <type> = <value>
var yourCard string = "Jack of Heart"var- Keyword to create a new variable
- Every declared variables must be used at least once
myCard- Name of the variable
string- Data type of the variable
- Go is a statically-typed language
- The variable type follows the variable name
- Go fundamental types
stringboolintfloat64
- We can also declare only, then assigned a value later
- When using this approach, Go assigns the zero-equivalent default value of the type to the declared variable
// Declare variable:
// Default zero-value for string => ""
var someCard string
// Declare variable:
// Default zero-value for int => 0
var someInt int
// Assign value to variables later
someCard = "5 of Heart"
someInt = 1001- Go can also automatically infer the variable type from the assigned value
- We use
:=and omit thevarkeyword - Only use
:=when declaring a new variable WITH initialization AND type inference
- We use
// These are equivalent to the above declarations
someCard := "5 of Heart" // Inferred type: string
someInt := 1001 // Inferred type: int- Obviously, we can re-assign values to any variable
- We can only assign value of the same type
- However, make sure to use
=instead of:=when re-assigning:=is only used for initializing with type inference=is used for all successive re-assignments
- Make sure that the type of the value matches the declared type of the variable
// Reassigning a string variable
someCard = "10 of Diamond"
// Reassigning an int variable
someInt = 2000- In Go, there are 2 principal types of functions:
main() Function |
Helper Functions |
|---|---|
| Only one per project | Can be multiple per project |
Contained in the main.go file |
Contained in differently-named .go files |
Declared with package main |
Declared with different package names |
| This is the entry-point of execution of an executable | These are re-usable blocks of logic for DRY |
// func <name>(<arg?> <argType?>) <returnType?> {
// <body>
// ...
// <return?>
// }func newCard() string {
newCard := "5 of Diamonds"
return newCard
}func- Keyword to declare a function
name- Name of the function
args- Arguments of the function
- Arguments can be multiple
- Arguments are optional
- Argument types must be specified
returnType- The type of the value returned by the function
- A function returning a value needs to explicitly declare its
returnTypein its declaration - If the function returns nothing (e.g. only prints to the screen), skip the
returnType
body- The body of the function's logic
- Typically ends with a
returnstatement to return the value from the function - However,
returnis optional and can be skipped - Returned value must match the
returnTypeof the function - A function returning a value needs to declare its
returnTypein its declaration
// Package
// *******
package main
// Imports
// *******
import "fmt"
// Helper Functions
// ****************
func newCard() string {
return "5 of Diamonds"
}
func getAge() int {
return 20
}
// Main Function
// *************
func main() {
// We are calling a function and assigning its return value to the variable
// When calling a function, the return type of the function becomes the type of the variable it is assigned to
var card string = newCard() // string
var age int = getAge() // int
// Making use of the variables
// All declared variables must be used
fmt.Println(card)
fmt.Println(age)
}- We can return multiple values using tuple-like
- On function, make sure to annotate the
returnTypeusing a tuple-like format
- On function, make sure to annotate the
- We can also assign multiple variables using tuple-like unpacking
// A function that returns a tuple-like (deck, deck)
func deal(d deck, handSize int) (deck, deck) {
// Split the original deck into 2 using the handSize
hand := d[:handSize]
remainingDeck := d[handSize:]
// Return the "hand" and the "remaining deck" as a tuple
return hand, remaining_deck
}2 types of data structures in Go for handling lists of records
- Basic list of values
- 0-based index
- Same element access syntax as typical lists and arrays
- Fixed-length
- Primitive Data Structure for lists
- All of its elements must have the same type
- Useful when needing a static list of constants
// Array declaration format
variable := [length]type{csvValues}
// Example of Array
days := [7]string{
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
}- Same element-access syntax as typical lists and arrays
// Accessing an array element
today := days[0]
fmt.Println("Today is", today)- A bit advanced list of values than arrays
- 0-based index
- Same element-access syntax as typical lists and arrays
- Flexible-length: Can grow or shrink in length
- All of its elements must have the same type
- Useful when needing to work on dynamic lists
// Slice declaration format
variable := []type{values}
// Example of a Slice
cards := []string{
"Ace of Diamond",
newCard(),
newCard()
}
// Example of a Slice
fruits := []string{
"apple",
"banana",
"grape",
"orange",
}- Same element-access syntax as typical lists and arrays
- 0-based indexing
// Accessing a slice element
fruit := fruits[0]
fmt.Println("My fruit is", fruit)- This also follows the typical pattern of slices in other languages
- Also, the up-to-index is up-to-but-not-including
// slice[startIndex: upToIndex]
twoFruits := fruits[0:2]
fmt.Println("two_fruits:")
for _, fruit := range twoFruits {
fmt.Println("-", fruit)
}- We can also use inference for the beginning or end of slice
- If we skip the
startIndex, we grab everything before theupToIndex
- If we skip the
// Skipping the start_index
allFruitsButLast := fruits[:len(fruits)-1]
fmt.Println("allFruitsButLast:")
for _, fruit := range allFruitsButLast {
fmt.Println(fruit)
}- If we skip the
upToIndex, we grab everything after thestartIndex
// Skipping the upToIndex
allFruitsButFirst := fruits[1:]
fmt.Println("allFruitsButFirst:")
for _, fruit := range allFruitsButFirst {
fmt.Println("-", fruit)
}- If we skip both, we grab everything
// Skipping both startIndex and upToIndex
allFruits := fruits[:]
fmt.Println("allFruits:")
for _, fruit := range allFruits {
fmt.Println(fruit)
}- Because Slice is dynamic in length, we can add new elements to it
append()is a Pure Function- Appending does not modify the existing value
- Instead, it returns a new value with the modification added
- We have to set it back to the original variable
// Appending a new card
cards = append(cards, "6 of Spades")- We can iterate over both arrays or slices
for-loops are typically for iterating over a closed-set (finite set) of elements
for index, card := range cards {
fmt.Println(index, "--", card)
}range <slice>- The range of slice we want to iterate over
:=- With
for-loops, the iteration variables are re-declared at each iteration - So we have to use
:=instead of=
- With
index, card- Variables used within the
for-loop block - Every declared variable must be used
- If either
indexorcardis not going to be used in the loop body, replace with_
- Variables used within the
cards := []string{
newCard(),
newCard(),
newCard()
}
fmt.Println("Using for-loop with range:")
for index, card := range cards {
fmt.Println(index, "--", card)
}
fmt.Println("Using for-loop with range without using index:")
for _, card := range cards {
fmt.Println("--", card)
}- In Go, there is no
whilekeyword for doing iterations over infinite sets - Instead,
forcan also be used in awhile-like style for iterating over infinite sets of elements
cards := []string{
newCard(),
newCard(),
newCard()
}
fmt.Println("Using for-loop in a While-like style:")
i := 0
for i < len(cards) {
fmt.Println("--", cards[i])
i = i + 1
}rangeis typically used with Go'sfor-loop- However, we can always fallback to a C-style of
foras well
cards := []string{
newCard(),
newCard(),
newCard()
}
fmt.Println("Using for-loop in a C-like-for-loop style:")
for i := 0; i < len(cards); i++ {
fmt.Println("--", cards[i])
}- Go does not have a
do-whileloop either - Similar in other programming language, we can use
breakandcontinueto manipulate the flow of the loop - We can also get an infinite loop if we use
forwithout any conditions- Using this and
break, we can get ado-while-like loop usingfor
- Using this and
cards := []string{
newCard(),
newCard(),
newCard()
}
fmt.Println("Using for-loop in an infinite-loop with break (Do-While-like) style:")
j := 0
for {
// Do something at least once
fmt.Println("--", cards[j])
j += 1
// Then check the condition:
// Make sure it is reachable to avoid an infinite loop
if j >= len(cards) {
break
}
}- Go is not an Object-Oriented language
- It does not have any comprehension of Object and Class types
- Instead, we use Types and Receivers (Methods)
- Abstracted primitive types with additional functionalities
- We want to extend a base type and add some extra functionalities to it
- We could think of Type as a very simplified version of a Class
// Declaring a type:
// type <typeName> <equivalentType>
type deck []stringtype- Keyword to declare a new type
typeName- The name of the type
equivalentType- The primitive type that is equivalent to the declared type
- Because Go is not an OOP language, it does not have a Constructor for the types
- Instead, we used an Initializer function that acts as a type-instance generator function
// Initializes and returns a new deck of cards.
func newDeck() deck {
// A deck is just an abstraction of a slice of strings
cards := deck{}
// Suits: An array of strings
suits := [4]string{"Spade", "Diamond", "Heart", "Club"}
// Values: An array of strings
values := [13]string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}
// Build the combinations of Suits and Values
for _, suit := range suits {
for _, value := range values {
// Create the new card
newCard := fmt.Sprintf("%s of %s", value, suit)
// Append the new card to the deck
cards = append(cards, newCard)
}
}
// Return the new deck
return cards
}- Receiver functions are like Methods that we attach to Types
- Receiver functions are called like Methods on type instances
- When attaching to a type, we typically use the initial of the type as the
thisorselfkeywords within the function to refer to the Instance of the type- This is not a mandate but a generally-accepted convention
- Example:
deck->d
func (t <type>) <funcName>(<args>) <returnType> {
<body>
}<type>- The type that we are attaching the receiver function to
t- The Instance Variable
- With Go, we never use
thisorself - Instead, by convention, we typically use the initial of the type
- NOTE: When the instance variable is not being used in the function, we can remove it
// Declaring a Receiver Function: Attaching to a deck type
// Returns a tuple-like
func (d deck) deal(handSize int) (deck, deck) {
// Split the original deck into 2 using the handSize
hand := d[:handSize]
remDeck := d[handSize:]
// Return the "hand" and the "remaining deck"
return hand, remDeck
}- When using a receiver function, we generally use it like a Method on the Instance of the type
- NOTE: We can return multiple values from a Go function
- If we do not need one of the returned values, we can ignore by assigning it to
_
- If we do not need one of the returned values, we can ignore by assigning it to
cardDeck := newDeck()
hand, _ := cardDeck.deal()- Typically, types and their functionalities would be defined in a separate
.gofile- Using the same
packageto link them all inside the same project
- Using the same
- After declaring types and their functionalities Receiver functions, we can make use of them in our executable
main
// This is the main entry of the application
func main() {
// Declaring and Initializing variable deck type
// playingDeck is essentially a slice of strings
playingDeck := newDeck()
// Calling Type Receiver Function: Deal 5 cards
hand, playingDeck := playingDeck.deal(5)
// Calling Type Receiver Function: Print to screen
hand.print()
}- To deal with underlying Operating System files such as text files, we make use of the
osstandard package - Use
os.WriteFile()to write to a system file
import "os"
os.WriteFile(
filename string,
data []byte,
permissions FileMode
)filename- A path of the file to write
[]byte- Essentially a string of characters in binary format
- Every element inside a Byte Slice correspond to an ASCII character code
- We can use asciitable.com as Xwalk table for the
Deccolumn for better comprehension - Essentially, a Byte Slice is just another way to represent a string
permissions- Unix-like permission in Octal format
- Returns an
errortype by default
import (
"fmt"
"os"
)- Because
WriteFile()only takes Byte Slice, we need to convert the string to write to file into a Byte Slice[]byte
// Converting a string to a []byte
greetingStr := "Hello World!"
greetingBytes := []byte(greetingStr)
fmt.Println(greetingBytes)- Now, we can use
WriteFile()- We will just use a permission of
0o666(read/write) WriteToFile()returns an error type by default- We can use that with error handling later
- For now, we will just ignore it
- We will just use a permission of
// Writing text to file and ignoring returned error type
_ = os.WriteFile(
"datasave_hello_world.tmp",
greetingByte,
0o666
)import "os"
os.ReadFile(filename string)- To deal with underlying OS files such as text files, we make use of the
"os"standard package - Use
ReadFile()to read from a file filename- A path of the file to read from
- Returns a Byte-Slice
- Returns an
errortype by default
import (
"fmt"
"os"
)- Reading from the previously stored file
// Reading from a file
greetingByte, err := os.ReadFile(
"datasave_hello_world.sav"
)
// Converting Byte-Slice back to string
fmt.println(string(greetingByte))- Go does not have a
try-catchor similar-clauses - Instead, it returns errors-as-values as part of the function call, typically as a second value
- If there was any error in reading the file, returned
errwill be notnil
// Function call: Error would be returned as a second argument
greetingByte, err := os.ReadFile("datasave_hello_world.sav")
// Error Handling
if err != nil {
// We have an error: Handle to resolution
fmt.Println("Error:", err, "Creating a new file now.")
greetingStr := "Hello World!"
greetingByte = []byte(greetingStr)
}
// If here, then there was no error
fmt.Println(greetingByte)- Instead of handling the error, we could also stop the execution completely
// Function call: Error would be returned as a second argument
greetingByte, err := os.ReadFile("datasave_hello_world.sav")
// Error Handling
if err != nil {
// We have an error: Stop execution and panic
panic(err)
os.Exit(1) // 0 is success, anything else is fail
}
// If here, then there was no error
fmt.Println(greetingByte)- What makes sense
- What do you really care about with the feature?
- Go does not have a very strong unit test framework
- Very small set of functions for testing
- Not similar to using typical Testing Framework
- We write Go codes to test Go codes
- Create a new file ending in
_test.go- E.g. For testing
deck.go, usedeck_test.go
- E.g. For testing
- Define the test functions with
Test_prefix- These
Test_functions will be automatically called witht *testing.T - The name after the
Test_prefix does not necessarily need to match an existing function name tis the test-handler- If something is wrong, we use
tto notify with an error message t.Errorf()- Allows to return an error with string formatting
- If something is wrong, we use
- These
- To run all the tests in the package:
$ go test- Make sure to run this from the location where the test files are located
// In deck_test.go
func Test_newDeck(t *testing.T) {
// Deck Instance to test on
var d deck
// TEST CASE 1: A deck should be created with x number of cards
// ------------------------------------------------------------
// Create a new deck
d = newDeck()
// Set expectations:
// The deck should have 52 number of cards
expectedDeckLen := 52
actualDeckLen := len(d)
// Test expectations
if expectedDeckLen != actualDeckLen {
// If not, something is wrong --> Notify the test-handler t
t.Errorf("Test Case 1: Expected deck length of %v. Got %v", expectedDeckLen, actualDeckLen)
}
}- When testing with files, we have to make sure that we cleanup the files we test with
- Go does not automatically take care of cleaning up test files
// In deck_test.go
func Test_saveToFileAndNewDeckFromFile(t *testing.T) {
// Delete any file _decktesting.tmp from past tests if any
os.Remove("_decktesting.tmp")
// Create a new deck
d := newDeck()
// Save the deck to file
d.saveToFile("_decktesting.tmp")
// Attempt to load from disk
loadedDeck := newDeckFromFile("_decktesting.tmp")
// Set expectations:
// The length of the loaded deck from file should be the same as the original deck
expectedLoadedDeckLen := len(d)
actualLoadedDeckLen := len(loadedDeck)
// Test expectations
if expectedLoadedDeckLen != actualLoadedDeckLen {
// If not, something is wrong --> Notify the test-handler t
// Errorf() is a formatted string: We can use % for placeholders
t.Errorf("Expected loaded deck length of %v. Got %v", expectedLoadedDeckLen, actualLoadedDeckLen)
}
// Finally, clean up any temp test files
os.Remove("_decktesting.tmp")
}