The Complete Developers Guide (Golang)
Overview
My notes of the "Go: The Complete Developers Guide (Golang)" course on Udemy to learn Golang.
Notes
From Simple Start Chapter, questions:
- how do we run the code in our project?
- what does
package main
mean? - what does
import "fmt"
mean? - what's that
func
thing? - how is the
main.go
file organized
Simple Start
run code
go run main.go
- go run = compile & run
- go build = just compile
- go install, go get = installs pkg, download raw source code
package main
package == project == workspace
all files belonging to that "package" must declare so at the top with package main
.
2 types of package = executable (runnable) and reusable (a 'helper', good for logic, libs)
package main
is SACRED! only use it if you want to create a "runnable" file. a "non-main" package, compiles but does NOT run anything.
any executable package main must have a func main
in it.
rename package to anything else and do go build
does NOT build any executable file.
import fmt
gives us access to fmt
library/package.
func
fun main() {
- short for function.
main.go organized
- package declaration
- imports packages
- func main
Deeper into Go
variable declaration
this var Name string
and name := "Ron"
are EQUIVALENT in variable initialization and (initial) assignment the :=
will determine the type for you.
:=
use only when declaring new vars, dont use for value "assignment".
functions
when writing up non-main functions, this is the format func funcName() typeToReturn
e.g. func newCard() string {
you'll get an error message if return type and return value type are mismatched.
slice and for Loops
Array = fixed list Slice = array that can grow/shrink
the TYPES in a slice must be of the same type.
declare a slice: cards := []string{"elements","inside","slice"}
. the []
and string
declares its a "slice" of type "string", and then {}
to hold the elements of the slice.
adding elements to a slice: cards = append(cards, "newElement")
<-- important to note here that the original "cards" slice is not modified with this new element, instead a NEW slice with the appended element is returned.
iterate over a slice, print every element: for i, card := range cards { fmt.Println(i, card)}
i = index, range = iterate over every element in slice.
oo vs go
Go is NOT OO language.
Think - go types (string, int, float, array, map), then extend type deck []string
, then functions
main.go
- our main program that manipulates the deckdeck.go
- describe deck, how it works (spec?)deck_test.go
- automatically test our deck
card deck go program
For our card app, what functions do we want?
- newDeck - create return list of playing cards (array of strings)
- shuffle
- deal
- saveToFile
- newDeckFromFile
add the custom types and receivers as necessary.
custom types and receiver functions
we can do something like this to sort of simulate "extends" from OO approach
// in a separate .go file
// declare new type
type deck []string
// create custom method for new type
func (d deck) print() {
for i, card := range d {
fmt.Println(i, card)
}
}
We declared a custom type with type deck []string
and can now use deck
"type" anywhere in our main package code.
Printing the new type out now made easier with our print()
function.
(d deck)
is the bit that makes this func a "receiver function".
A receiver sets up methods on variables we create e.g. we create var card
of type deck
, and now the print func can be setup on ANY var of type deck
e.g. card.print()
Think of d
arg as this
or self
- in go, never use "this" or "self", and also always refer to the THING that you're setting method up on, by convention if your type is deck
your arg name will be d
, but you can do whatever as long as the references match i.e. (d deck)
and range d
match.
If you have a var that you don't care to use, and want to avoid the "you declared but haven't used this var" error message, replace them with _
underscore.
e.g.
func (d deck) print() {
for i, card := range d {
fmt.Println(i, card)
}
}
slice range syntax
Dealing out a "hand", a slice of the 52 available cards.
slices indexed from 0
, e.g. fruits[0]
for a subset, or range of the slice: fruits[0:2]
will give you fruits of index 0,1 cos the first index 0
is "inclusive", but the 2nd index 2
is "up to, but NOT including" 2
.
shorthand for this can be [:2]
is the same as [0:2]
, another example is [2:]
is the same as [2:n]
i.e. "from index 2 until the end of the index"
when returning 2 x type deck values from the function, you assign them to two variables like this: hand, remainingDeck := deal(cards, 5)
<-- because deal(cards, 5)
gets (deck, deck)
from the receiver function.
deck to string
trying to save to file, import io/ioutil
we need to transfer our strings to a []byte
"byte slice".
a way to do a "type conversion":
greeting = "Hi there!"
fmt.Println([]byte(greeting)) // changes the greeting string into a byte slice.
our process = start with deck
--> []string
--> string
--> []byte
the string
is ALL card strings smashed together to then convert to a []byte
.
join slice of Strings
lookup and use strings
library Join
function.
save data to HDD
lookup and use io/iotuil
library WriteFile
function for writing a []byte
byte slice to disk under "filename".
read data from HDD
use io/ioutil
functions ReadFile
to do the reverse and open a file from HDD, then use strings
function Split
to reverse what you did with Join
, now you have a []string
string slice AKA a deck type
.
error handling
the err
convention
...
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
make sure to import os
, and any non-zero number inside Exit()
to signal a bad exit.
Summary
- create
type deck
- tie methods to the new
type deck
using "receiver functions" - receivers = can tack onto the type deck e.g.
cards.print()
- no receiver for methods like
.deal()
because the "root" instance of the deck (??)
Assignment 1
write a go program that iterates over a range of numbers and evaluates even and odd and prints a statement to each.
my solution:
package main
import "fmt"
func main() {
numbers := []int{1,2,3,4,5,6,7,8,9,10}
for _, num := range numbers {
if num % 2 == 0 {
fmt.Println(num, " is even.")
} else {
fmt.Println(num, " is odd.")
}
}
}
}
Data Structures
structs
aka data structures are a "collection of properties that are related together".
we first define a "structure" e.g. of a person, then we create instances of people.
e.g.
type person struct {
firstName string
lastName string
}
few different ways to construct struct
alex := person{firstName: "Alex", lastName: "Anderson"}
var alex person
then init withalex.firstName = "Alex"
andalex.lastName = "Anderson"
var alex person
thenfmt.Printf("%+v", alex)
with%+v
printing out field names and values.
updating struct values
if you don't init the vars with values, if string they get assigned ""
empty string value.
if bool or int, they get 0
.
so if you fmt.Println(alex)
with no value assigned, you get { }
printed out i.e. "empty strings"
using fmt.Printf()
and syntax fmt.Printf("%+v", alex)
as the 3rd way to show structs.
final form :
package main
import "fmt"
type person struct {
firstName string
lastName string
}
func main() {
//alex := person{firstName: "Alex", lastName: "Anderson"} // version 1
var alex person // version 2
alex.firstName = "Alex"
alex.lastName = "Anderson"
fmt.Println(alex)
fmt.Printf("%+v", alex)
}
embedded structs
embed on struct inside another struct.
type person
type contactInfo
you can use custom types inside our structs.
package main
import "fmt"
type contactInfo struct {
email string
zipCode int
}
type person struct {
firstName string
lastName string
contact contactInfo
}
func main() {
jim := person{
firstName: "Jim",
lastName: "Party",
contact: contactInfo{
email: "[email protected]",
zipCode: 94000,
},
}
fmt.Printf("%+v", jim)
}
another method of declaring our contactInfo
is to remove the explicit variable name contact
and just do the following:
type person struct {
firstName string
lastName string
contactInfo
}
and then
function main() {
jim := person{
firstName: "Jim",
lastName: "Party",
contactInfo: contactInfo{
email: "[email protected]",
zipCode: 94000,
},
}
}
structs receiver functions
recap receiver functions
func (letter type) funcName(varName varType) { }
// e.g. func (p person) updateName(newFirstName string)
pointers
the reason the updateName function did not work.
Go is a "pass by values" means you work on a COPY of the value of the object/thing, not the original object/thing.
e.g. we contruct the person{firstName: "Jim"...}
person, but when we pass the value to our function for a name update func (p person) updateName(newFirstName string)
, it creates a copy p
of the person Jim:
RAM | |
---|---|
Address | Value |
0000 | |
0001 | person{firstName: "Jim"...} |
0002 | |
0003 | person{firstName: "Jim"...} |
original Jim object at 0001
, "newFirstName" Jim object (i.e. p
) at 0003
.
to summarize, when updateName
is called, go makes a COPY of that struct, and then makes the COPY (p
) available to the function for processing.
why does go do this?
Pointer Operations
&
creates a memory ADDRESS pointer
*
creates a memory VALUE pointer
&jim
= "give me the memory address of the value this variable is pointing to"
so jimPointer := &jim
is now pointing to the MEMORY ADDRESS of whatever &jim
evaluated to.
*pointer
= "give me the value this memory address is pointing to"
so func(pointerToPerson *person)
says give me the VALUE at the pointer where this *pointer
memory address is pointing.
understand difference between pointer as a TYPE and as a POINTER:
- pointer in front of a type e.g.
func (pointerToPerson *person)
means this receiver can only accept a type of a "pointer to a person" i.e. something likejimPointer
- pointer in front of a pointer e.g.
(*pointerToPerson).firstName
use *address
to turn an address into value.
use &value
to turn a value into address.
Shortcuts
with this code:
jimPointer := &jim
jimPointer.updateName("jimmy")
jim.print()
you can remove jimPointer := &jim
, and the func (pointerToPerson *person)
will automatically take your "type person" and turn it into a "pointer person (*person
)" for you.
Pointer Gotchas
struct vs slice
with struct, you need pointers to update the actual values.
with stlices - values seem to update directly as you act upon the values.
reference vs value types
Arrays vs Slices - arrays are primitive, can't be resized. we use slices more.
A slice gives us BOTH a slice data structure (ptr, capacity, length) and array data structure (ptr points to the elements in our slice "array").
in memory, our SLICE is registered as the slice data structure at one memory address, and the actual array with our elements in it at another memory address.
when the "pass by value" happens when we pass our slice to a function, the COPY go makes of the slice is just the SLICE data structure, which goes into another address- BUT, this COPY still points to the SAME ARRAY values the original slice data structure points to.
so when we modify the SLICE, we are not modifying a COPY of the array values for the slice, there is no COPY, we are modifying the original array values from the slice.
slice is a "reference" type - it a data structure that refers to ANOTHER data structure in RAM.
Reference types:
- maps
- channels
- pointers
- functions.
value types:
- int
- float
- string
- bool
- structs.
Maps
# MAP
key --> value
key --> value
key --> value
comparably: map(go) = hash(ruby) = object(javascript) = dict(python)
Maps = statically typed i.e. all keys = same type, all values = same type.
package main
import "fmt"
func main() {
// 2 x ways of init a "null" map:
// 1. var colors map[string]string
// 2. colors := make(map[string]string)
// example of using type=int
// colors := make(map[int]string)
// colors[10] = "#ffffff"
// delete(colors, 10)
// fmt.Println(color)
colors := map[string]string{
"red": "#ff0000",
"green": "#00ff00",
"white": "#ffffff",
}
printMap(colors)
}
func printMap(c map[string]string) {
// iterate over a map
for color, hex := range c {
fmt.Println("Hex code for", color, "is", hex)
}
}
Always use []
braces with maps e.g. color["white"] = "#ffffff
.
Let's break down the func, loop
// c = argument
// map[string]string = type of the argument
// color, hex = the variables that will receive the `key, value` during the loop
// range c = "iterate over the range 'c'"
func printMap(c map[string]string) {
// iterate over a map
for color, hex := range c {
fmt.Println("Hex code for", color, "is", hex)
}
}
Maps vs Structs
Maps = statically typed = all keys and values must be same type. Struct = values can be different type.
Maps = keys are indexed 0...12, can iterate over. Struct = keys dont support index, can't interate over.
Maps = original data structure of map directly modified Structs = original data structure is copied, and edited, original is unmodified.
- when representing a set of collection of closely related properties
- when your scenario doesn't know all keys, types at compile time (otherwise, look at structs if you know)
vast majority of golang = use structs.
Interfaces
Purpose of interfaces
reuse, generic code or code that has common factors - write an interface instead of duplicate code.
package main
import "fmt"
type bot interface {
getGreeting() string
}
type englishBot struct{}
type spanishBot struct{}
func main() {
eb := englishBot{}
sb := spanishBot{}
printGreeting(eb)
printGreeting(sb)
}
func printGreeting(b bot) {
fmt.Println(b.getGreeting())
}
func (eb englishBot) getGreeting() string {
// very custom logic for generating english greeting
return "Hi there!"
}
func (sb spanishBot) getGreeting() string {
return "Hola!"
}
Create a new type bot
that says, if you have a getGreeting()
function, with return type string
, you can be type cast as bot
and use what bot
can use i.e. printGreeting()
.
Rules of Interfaces
type bot interface {
// input args = string, int
// returns = string, error
getGreeting(string, int) (string, error)
// input args = none
// returns = float
getBotVersion() float
// input args = user
// returns = string
respondToUser(user) string
}
Concrete Types = map, struct, int, englishBot, string -- can create values directly e.g. int a := 12
Interface Type = bot -- can't create value directly i.e. bot can't "equal 5"
Some key interface points:
- Interfaces are NOT generic types.
- Interfaces are implicit i.e. when you declare a
type bot interface
and then do atype englishBot struct{}
, go will implicitly treatenglishBot
as typebot interface
. - Interfaces are a contract to help us manages types.
- Interfaces only help reuse code, doesn't check logic or test things for you, garbage in = garbage out.
The HTTP Package
A program that:
- HTTP request --> google.com
- print response to terminal
Get used to following the docs:
e.g. we are looking for where the "Body" of the HTTP request we made is, so we follow the code
https://pkg.go.dev/net/http#Get --> func Get(url string) (resp *Response, err error)
--> https://pkg.go.dev/net/http#Response --> Body io.ReadCloser
--> https://pkg.go.dev/io#ReadCloser --> type ReadCloser interface
--> https://pkg.go.dev/io#Reader
my thought: interfaces re-packages the data into a form that is generic enough, to be consumed by general code e.g. the Reader interface accepts all sources of Input and then "output" it to a byte[]
slice, and that's a generic enough type to be handled by anything on the other side of the interface.
type Reader interface {
Read(p []byte) (n int, err error)
}
this interface, has func Read
, accepts input p
of type []byte
, and returns n int
and err error
.
Writer Interface
where the Reader is like: source of input
--> Reader
--> []byte
Writer is like: []byte
--> Writer
--> source of output
What, in the "standard library", implements the Writer interface? io.Copy()
So this:
bs := make([]byte, 99999) //fixed byte size 99999 empty elements
resp.Body.Read(bs)
fmt.Println(string(bs))
does the same thing as this:
io.Copy(os.Stdout, resp.Body)
where resp
is from resp, err := http.Get("http://google.com")
Copy interface, takes two types: func Copy(dst Writer, src Reader) (written int64, err error)
a Writer
or "something that implements the Writer interface" and a Reader
or "something that implements the Reader interface" e.g. io.Copy(os.Stdout, resp.Body)
i.e. io.Copy
implmements the Writer
interface, and resp.Body
implements the Reader interface.