Essentials
- open source programming language by google
- clarity like python, concurrency like c, type safety, etc
- basic code - run using
go run app.go1 2 3 4 5 6 7
package main import "fmt" func main() { fmt.Print("Hello World!") } - “package” - helps organize code
- “fmt” - package part of the go standard library
- “main package” - main entry point
- a “module” contains multiple “packages”
- the method above of running a go file works when we have go installed. we can however build the project to generate an executable as follows -
- initialize module -
go mod init example - build the module -
go build - run the executable -
./example
- initialize module -
- we can run the module directly without building using
go run .. notice how this is different from running the specific file - this works i.e. prints hello world because we named the package and the function
main - this main would not be required if for e.g. we were building a library with some utility functions, as it need not be executable
- go types come with a “null value”, which is the value stored in a variable if no other value is explicitly set. e.g. null value of int is 0, floa is 0.0, string is “” and so on
- when using float with int, i see the error
mismatched types int and float64. therefore, converting everything to float64 explicitly -1 2 3 4 5
var investmentAmount = 1000 var expectedReturnRate = 5.5 var years = 10 var futureValue = float64(investmentAmount) * math.Pow(1+(expectedReturnRate/100), float64(years))
- the other option apart from converting would be to specify the types upfront instead of relying on type inference -
1 2 3 4 5
var investmentAmount float64 = 1000 var expectedReturnRate = 5.5 var years float64 = 10 var futureValue = investmentAmount * math.Pow(1+(expectedReturnRate/100), years)
- some shorthands -
- we do not need the var keyword when type inference is enough for us
- in this case, the assignment happens using
:=instead of=1
expectedReturnRate := 5.5
- if multiple assignments are of the same type, we can club them in one line
1
var investmentAmount, years float64 = 1000, 10
- we can use
constinstead of var if the value is constant and never changes - example of printing formatted strings -
1
fmt.Printf("EBT (Earnings before tax): %.2f\n", ebt) - we can create formatted strings instead of printing them using
Sprintf - we can use multiline strings using backticks
- creating functions. go can return multiple values as well. notice how the multiple return types have to be wrapped around parentheses as well
1 2 3 4 5 6 7 8
func calculate(revenue float64, expenses float64, taxRate float64) (float64, float64, float64) { ebt := revenue - expenses profit := ebt * (1 - (taxRate / 100)) ratio := ebt / profit return ebt, profit, ratio } - now, we can call the function as follows. just like above, i think i did not have to declare the values using var as i am probably using type inference
1
ebt, profit, ratio := calculate(revenue, expenses, taxRate)
ifstatement example. we can add parentheses around it. we can chain it withelse if,elseas well1 2 3 4
if withdraw <= 0 || withdraw > accountBalance { fmt.Println("Error: Deposit should be >= 0") return }- for loop example
1 2 3
for i := 0; i < 5; i++ { fmt.Printf("i is %d\n", i) } - go only has a for loop, so an infinite loop can be written as follows -
1 2 3 4 5 6
for { // ... continue // ... break } - switch example - notice how we do not need a break after every case like other programming languages -
1 2 3 4 5 6 7 8 9 10
switch choice { case 1: // ... case 2: // ... default: // ... }
Error Handling
- “error handling” in go - it does not typically throw errors and crash applications. like other languages, we do not wrap around try catch etc. instead, we return errors since multiple values can be returned, and return reasonable defaults for the actual value. here, we first check for error, and then ourselves return a custom error as well
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
func example() (float64, error) { data, err := strconv.ParseFloat("hello", 64) if err != nil { fmt.Printf("data = %.2f\n", data) fmt.Printf("error = %s\n", err) return 1.3, errors.New("Failed to balance life") } return data, nil } // data = 0.00 // error = strconv.ParseFloat: parsing "hello": invalid syntax - there can be situations where we cannot continue the execution. we can in such cases call
panic("some reason")from anywhere in the application
Packages
- we can have two files - a.go and b.go in the same folder, both say with the same
package main - we can access functions of a.go inside b.go easily, without adding any additional imports etc
- we can also use different packages to separate concerns
- however, we cannot have multiple packages in the same folder, it gives an error
- note - folder name should be the same as package name i believe
- assume we create a folder called fileops, some go files in it with the statement
package fileops - now, we need to import them using
example/fileopsi.e. module name and then package name - only functions and variables that start with an uppercase are available in another package
- so, no explicit export is required
- fileops/fileops.go
1 2 3 4 5
package fileops func GetFloatFromFile(fileName string) (float64, error) { ... } - main.go -
1 2 3 4 5 6 7 8 9
package main import "example/fileops" const balanceFileName = "balance.txt" func main() { accountBalance, _ := fileops.GetFloatFromFile(balanceFileName) } - example command to instal a third party package -
go get github.com/Pallinder/go-randomdata - this adds this line in the go.mod file -
require github.com/Pallinder/go-randomdata v1.2.0 // indirect - another developer working on the to download all the dependencies listed in go.mod, use
go get
Pointers
- every value is stored somewhere in the computer’s memory
- “pointers” - we store addresses instead of values
- advantage of using pointers -
- avoid unnecessary copies
- directly mutate values
- “avoid unnecessary copies” - when we pass values to functions, go creates a copy of the value. once the function execution is over, garbage collection cleans up after some time. this can cause unnecessary copies when values are very large
- “directly mutate values” - since the variable in the function has the address, it can directly mutate the original value
- declaring pointers and assigning it values -
1 2 3 4 5 6 7 8 9 10 11
func Pointers() { age := 42 var agePointer *int agePointer = &age fmt.Printf("age pointer = %p\n", agePointer) } // age pointer = 0x617c0388b7f0 - recall how to declare shorthands for the same -
agePointer := &age - accessing the value behind the pointer -
1
fmt.Printf("age pointer value = %d\n", *agePointer) - the null value for pointers is
nil - an example of mutating values using pointers, using pointers in function parameters, etc -
1 2 3 4 5 6 7 8 9 10 11
func Pointers() { age := 42 adultYears(&age) fmt.Printf("adult years = %d\n", age) } func adultYears(agePointer *int) { *agePointer = *agePointer - 18 } - use case of pointers - “scan”
1 2 3
var choice int fmt.Print("Your choice: ") fmt.Scan(&choice)
Struts and Custom Types
- structs help us group related data together
- in go, we can scope types to functions, but we typically define it outside to use it across different functions
- we can start types with an uppercase to use it across different packages, or lowercase if we do not want to do that
- we can also nest structs inside other structs, e.g. createdAt below is of type time which itself is a struct internally that comes from the time package
1 2 3 4 5 6
type user struct { firstName string lastName string birthDate string createdAt time.Time } - next, we can create a variable of this struct type using “struct literals” as follows -
1 2 3 4 5 6
appUser := user { firstName: scannedFirstName, lastName: scannedLastName, birthDate: scannedBirthDate, createdAt: time.Now(), } - we can also omit the keys -
1 2 3 4 5 6
appUser := user { scannedFirstName, scannedLastName, scannedBirthDate, time.Now(), } - we do not need to specify values for all the keys, we can omit some / all of the attributes as well
1
appUser := user {} - we can pass structs around in functions like so -
1 2 3 4 5 6
print(user) // ... func print(appUser user) { fmt.Printf("name: %s %s\nbirth date: %s\n", appUser.firstName, appUser.lastName, appUser.birthDate) } - recall how this is basically a copy, and we can use pointers to avoid this -
1 2 3 4 5 6
print(&appUser) // ... func print(appUser *user) { fmt.Printf("name: %s %s\nbirth date: %s\n", appUser.firstName, appUser.lastName, appUser.birthDate) } - notice how we are still referencing using
appUser.firstNamewhile we should have technically used(*appUser).firstName. the first one is just a shortcut that go allows us to use - “methods” - functions on structs. we specify the struct to attach it to, and we give it a name to be able to reference it inside the function, e.g.
u user. it is like a special kind of argument / parameter1 2 3 4 5
func (u user) print() { fmt.Printf("name: %s %s\nbirth date: %s\n", u.firstName, u.lastName, u.birthDate) } appUser.print() - now, assume we want to create a function on the struct so that can mutate its fields. notice how we need to use
(u *user)and not(u user)for it to work, otherwise the copy is mutated and we do not see any change in the original struct. also, notice that when calling the function, we do not need to use(&appUser).clearName(), maybe this again is a short hand that go has given us1 2 3 4
func (u *user) clearName() { u.firstName = "" u.lastName = "" } - “constructors” - a utility function that helps create the struct. convention in go - name it New. also, usually models would be present in their own package, so we can invoke it using for e.g.
errors.Newand so on. finally, notice how we also add error handling to this1 2 3 4 5 6 7 8 9 10 11 12 13 14
func New(title string, content string) (Note, error) { if title == "" { return Note{}, errors.New("title cannot be empty") } note := Note{ title: title, content: content, createdAt: time.Now(), } return note, nil } - again as usual, we can return a pointer to this instead of a copy. compare it with the version above, and notice how the return type changes as well -
1 2 3 4 5 6 7 8
func newUser(firstName, lastName, birthDate string) *user { return &user{ firstName: firstName, lastName: lastName, birthDate: birthDate, createdAt: time.Now(), } } - because of how the shorthand etc we described earlier works, we need not dereference the properties or methods of this struct first
- assume we want to add validation to the user creation logic, example first name must not be null. we can use techniques discussed here
- the capital casing we discussed for exposing variables outside a package applies to the fields defined inside a struct and also the methods defined for them
Embeddings
- since go does not have the concept of inheritance like in java, we need to use “embedding”
- option 1 - we specify the field name explicitly
1 2 3 4 5
type admin struct { email string password string user user } - this is how we access the nested members of user now -
1 2 3
func (a *admin) Authenticate() { fmt.Printf("logging in %s\n", a.user.firstName) } - option 2 - “anonymous”
1 2 3 4 5
type admin struct { email string password string user } - notice how we can access the members of user directly from the admin as well now -
1 2 3
func (a *admin) Authenticate() { fmt.Printf("logging in %s\n", a.firstName) } - note that when we made it anonymous, we could still continue using
a.user.firstName
Custom Types
- we can create aliases for existing types as follows -
1
struct customStr string
- example use case - if we try to add a method to string as follows -
1 2 3
func (text string) log() { fmt.Printf("[%s] %s\n", time.Now(), text) } - we see this exception -
cannot define new methods on non-local type stringcompilerInvalidRecv - however, we can define this on the custom type we created -
1 2 3 4 5 6 7
func (text customStr) log() { fmt.Printf("[%s] %s\n", time.Now(), text) } // ... var text customStr = "lorem ipsum" text.log()
Struct Tags
- go allows us to add metadata to the fields of a struct
- these can then be used by the various packages appropriately
- e.g. assume we have a struct called note -
1 2 3 4 5
type Note struct { Title string Content string CreatedAt time.Time } - and we are writing it to a json file as follows -
1 2 3
// ... serialized, _ := json.Marshal(note) os.WriteFile(fileName, serialized, 0644)
- by default, the keys in the json file would be the same as the struct field names
1
{"Title":"Learn Go","Content":"Go Programming","CreatedAt":"2026-09-14T08:36:29.35401+05:30"} - to change this behavior, we can use struct tags as follows -
1 2 3 4 5
type Note struct { Title string `json:"title"` Content string `json:"content"` CreatedAt time.Time `json:"created_at"` } - the new json that gets generated looks like this -
1
{"title":"Learn Go","content":"Go Programming","created_at":"2026-09-14T08:45:59.851003+05:30"}
Interfaces
- like a “contract” - any struct that implements the interface should implement the methods inside the interface
1 2 3
type saver interface { Save() error } - an interface can have multiple methods
- now, we can have a function that uses this interface -
1 2 3 4 5 6 7 8 9 10
func save(data saver) { err := data.Save() if err != nil { fmt.Println("saving failed") } fmt.Println("saving successful") } - my understanding - an explicit
implements interfaceis not needed in go. as long as we call the save function with structs that have a method that matches the signature of the method defined in the interface, we are good to go. e.g. Todo already has a method like this -func (todo Todo) Save() error {. so, we need not specify explicitly that it implements some interface - embeddings is supported in interfaces as well
Any
anymeans any type- use case - if we want to for instance create a wrapper around logging. there are two options for achieving it -
anyandinterface{}1 2
func print(data any) {} func print(data interface{}) {} - now, what if we want to execute logic differently based on the type of the value?
- we can
switch. this is called “type switches” -1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
func printSomething(value any) { switch value.(type) { case int: fmt.Printf("Integer = %d\n", value) case float64: fmt.Printf("Float64 = %.2f\n", value) } } func main() { printSomething(1) // Integer = 1 printSomething(2.4566) // Float64 = 2.46 printSomething("hello") // no output } - when we try running the code below, i see a compile time error -
invalid operation: value + 1 (mismatched types any and int)1 2 3
func printSomething(value any) { fmt.Print("incremented value is = %d\n", value + 1) } - solution - we can type it explicitly as follows. when i hover over typedValue, it says that it is an int. the second return value tells whether the value is of the type we passed between the parentheses
1 2 3 4 5 6 7 8
func printSomething(value any) { typedValue, isTypedValue := value.(int) if isTypedValue { fmt.Printf("incremented value is = %d\n", typedValue + 1) } }
Generics
Tis a convention, we can use anything and we can also have multiple generic types- we can specify a list of pre defined types that T can be
1 2 3
func add[T int | float64 | string](a T, b T) T { return a + b }
Arrays and Slices
- “arrays” - hold different values for the same kind of thing
1 2 3 4
prices := [4]float64{1.1, 2.2, 3.3, 4.4} fmt.Println(prices) // [1.1 2.2 3.3 4.4] - accessing specific positions -
1 2
prices[1] = 2.222 fmt.Println(prices[1])
- we can just declare a variable like this -
var prices [4]float64 - we can initialize a list with empty (meaning maybe nil?) values like so -
prices := [4]float64{} - “slicing” - includes first argument, excluding last argument
1 2 3 4
prices := [4]float64{1.1, 2.2, 3.3, 4.4} fmt.Println(prices[1:3]) // [2.2 3.3] fmt.Println(prices[:3]) // [1.1 2.2 3.3] fmt.Println(prices[1:]) // [2.2 3.3 4.4] - picking negative indices / larger than length of array indices is not supported here unlike other programming languages
- slices is like creating a window on the existing array. it is like a pointer. so, wen we modify an element of the slice, we also modify the element of the array
1 2 3 4 5 6
prices := [4]float64{1.1, 2.2, 3.3, 4.4} featuredPrices := prices[1:3] featuredPrices[0] = 9.9 fmt.Println(prices) // [1.1 9.9 3.3 4.4] - internals about slices - even if we set it with a certain window, we can still select more items to the right but not more to the left. so, we can reslice arrays to go more towards the right
1 2 3 4 5 6 7 8
prices := [4]float64{1.1, 2.2, 3.3, 4.4} featuredPrices := prices[1:2] fmt.Println(featuredPrices) // [2.2] fmt.Println(featuredPrices[:]) // [2.2] fmt.Println(featuredPrices[:3]) // [2.2 3.3 4.4] fmt.Println(featuredPrices[:4]) // panic: runtime error: slice bounds out of range [:4] with capacity 3 - issue till now - we were hardcoding the size using
[4]float64. however, what if we want a dynamic array i.e. we do not know ahead of time how many elements our array would have. we do not specify the size inside the square braces. notice how when we try to directly assign a value to the index we get a runtime error1 2 3 4
prices := []int{1, 2} fmt.Println(prices) // [1 2] prices[2] = 4 // panic: runtime error: index out of range [2] with length 2 - actually, when we do not specify the size upfront, what we get is a “slice” and not an “array”, or i believe it is basically a slice with an array managed by this slice bts
- now, see how using append works. notice how the original slice stays unchanged, and a new slice is returned instead
1 2 3 4 5 6
prices := []int{1, 2} fmt.Println(prices) // [1 2] updatedPrices := append(prices, 4) fmt.Println(updatedPrices) // [1 2 4] fmt.Println(prices) // [1 2] - updating an element only updates the new slice, and not the original slice
1 2 3
updatedPrices[0] = 7 fmt.Println(updatedPrices) // [7 2 4] fmt.Println(prices) // [1 2]
- while we can dynamically append to slices, we can also specify a capacity upfront using
make. here, we are asking go to first create two empty slots, and then also reserve space for a total of 5 elements. my understanding - this way, while we are add elements till the total size reaches 5, go need not recreate the entire arrays from scratch1
numbers := make([]int, 2, 5)
- notice how appending appends to the slice and does not touch the empty slots that were created
1 2
numbers = append(numbers, 3) fmt.Println(numbers) // [0 0 3]
- probably once we have called append three times, and then call it for a fourth time, go would have to recreate the array
- declaring an array of structs -
1 2 3 4
products := []Product{ Product{"shoes", 90.4}, Product{"bag", 40.0}, } - using a shorthand - we can skip specifying the struct name for each element, since that is inferred by the type specified for the array earlier
1 2 3 4
products := []Product{ {"shoes", 90.4}, {"bag", 40.0}, } - appending multiple numbers by specifying them one at a time - ``` numbers := []int{1, 2, 3} numbers = append(numbers, 4, 5)
- appending a slice to another slice -
1 2
numbers := []int{1, 2, 3} numbersBig := []int{6, 7, 8}
Maps
- specify the type of key and value / use keys to help identify the kind of values
1 2 3 4 5 6
websites := map[string]string{ "google": "https://google.com", "aws": "https://aws.com", } fmt.Println(websites) // map[aws:https://aws.com google:https://google.com] - accessing values that are present vs not present (no error, just empty)
1 2
fmt.Println(websites["aws"]) // https://aws.com fmt.Println(websites["amazon"]) // <empty>
- adding key / values to the map
1 2
websites["linkedin"] = "https://linkedin.com" fmt.Println(websites) // map[aws:https://aws.com google:https://google.com linkedin:https://linkedin.com]
- above syntax also works when we want to overwrite values for a key
- removing keys -
1 2
delete(websites, "aws") fmt.Println(websites) // map[google:https://google.com linkedin:https://linkedin.com]
- structs vs maps - fields in structs are predefined unlike in maps
- similar to slices, we can use make to avoid reallocating memory for maps as well. however, there is no concept of empty slots in maps, so we just specify one argument
1
websites := make(map[string]string, 4)
For Loops
1
2
3
4
5
6
7
8
9
10
numbers := []float64{1.1, 2.2, 3.3, 4.4}
for index, value := range numbers {
fmt.Printf("numbers[%d] = %.2f\n", index, value)
}
// numbers[0] = 1.10
// numbers[1] = 2.20
// numbers[2] = 3.30
// numbers[3] = 4.40
1
2
3
4
5
6
7
8
9
courseRatings := map[string]float64{"dsa": 5.0, "lld": 2.5, "hld": 4.5}
for key, value := range courseRatings {
fmt.Printf("ratings[%s] = %.2f\n", key, value)
}
// ratings[dsa] = 5.00
// ratings[lld] = 2.50
// ratings[hld] = 4.50