This repo documents my personal notes on learning Go language. I picked up the language by following tutorials from Pak Eko. Since the tutorial is in Indonesian, I've created this English notes for those who want to learn from his teachings but may not speak Indonesian.
- Developed by Google using the C programming language.
- Released to the public as open source in 2009.
- Gained popularity, particularly after being utilized to build Docker in 2011.
- Presently, numerous cutting-edge technologies are developed using Go, surpassing the usage C. Notable projects include Kubernetes, Promotheus, CockroachDB, etc.
- Currently gaining popularity as the preffered language for constructing backend APIa in microservices.
- Go is straightforward, requiring minimal time to grasp.
- Go supports effective concurrency programming, which aligns well with the current era of multicore processors.
- Go features built-in garbage collection, eliminating the need for manual memory management as required in languages like C.
- Garbage collection refers to the automatic management of computer memory. In languages like Go, the garbage collector is a mechanism that automatically identifies and frees up memory that is no longer in use or needed by the program. This process helps developers avoid the manual handling of memory allocation and deallocation, reducing the risk of memory leaks and making the development process more efficient. Essentially, garbage collection in Go takes care of cleaning up unsused memory, allowing developers to focus more on writing code and less on memory management details.
- It is one of the trending programming languages in contemporary times.
Our working file, main.go -> is compiled by Go Complier -> producing the binary file main
Go supports compilation for various operating systems such as MacOS, Linux, and Windows.
- A Project in Go is typically referred to as a module.
- To create a module, use the following command in the folder where we want to build it:
go mod init <module-name> - Typically. the folder where we want to build the module shares the same name with the module itself. For example, if our folder is named "learning-golang", we can create the module by executing the following command:
go mod init learning-golang
- Go is quite similar to C/C++ programming languages in that it requires a main function.
- The main function is a function that gets executed when the program is running.
- To define a function, the keyword used is func.
- The main function should be placed in a main package.
- In Go, semicolons are not mandatory; we can choose whether or not to use them at the end of our code lines.
- Go is case sensitive.
Example:
// helloworld.go file
package main
func main() {
// ...
}- To write and output a sentence to terminal, we first need to import the fmt module.
- This is pretty similar to Java.
- I think it's like console.log in JavaScript
Example:
// helloworld.go file
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}- Run the following command:
go build
- With that command, this project will attempt to find main function in any files and compile it into a program that matches our current operating system. The output file will have the same name as the project. For example, if we named our project "learning-golang", the the output file will also be named "learning-golang". Subsequently, the output file can be run using terminal. On MacOs or Linux:
On Windows:
./learning-golang./learning-golang.exe
It is used when developers are in the app development process.
go run <file-name>
- In Go, each function within a module/project must have a unique name. This means that we cannot create functions with the same name.
- Therefore, if we create a new file, for example, sample.go, and then attempt to create a function with the same name, main, we won't be able to build the module because the main function duplicates the one already existing in the main function of helloworld.go
There are two data type number:
-
Integer (1)
Data Type Minimum Value Maximum Value int8 -128 127 int16 -32768 32767 int32 -2147483648 2147483647 int64 -9223372036854775808 9223372036854775807 -
Integer (2)
Data Type Minimum Value Maximum Value uint8 0 255 uint16 0 65535 uint32 0 4294967295 uint64 0 18446744073709551615 Note: uint -> unsigned integer
| Data Type | Minimum Value | Maximum Value |
|---|---|---|
| float32 | 1.18x10-38 | 3.41038 |
| float64 | 2.23x10-308 | 1.80x10308 |
| complex64 | complex numbers with float32 real and imaginary parts | |
| complex128 | complex numbers with float64 real and imaginary parts | |
| Data Type | Alias For |
|---|---|
| byte | uint8 |
| rune | int32 |
| int | Minimal int32 |
| uint | Minimal uint32 |
- Boolean data type is a data type that has two values: true or false.
- In Go, the Boolean data type is represented by the keyword bool.
-
A string is a set of characters.
-
The total number of characters in a string can range from 0 to infinity.
-
In Go, the string data type is represented by the keyword string.
-
The value of string in Go always enclosed in quotation marks ("").
-
There are some built-in functions for strings, such as:
Function Purpose len("string") Calculate the length of the string "string"[number] Take a character at a specific position based on the index (starting from 0) -
Example:
package main import "fmt" func main() { fmt.Println("Learning Golang") fmt.Println("Go is Easy!") fmt.Println(len("Go is Easy!")) // 11 fmt.Println("Learning Golang"[0]) // 76 (in byte type), we can convert it to string }
-
A variable is a storage to save data.
-
Variables are used to make data accessible from various parts of the program.
-
In Go, a variable can only store data of the same data type. If we want to store data of different types, we need to create multiple variables.
-
To create a variable, we use the keyword var followed by the variable name and the its data type.
-
It is mandatory to state the data type when creating a variable, unless we directly initialize data to the variable.
-
The keyword "var" is not mandatory if we directly initialize data using := when creating a variable. If we want to change the value of the variable, we need to use the asignment operator = and not :=.
-
We can make multiple variables in one go, the code will be cleaner.
-
All variables created in Go must be used; otherwise, when we run the code, Go will generate an error.
-
Example:
func main() { // Printing to terminal fmt.Println("Learning Golang") // "Learning Golang" fmt.Println("Go is Easy!") // "Go is Easy!" fmt.Println(len("Go is Easy!")) // 11 fmt.Println("Learning Golang"[0]) // 76 (in byte type), we can convert it to string // Creating a variable var name string name = "Eko" fmt.Println(name) name = "Eko Kurniawan" fmt.Println(name) // Creating a variable without stating its data type var grade = 100 fmt.Println(grade) // Omitting "var" keyword country := "Indonesia" fmt.Println(country) // Re-assigning value to the existing variable country = "England" fmt.Println(country) // Creating multiple variables var ( firstName = "Pierle" lastName = "Dev" ) fmt.Println(firstName) // "Pierle" fmt.Println(lastName) // "Dev" }
- A constant is a variable whose value cannot be changed after the it is assigned for the first time.
- The keyword used is const, not var.
- Data must be directly assigned when the constant is created.
- We can also create multiple constants in one go.
- Example:
package main func main() { const day = 7 // day = 8 // error re-assigning value // Creating constants in one go const ( firstName string = "Pierle" lastName = "Dev" ) }
package main
import "fmt"
func main() {
var value32 int32 = 32768
var value64 int64 = int64(value32)
// Be careful when trying to convert to a data type that can hold data with a smaller size; it can cause a problem
var value16 int16 = int16(value32)
fmt.Println(value32) // 32768
fmt.Println(value64) // 32768
fmt.Println(value16) // -32768 -> The maximum value of int16 is 32767. When attempting to convert 32768 to the int16 data type, it is set to the lowest number that can be stored in int16, i.e., -32768.
var num32 int32 = 32770
var num64 int64 = int64(num32)
var num16 int16 = int16(num32)
fmt.Println(num32) // 32770
fmt.Println(num64) // 32770
fmt.Println(num16) // -32766
var activity = "Learning Golang"
var l = activity[0] // in byte data type
var lString = string(l) // L
fmt.Println(activity) // "Learning Golang"
fmt.Println(l) // 76
fmt.Println(lString) // "L"
}- Type declarations are a capability to create a new data type from an existing data type.
- Type declarations are typically used to create an alias for an existing data type, with the aim of making it easier to understand.
- Example:
package main import "fmt" func main() { type NoKTP string var myKTP NoKTP = "1234567" var rikaId string = "3625281" // converting rikaKTP to NoKTP data type var rikaKTP NoKTP = NoKTP(rikaId) fmt.Println(myKTP) // 1234567 fmt.Println(rikaKTP) // 3625281 }
- Addition (+), subtraction (-), multiplication (*), division (/), modulo (%).
- Example:
package main import "fmt" func main() { var a = 10 var b = 10 var c = a + b fmt.Println(c) // 20 }
| Maths Operations | Augmented Assignments |
|---|---|
| a = a + 10 | += 10 |
| a = a - 10 | a -= 10 |
| a = a * 10 | a *= 10 |
| a = a / 10 | a /= 10 |
| a = a % 10 | a %= 10 |
Example:
import "fmt"
func main() {
var i = 10
i += 10
fmt.Println(i) // 20
}| Operator | Notes |
|---|---|
| ++ | a = a + 1 |
| -- | a = a - 1 |
| - | Negative |
| + | Positive |
| ! | Inverse boolean e.g. !true means false |
Example:
package main
import "fmt"
func main() {
var i = 1
i++ // i + 1
fmt.Println(i) // 2
i++ // i + 1
fmt.Println(i) // 3
i-- // i - 1
fmt.Println(i) // 2
}- Comparison operation is used for comparing two pieces of data.
- It is an operation that yields a boolean value (true or false).
- Comparison operators
Operator Notes > Greater than < Less than >= Greater than or equal <= Less than or equal == Equal to != Not equal to - Example:
package main import "fmt" func main() { var word1 = "Learning" var word2 = "Golang" var result bool = word1 == word2 fmt.Println(result) // false var favNumber = 7 var hisFavNumber = 7 fmt.Println(favNumber == hisFavNumber) // true }
- The output is either true or false
Operator Notes && AND || OR ! NOT Value 1 Operator Value 2 Result true && true true true && false false false && true false false && false false true || true true true || false true false || true true false || false false Operator Value Result ! true false ! false true - Example:
package main import "fmt" func main() { var finalGrade = 90 var attendance = 80 var passingGrade bool = finalGrade > 80 var passingAttendance bool = attendance > 80 var pass bool = passingGrade && passingAttendance fmt.Println(pass) // false }
- An array is a data type that holds a set of elements of the same type.
- When creating an array, it's necessary to specify the number of elements it can contain.
- The capacity of an array cannot be altered after its creation.
- We can access every element in an array with index starting from 0.
Index Element 0 Eko 1 Kurniawan 2 Khannedy - Example:
import "fmt" func main() { // Creating an array of string than will hold 3 elements var names [3]string fmt.Println(names) // [] // Assigning elements to the array names[0] = "Siregar" names[1] = "Ali" names[2] = "Yessica" fmt.Println(names) // [Siregar Ali Yessica] fmt.Println(names[0]) // Siregar fmt.Println(names[1]) // Ali fmt.Println(names[2]) // Yessica }
- We can also create an array directly during variable declaration.
var values = [3]int{ 90, 80, 95, // <= if we specify elements for an array in a vertical format like this, a comma must be placed after the last element } fmt.Println(values) // [90 80 95] fmt.Println(values[0]) // 90 // If we specify an array should have 3 elements but we don't assign any value during the variable declration, then the array will have three elements of 0 var zeroNumbers = [3]int{} fmt.Println(zeroNumbers) // [0 0 0] // If we specify an array should have 3 elements but we only assign 2 elements during variable declaration, the last index will get a default value 0 var numbers = [3]int{20, 7} fmt.Println(numbers) // [20 7 0] fmt.Println(numbers[2]) // 0
- Function Array
Operation Explanation len(array) Get the array length array[index] Get an element/data at a certain index position array[index] = value Change an element at a certain index position - We can also omit specifying the amount of elements/data that should be in an array by using [...] rather than [number of elements]. This way, the program will automatically set the length of the array to the number of elements assigned during variable declaration.
var ages = [...]int{ 20, 22, 25, } fmt.Println(ages) // [20 22 25] fmt.Println(len(ages)) // 3 ages[2] = 27 fmt.Println(ages) // [20 22 27]
- In Go, there is no way to alter the array length, for example by removing an element as can be done in JavaScript with methods like .pop() or .shift().
- The data type slice is a segment of an array.
- A slice is similar to an array, with the key difference being that the size of a slice can change.
- Slices and arrays are always connected because a slice is the data that accesses a part or the whole data in an array.
- Slice has three important pieces of data: pointer, length, and capacity.
- Pointer is a reference to the first element in the array that the slice points to.
- Length is the size or length of the slice.
- Capacity is the total capacity of the slice, where the length cannot exceed the capacity.
- Creating slice from an array
Creating Slice Explanation array[low:high] Create a slice from an array starting from index _low_ up to index before _high_ array[low:] Create a slice from an array starting from index _low_ up to the last index array[:high] Create a slice from an array starting from index 0 up to index before _high_ array[:] Create a slice from an array starting from index 0 up to the last index - Example:
func main() { var months = [12]string{ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", } // A slice with a pointer to index 4, length 3, capacity 8 (the range from index 4 to the last index 11 is 8) // One of the differences between creating an array and a slice is that when creating a slice, we don't specify the length of the slice var slice1 []string = months[4:7] // A slice with a pointer to index 6, length 3, capacity 6 (the range from index 6 to the last index 11 is 6) var slice2 []string = months[6:9] fmt.Println(months) // [January February March April May June July August September October November December] fmt.Println(slice1) // [May June July] fmt.Println(slice2) // [July August September] names := [...]string{"Eko", "Alana", "Alaia", "Aisha", "Muhammad", "Christopher", "Ketut"} slice := names[4:6] fmt.Println(slice) // [Muhammad Christopher] fmt.Println(slice[0]) // Muhammad fmt.Println(slice[1]) // Christopher }
- Function in slice
Operation Explanation len(slice) Get the length of the slice, not the array cap(slice) Get the capacity of the slice append(slice, data) Create a new slice by adding data to the last position of slice. If the capacity is already full, a new array will be created make([]dataType, length, capacity) Create a new slice, array will automatically created by the slice copy(destination, source) Copy a slice from the source to the destination - Example:
func main() { days := [...]string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} daySlice1 := days[5:] daySlice1[0] = "New Friday" // the data array has also changed daySlice1[1] = "New Saturday" fmt.Println(days) // [Sunday, Monday, Tuesday, Wednesday, Thursday, New Friday, New Saturday] daySlice2 := append(daySlice1, "Holiday") daySlice2[0] = "Ups" fmt.Println(daySlice2) // [Ups, New Saturday, Holiday] fmt.Println(daySlice1) // [New Friday, New Saturday] fmt.Println(days) // [Sunday, Monday, Tuesday, Wednesday, Thursday, New Friday, New Saturday] // Create a new slice, array will automatically created by the slice // newSlice := make([]string, 2, 5) or var newSlice []string = make([]string, 2, 5) newSlice[0] = "Eko" newSlice[1] = "Eko" // newSlice[2] = "Eko" -> error, because we have set the length to be 2. To add another data, we should use the _append()_ function fmt.Println(newSlice) // [Eko Eko ] fmt.Println(len(newSlice)) // 2 fmt.Println(cap(newSlice)) // 5 newSlice2 := append(newSlice, "Adi") fmt.Println(newSlice2) // [Eko Eko Adi] fmt.Println(len(newSlice2)) // 3 fmt.Println(cap(newSlice2)) // 5 // Copy slice fromSlice := days[:] toSlice := make([]string, len(fromSlice), cap(fromSlice)) fmt.Println(fromSlice) // [Sunday Monday Tuesday Wednesday Thursday New Friday New Saturday] fmt.Println(toSlice) // [ ] copy(toSlice, fromSlice) fmt.Println(toSlice) // [Sunday Monday Tuesday Wednesday Thursday New Friday New Saturday] }
- Be careful when creating an array, if done incorrectly, you may end up creating a slice instead of an array and vice versa.
import "fmt" func main() { anArray := [...]int{1, 2, 3, 4, 5} aSlice := []int{1, 2, 3, 4, 5} // In a slice, we don't specify the length/amount of data fmt.Println(anArray) // [1 2 3 4 5] fmt.Println(aSlice) // [1 2 3 4 5] }
- In Go, the use of arrays is less common when developing applications compared to the widespread use of slices.
- In an Array or Slice, we use index numbers/integers from 0 to access data.
- A Map is a data type that holds a collection of homogeneous data, allowing us to specify the data type for each unique index.
- Simply put, a Map is a data type that consists of key-value pairs, where each key must be unique.
- In contrast Arrays and Slices, a Map can accomodate an unlimited amount of data, as long as the keys are distict. If the same key is used, the previous data will be automatically replaced by the new data.
- Function Map:
Operation Explanation len(map) Retrieve the length of the data in a Map map[key] Retrieve the data associated with the specified key map[key] = value Modify data associated with a specific key var variable name map[TypeKey]TypeValue = map[TypeKey]TypeValue{} Create a new Map variable name := map[TypeKey]TypeValue{[key]: [value]} Create a new Map make(map[TypeKey]TypeValue) Create a new Map delete(map, key) Remove data associated with the specified key - Example:
import "fmt" func main() { // First way: creating a Map withh empty key-value pairs var aisha map[string]string = map[string]string{} aisha["username"] = "aisha" aisha["address"] = "Lombok" fmt.Println(aisha) // map[address:Lombok username:aisha] fmt.Println(aisha.username) // aisha fmt.Println(aisha.address) // Lombok // Second way: creating a Map and directly set the key-value pairs person := map[string]string{ "username": "pierledev", "address": "Bandung", } fmt.Println(person) // map[address:Bandung username:pierledev] fmt.Println(person["username"]) // pierledev fmt.Println(person["address"]) // Bandung // When attempting to access a value using a key that does not exist in our Map, it will return an default value and the type of the value will be depend on the Map type that we have specified. If we have specified the Map to be of string type, the default value will be an empty string fmt.Println(person["hobby"]) // "" movie := make(map[string]string) movie["title"] = "Hannah Montana" movie["actress"] = "Miley Cyrus" movie["wrong"] = "Ups" delete(movie, "wrong") fmt.Println(movie) // map[actress:Miley Cyrus title:Hannah Montana] }
- If is one of the keywords used for branching in programming.
- Branching allows the execution of specific code when a certain condition is met.
- the majority of programming languages support the if expression.
- Example:
func main() { name := "Pierle" if name == "Pierle" { fmt.Println("Hello, Pierle!") // Hello, Pierle! } }
- The if block is executed when the if condition evaluates to true.
- The else expression is used to execute a specific program when the condition evaluates to false.
- Example:
func main() { money := 100 if money > 140 { fmt.Println("Buy the sneakers!!") } else { fmt.Println("Save your money!!") } // Save your money!! }
- We use the else if expression when we need to evaluate multiple conditions.
- Example:
func main() { candidateNumber := 1 if candidateNumber== 1 { fmt.Println("Anies Baswedan") } else if candidateNumber == 2 { fmt.Println("Prabowo Subianto") } else { fmt.Println("Ganjar Pranowo") } // Anies Baswedan }
- The if expression supports a short statement before the condition.
- It is very suitable for creating a simple statement before checking the condition.
- Example:
func main() { username := "pierledev" if length := len(username); length > 10 { fmt.Println("Username is too long") } else { fmt.Println("You can use that username") } // You can use that username }
- In addition to the if expression, to create branching, we can also use the switch expression.
- The switch expression is simpler compared to the if statement.
- Typically, the switch expression is used to check conditions within a single variable.
- Example:
func main() { name := "Kenang" switch name { case "Kenzo": fmt.Println("Halo, Kenzo!") case "Kezia": fmt.Println("Halo, Kezia!") case "Kenang": fmt.Println("Halo, Kenang!") default: fmt.Println("Hi, anonim!") } // Halo, Kenang! }
- The switch statement also supports a short statement before the variable's condition that will be checked.
func main() { name := "Rina Ilase" switch length := len(name); length > 5 { case true: fmt.Println("Name is too long") case false: fmt.Println("You can use that name") } // Name is too long }
- Conditions in the switch expression are not mandatory.
- If we are not using conditions in the switch expression, we can include conditions in each case.
func main() { name := "Pierledev" length := len(name) switch { case length > 10: fmt.Println("Name is too long") case length > 5: fmt.Println("Name is quite long") default: fmt.Println("Perfect!") } }
- Based on the provided code, in this case, it might be better to use the if-else expression.
- In programming languages, there is typically a feature called a loop.
- One of the loop features is for loop.
- Example:
func main() { counter := 1 for counter <= 10 { fmt.Println("Loop ", counter) counter++ } fmt.Println("Finish") /* Loop 1 Loop 2 Loop 3 Loop 4 Loop 5 Loop 6 Loop 7 Loop 8 Loop 9 Loop 10 Finish */ }
- Inside the for loop, we can add statements, and there are two types of statements that can be included in th for loop:
- Init statement: a statement executed before the for loop begins.
- Post statement: a statement executed at the end of each iteration of the loop.
func main() { for counter := 1; counter <= 10; counter++ { fmt.Println("Loop", counter) } // counter := 1 -> init statement // counter <= 10 -> condition // counter++ -> post statement }
- The for loop can be used to iterate over all elements in a data collection, such as an Array, Slice, or Map.
// Manual names := []string{"Andika", "Chris", "Ana"} for i := 0; i < len(names); i++ { fmt.Println(names[i]) } /* Andika Chris Ana */ // With for range for index, name := range names { fmt.Println("index", index, "=", name) } /* index 0 = Andika index 1 = Chris index 2 = Ana */ // With for range, but we don't use the indexes for _, name := range names { fmt.Println(name) } /* Andika Chris Ana */
- break & continue are keywords that can be used in a loop.
- break is used to terminate the entire loop.
- continue is used to stop the current iteration and immediately proceed to the next iteration of the loop.
- Example:
func main() { for i := 0; i < 10; i++ { if i == 5 { break } fmt.Println("Loop", i) } /* Loop 0 Loop 1 Loop 2 Loop 3 Loop 4 */ for i := 0; i < 10; i++ { if i % 2 == 0 { continue } fmt.Println("Loop", i) } /* Loop 1 Loop 3 Loop 5 Loop 7 Loop 9 */ }
- Earlier, we learned about a mandatory function to be created for our program to run, the main function.
- A function is a deliberately created code block in a program that can be used multiple times.
- To create a function, we use the func keyword, followed by the function name and the code block inside the function.
- After creating a function, we can execute it by calling it using the function name followed by parentheses.
- When creating a function, sometimes we need data from outside, which we refer to as arguments passed into function parameters.
- We can set more than 1 parameter.
- Parameter is not mandatory.
- Parameters are not mandatory, but if we define a function with parameters, we must provide data (arguments) for those parameters when calling the function.
- Functions can return a value or multiple values.
- To indicate that a function returns a value/multiple values, we should specify the data type expected to be returned by the function one by one
- If we declare a function with a return data type, it is mandatory to return data wthin the function.
- To return a value from a function, we can use the return keyword followed by the value/data.
- Multiple return values must be captured for all the values.
- If we want to ignore the return values, we can use the underscore _ symbol.
- Named return values. Typically, when we indicate that a function returns a value, we declare the data type of the returned value in the function. However, we can also create variables directly in the function's return data type.
- Example:
// Without parameter func sayHello() { fmt.Println("Hello") } // With parameters func sayHelloTo(firstName string, lastName string) { fmt.Println("Hello", firstName, lastName, "!") } // Return a value func getHello(name string) string { return "Hello " + name } // Return multiple values func getFullName() (string, string) { return "Komang", "Ida" } func getCompleteName() (username, email, phone string) { username = "chika" email = "chika@gmail.com" phone = "08726241233" return username, email, phone } func getGrade() (name string, grade int) { name = "Ario" grade = 90 return ario, grade } func main() { sayHello() // Hello sayHelloTo("Robert", "Agung") // Hello Robert Agung ! result := getHello("Rika") fmt.Println(result) // Hello Rika firstName, lastName := getFullName() fmt.Println(firstName, lastName) // Komang, Ida name, _ := getFullName() fmt.Println(name) // Komang username, email, phone := getCompleteName() fmt.Println(username, email, phone) // chika, chika@gmail.com, 08726241233 _, grade := getGrade() fmt.Println(grade) // 90 }
- The last parameter in a function has the ability to be a varargs.
- Varargs means the parameter can receive more than one input or can be considered similar to an Array
- The difference between regular parameters and the array data is:
- For the parameter with an array type, it is madatory to create an array before sending it to the function.
- For a parameter using varargs, we can directly send the data, and if there is more than one value, we can use a colon.
- Example:
func sumAll(numbers ...int) int { // numbers here is actually a slice of integers []int total := 0 for _, number := range numbers { total += number } return total } func main() { total := sumAll(10, 10, 10, 10, 10, 10) fmt.Println(total) // 60 }
- Sometimes, there are some cases where we use a Variadic Function, but we have a variable in the form of a slice. We can convert the slice to be a vararg parameter.
func sumAll(numbers ...int) int { // numbers here is actually a slice of integers []int total := 0 for _, number := range numbers { total += number } return total } func main() { total := sumAll(10, 10, 10, 10, 10, 10) ft.Println(total) // 60 numbers := []int{10, 10, 10} // slice total = sumAll(numbers...) // convert slice data to varargs fmt.Println(total) // 30 }
- A function is a first-class citizen.
- Additionnally, a function is a data type and can be stored in a variable.
func getGoobBye(name string) string { return "Good Bye " + name } func main() { goodbye := getGoodBye fmt.Println(goodbye("Eko")) // Good Bye Eko }
- A function cannot only be stored in a variable as a value, but can also be used as a parameter for aother function, function as parameter.
func sayHelloWithFilter(name string, filter func(string) string) { fmt.Println("Hello ", filter(name)) } func spamFilter(name string) string { if name === "Bitchie" { return "..." } else { return name } } func main() { sayHelloWithFilter("Bitchie", spamFilter) // Hello ... }
- Type declarations can be used to create an alias for a function, making it easier to use the function as a parameter.
type Filter func(string) string func sayHelloWithFilter(name string, filter Filter) { fmt.Println("Hello ", filter(name)) } func spamFilter(name string) string { if name == "Bitchie" { return "..." } else { return name } }
- An anonymous function, also known as a function without a name, allows us to create a function directly in a variable or parameter without the need to define a function separately. Earlier, every time we created a function, a name was always assigned to it. However, sometimes it is easier to use a function directly without the need for a predefined name.
type Blacklist func(string) bool func registerUser(name string, blacklist Blacklist) { if(blacklist(name) { fmt.Println("Your are blocked", name) } else { fmt.Println("Welcome", name) }) } func main() { // The first way blacklist := func(name string) bool { return name === "Bitchie" } registerUser("Andini", blacklist) // Welcome Andini // The second way registerUser("Bitchie", func(name string) bool { return name == "Bitchie" }) // you are blocked Bitchie }
- A recursive function is a function that calls itself. One illustrative example is the calculation of a factorial.
// Factorial with loop func factorialLoop(value int) int { result := 1 for i := value; i > 0; i-- { result *= i } return result } // Factorial with recursive func factorialRecursive(value int) int { if value == 1 { return 1 } else { return value * factorialRecursive(value - 1) } } func main() { fmt.Println(factorialLoop(10)) // 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 = 3628800 fmt.Println(factorialRecursive(10)) // 3628800 }
- Closure refers to a function's capability to interact with data within the same scope. It's crucial to use this feature wisely when developing applications.
func main() { counter := 0 increment := func() { // anynomous function fmt.Println("Increment") counter++ } increment() increment() fmt.Println(counter) // 2 }
- This concept is somewhat akin to a try-catch structure in other programming languages.
- A defer function is a function that can be scheduled to be executed after another function has finished executing.
- The defer function will always be executed even if an error occurs in the executed function.
- This is a useful pattern for tasks like cleanup operations or logging that you want to ensure happen even if an error occurs or in a specific order.
func logging() { fmt.Println("Finished calling function") } func runApplication() { defer logging() // It will be called after all the block code below run. It schedules the logging() function to be executed after the runApplication() function finishes. fmt.Println("Run application") // Any other code in the runApplication function would be executed before the deferred function. /* Run application Finished calling function */ } func main() { runApplication() }
- The panic function is used to abruptly stop a program.
- The panic function is typically invoked when a panic occurs during the execution of a program.
- When the panic function is called, the program is halted, but any deferred function will still be executed.
func endApp() { fmt.Println("End App") } func runApp(error bool) { defer endApp() if error { panic("Error") } endApp() // If this regular function/a defer function is placed below the error-checking block and an error occurs, the code will not be triggered. } func main() { runApp(true) /* End App panic: Error */ }
- Recover is a function used to capture data from a panic.
- Through the recover process, a panic is halted, allowing the program to continue running.
func endApp() { fmt.Println("End App") } // Incorrect recover program code func runApp(error bool) { defer endApp() if(error) { panic("ERROR") } // The following codes won't be executed once the program run and the panic() function is executed message := recover() fmt.Println("Error happened", message) }
// Correct recover program code // Place the recover function and get the message from the panic() function inside the defer function because even if the program encounters an error, the defer function will still be executed func endApp() { fmt.Println("End App") message := recover() ft.Println("Error happened", message) } func runApp(error bool) { defer endApp() if(error) { panic("ERROR") } } func main() { runApp(true) fmt.Println("Printed!") /* Output: End app Error happened ERROR Printed! */ }
- The best comment for a code is the code itself.
- Write code for maximum readability, keeping it clear and straightforward.
- Add comments only when necessary for additional context.
// This is a single line comment /* This is multiline comment */
- Struct is a data template/data prototype used to combine zero or more data types into a unified entity.
- Typically, structs represent data in an application program that we create.
- Data in a struct is stored in a field.
- In simple terms, a struct is a collection of fields.
- Struct cannot be used directly.
- However, we can create data or objects from the struct that we have defined.
type Customer struct { Name, Address string Age int } func main() { var loyalCustomer1 Customer fmt.Println(loyalCustomer1) // {"" "" 0} // default values for data created with the Customer struct loyalCustomer1.Name = "Jacob" loyalCustomer1.Address = "SCBD" loyalCustomer1.Age = 24 fmt.Println(loyalCustomer1) // {Jacob SCBD 24} fmt.Println(localCustomer1.Name) // Jacob }
- Earlier, we have already created data using a struct, but actually, there are various methods for creating data from a struct, one of them is struct literals.
type Customer struct { Name, Address string Age int } func main() { joko := Customer{ Name: "Joko", Address: "Indonesia", Age: 30, } fm.Println(joko) // {Joko Indonesia 30} budi := Customer{"Budi", "Indonesia", 30} fmt.Println(budi) // {Budi Indonesia 30} }
- A struct is a data, similar to other data types, it can be used as a parameter for functions.
- Structs can have methods, just like how functions can be associated with a struct.
- A method is essentially a function.
type Customer struct { Name, Address string Age int } func (customer Customer) sayHello(name string) { fmt.Println("Hello", name, "my name is", customer.Name) } func main() { rully := Customer{Name: "Rully"} rully.sayHello("Radit") // Hello Radit my name is Rully }
- An interface is an Abstract data type without direct implementation.
- It consists of method definitions.
- Generally, an interface is utilized as a form of contract. A contract implies that there must be code that implements it.
- Any data type that conforms to the contract of the interface is considered to be of that interface type, thus, there is no need to manually implement the interface.
- This is somewhat different from other programming languages where, when creating an interface, we must explicitly specify which interface is being implemented.
type HasName interface { GetName() string // the code that implements this interface must have method GetName that returns string data type } func SayHello(value HasName) { fmt.Println("Hello", value.GetName()) } type Person struct { Name string } // In general, a person is an implementation of the HasName interface. func (person Person) GetName() string { return person.Name } func main() { person := Person{Name: "Eko"} SayHello(person) // Hello Eko } type Animal struct { Name string } func (animal Animal) GetName() string { return animal.Name } animal := Animal{Name: "Cat"} SayHello(animal) // Hello Cat
- Go is not an object oriented programming language.
- Typically, in object-oriented programming languages, there is a single parent data type at the top that can be considered as the root for all data implementations in that programming language; for example, in Java, there is java.lang.Object.
- To handle such cases in Go, we can use an empty interface.
- An empty interface is an interface that does not have any method declarations, making automatically every data type an implementation of it.
- The empty interface is also given a type alias named any. type any = interface{}.
- There are examples of the use of th empty interface in Go:
- fmt.Println(a ...interface{})
- panic(v interface{})
- recover() interface{}
- etc.
- So, when we use methods like fmt.Println, panic, or recover in Go, the parameters are of type empty interface (interface{}). This allows these functions to accept arguments of any type. The empty interface is a powerful feature in Go that provides flexibility in handling various types of data without explicitly specifying their types.
func Ups() interface{} { // iterface{} or any // return 1 it can returns int // return true it can also return bool return "Ups" // it can also return string and any other types } func main() { empty := Ups() fmt.Println(empty) }
- Typically, in other programming languages, an object that has not been initialized will automatically have null or nil as its value.
- In Go, when we create a variable with a certain data type, it automatically gets a default value.
- However, in Go, there is special value called nil, representing an empty data.
- Nil itself can only be used with certain data types, such as interface, function, map, slice, pointer, and channel.
// Invalid because nil cannot be used with data type string func check(name string) string { if name == "" { return nill } else { return name } } // Valid func NewMap(name string) map[string]string { if name == "" { return nil } else { return map[string]string { "name": name, } } } func main() { data := NewMap("") if(data == nil) { fmt.Println("Empty data") } else { fmt.Println(data) } // Output: Empty data }
- Type Assertions is a capability to convert a data type to a desired data type.
- This feature is often used when we dealing an empty data interface.
func random() interface{} { return "OK" } func main() { /* result := random() resultString := result.(string) // convert from any (interface{} type is any) to string fmt.Println(resultString) // OK resultInt := result.(int) fmt.Println(resultInt) // panic */ // OR var result any = random() var resultString string = result.(string) fmt.Println(resultString) // OK var resultInt int = result.(int) fmt.Println(resultInt) // panic }
- When we incorrectly use type assertions, it can lead to a panic in our application. If a panic occurs and is not recovered, the program will terminate automatically. To ensure safety, a better approach for type assertion is to first check the data type in use and then perform type assertion using a switch expression.
func main() { result := random() switch value := result.(type) { case string: fmt.Println("String", value) case int: fmt.Println("Int", value) default: fmt.Println("Unknown") } }
- By default, all variables in Go are passed by value and not by reference. This means that when we send a variable to a function, method, or other variables, what is actually sent is a duplication of its value. Therefore, when we modify the data, the original data remains safe and unaffected because the modification does not use a reference or point to the same memory location.
- In other programming language like in JavaScript, objects (including arrays and functions) are assigned and passed by reference, while primitive types (like numbers and strings) are assigned and passed by value. In JavaScript, when we assign an object to another variable, both variables reference the same object in memory. Therefore, modifications to the object through one variable will affect the other. This is different from Go's behavior, where variables hold independent copies of their values.
// Go package main import "fmt" type Address struct { City, Province, Country string } func main() { // Creating an instance of the Address struct address1 := Address{"Subang", "Jawa Barat", "Indonesia"} // Creating a new variable and copying the values from address1 address2 := address1 // Copy value // Modifying the City field in the new variable address2.City = "Bandung" // Printing the original and modified variables fmt.Println(address1) // City remains as "Subang" fmt.Println(address2) // City is now "Bandung" }
// JavaScript let address1 = { city: "Subang", province: "Jawa Barat", country: "Indonesia" }; // Creating a new variable and referencing the same object let address2 = address1; // Modifying the city property in the new variable address2.city = "Bandung"; // Printing the original and modified variables console.log("Original Address:", address1); // City is now "Bandung" console.log("Modified Address:", address2); // City is also "Bandung"
- A pointer is a capability to create a reference to the data location in the same memory without duplicating the existing data.
- In simple words, with the ability of pointers, we can achieve pass by reference.
- To assign a variable with the pointer value to another variable, we can use operator & followed by the variable name.
type Address struct { City, Province, Country string } func main() { address1 := Address{"Subang", "Jawa Barat", "Indonesia"} address2 := &address1 // address2 is set to have the data that has the same location with address1 // &address1 -> pointer to address1 /* OR var address1 Address = Address{"Subang", "Jawa Barat", "Indonesia"} var address2 *Address = &address2 */ address2.City = "Bandung" fmt.Println(address1) // City is now Bandung fmt.Println(address2) // City is also Bandung }
- When we modify a variable through a pointer, only that specific variable is affected.
- Other variables referencing the same data will remain unchanged.
type Address struct { City, Province, Country string } func main() { address1 := Address{"Subang", "Jawa Barat", "Indonesia"} address2 := &address1 address2.City = "Bandung" address2 = &Address{"Jakarta", "DKI Jakarta", "Indonesia"} // address2 is a pointer to address1, so we need to put '&' fmt.Println(address1) // {Bandung Jawa Barat Indonesia} fmt.Println(address2) // {Jakarta DKI Jakarta Indonesia} }
- If we want to update all variables referencing that data, we can use the * operator.
type Address struct { City, Province, Country string } func main() { address1 := Address{"Subang", "Jawa Barat", "Indonesia"} address2 := &address1 address2.City = "Bandung" *address2 = Address{"Jakarta", "DKI Jakarta", "Indonesia"} fmt.Println(address1) // {Jakarta DKI Jakarta Indonesia} fmt.Println(address2) // {Jakarta DKI Jakarta Indonesia} }
- Earlier, we created pointers using the & operator.
- Go also provides a new function that can be used to create a pointer.
- However, the new function can only return a pointer to an empty data, meaning there is no initial data assigned.
type Address struct { City, Province, Country string } func main() { // Creating pointer not with the 'new' function var address1 *Address = &Address{} var address2 *Address = address1 // Creating pointer with the 'new' function address1 := new(Address) // pointer to Address address2 := adddress1 // also pointer to address1 address2.Country = "Indonesia" // the result is, all pointers to the data Country changed to Indonesia fmt.Println(address1) // &{"" "" "Indonesia"} fmt.Println(address2) // &{"" "" "Indonesia"} }
- When we create a parameter in a function, by default, it is pass by value, meaning the data will be copied and then sent to the function.
- Therefore, if we change the data in a function, the original data will never be altered.
- This makes variables secure, as they cannot be modified.
- However, sometimes we want to create a function that can change the original data parameter.
- To achieve this, we can use a pointer in a function's parameter.
- To make a parameter a pointer, we can use the * operator in the parameter.
// Example for code that is not yet using a pointer type Address struct { City, Province, Country string } func ChangeAddressToIndonesia(address Address) { address.Country = "Indonesia" } func main() { address := Address{"Subang", "Jawa Barat", ""} ChangeAddressToIndonesia(address) fmt.Println(address) // not changed {"Subang" "Jawa Barat" ""} }
// Example for code that is using a pointer type Address struct { City, Province, Country string } func ChangeAddressToIndonesia(address *Address) { address.Country = "Indonesia" } func main() { var address *Address := &Address{"Subang", "Jawa Barat", ""} ChangeAddressToIndonesia(address) fmt.Println(address) // changed to {"Subang" "Jawa Barat" "Indonesia"} }
// If we've already created it as a non-pointer, we can simply add '&' directly to the function argument. type Address struct { City, Province, Country string } func ChangeAddressToIndonesia(address *Address) { address.Country = "Indonesia" } func main() { address := Address{"Subang", "Jawa Barat", ""} ChangeAddressToIndonesia(&address) // <= here fmt.Println(address) // changed to {"Subang" "Jawa Barat" "Indonesia"} }
- Although methods are attached to a struct, the data struct accessed in a method is actually passed by value.
- It's recommended to use pointers in methods to avoid unnecessary memory duplication when calling methods.
// Without pointer type Man struct { Name string } func (man Man) Married() { man.Name = "Mr. " + man.Name } func main() { eko := Man{"Eko"} eko.Married() fmt.Println(eko.Name) // Eko, not Mr.Eko because the Married function accepts a parameter by value, creating a copy and not using a reference to the original variable/argument }
// With pointer type man struct { Name string } func (man *Man) Married() { man.Name = "Mr. " + man.Name } func main() { eko := Man{"Eko"} eko.Married() fmt.Println(eko.Name) // Mr. Eko }
- A package is a container used to organize program code in Go.
- By utilizing packages, we can structure and organize the code we create.
- In essence, a package corresponds to a directory or folder in our operating system.
- The name of a Go package should match the name of the folder or directory in which the Go files of the package are stored. This is a convention in Go. This helps Go tools and developers maintain a consistent and predictable structure for organizing code. For example, if we have our existing folder "learning-golang" and we make another folder inside it called helper, create a Go file inside it also named helper or other name. At the very beginning of that file, write:
package helper func SayHello(name string) string { return "Hello " + name }
- To use the package, we use import.
- In standard practice, a Go file can only access other Go files that belong to the same package.
- If we want to access Go files outside of the package, we can use import. For example if we want to use helper package inside main.go file in the root folder a.k.a "learning-golang", then we should write the following code:
// main.go
import (
"learning-golang/helper"
"fmt"
)
func main() {
result := helper.SayHello("Eko")
fmt.Println(result) // Hello Eko
}- In other programming languages, there is usually a keyword that can be used to specify an access modifier for a function or variable.
- However, in Go, you can determine the access modifier simply by the name of the function/variable/struct/interface.
- If the name begins with a capital letter, it means it can be accessed from other packages. If it starts with a lowercase letter, it means it cannot be accessed from other packages.
package helper // Can't be accesse from other packages var version = "1.0.0" func sayGoodBye(name string) string { return "Hello " + name } // Can be acessed from other packages var Application = "golang" func SayGoodBye(name string) string { return "Hello " + name }
- When creating a package in Go, we have the option to include a function that will be accessed automatically when our package is used.
- This is particularly useful, for instance, when our package contains functions related to database communication, and we want to establish a connection to the database during initialization.
- To create a function that is automatically accessed when the package is used, we simply need to name the function init.
- For instance, let's say we create a new folder named "database" and inside it, a file named "mysql." We then write the following code inside the "mysql" file:
Now, let's use the "database" package in the main.go file located in the root folder of "learning-golang":
package database var connection string func init() { connection = "MySQL" } // the init function will be automatically called when we use the "database" package. func GetDatabase() string { return connection }
By importing the "learning-golang/database" package, the init() function inside the "mysql" file will be automatically executed without the need for an explicit call.package main import ( "learning-golang/database" // <module-name>/<package-name> "fmt" ) func main() { fmt.Println(database.GetDatabase()) }
- Sometimes, we might only be interested in running the init function in a package without necessarily executing one of the functions that exist in the package.
- By default, Go would raise complaints if an imported package is left unused.
- To address this, we can employ the use of the blank identifier (_) before the package name during the import. Here, the underscore before the package name signals to Go that we are intentionally importing the package for the side effects (like the execution of init), and it suppresses the unused import complaint.
package main import ( _ "learning-golang/database" ) func main() { // The 'database' package is imported with the blank identifier, // allowing the 'init' function to be executed without complaints }
- Earlier, we already learned about panic, recover, and defer for scenarios where an error needs to halt the application.
- However, if we just want to signal an error without stopping the application, we can use the error feature.
- Go has an interface named error that serves as the standard interface for creating errors. The interface is defined as follows:
// The error built-in interface type is the conventiona interface for representing an error condition, with the nil value representing no error type error interface { Error() string }
- Creating an error in Go doesn't require manual effort. Go provides a library in the errors package that makes it easy to create error helpers.
import ( "errors" "fmt" ) func Divider(nilai int, pembagi int) (int, error) { if nilai == 0 { return 0, errors.New("Pembagian dengan 0") } else { return nilai / pembagi, nil } } func main() { hasil, err := Divider(100, 0) if err == nil { fmt.Println("Hasil", hasil) } else { fmt.Println("Error", err.Error()) } // output:Error Pembagian dengan 0 }
- We can create custom errors in Go by implementing the error interface. Since the error interface has only one method, Error() string, we can define a struct that satisfies this interface by implementing the Error() method.
A little breakdown from the codes above:
// Defining a custom error type for validation errors type validationError struct { Message string } // Implementing the Error() method for the validationError type func (v *validationError) Error() string { return v.Message } // Defining a custom error type for not found errors type notFoundError struct { Message string } // Implementing the Error() method for the notFoundError type func (v *notFoundError) Error() string { return v.Message } // Using the custom error types in the SaveData function func SaveData(id string, data any) error { if id == "" { // Returning a validation error when ID is empty return &validationError{Message: "validation error"} // because it is an interface, so we return it as a pointer } if id != "eko" { // Returning a not found error when ID is not "eko" return ¬FoundError{Message: "data not found"} } // Other operations... // Returning nil when there is no error return nil } func main() { // Checking the kind of error returned by SaveData err := SaveData("", nil) // If else if err != nil { if validationErr, ok := err.(*validationError); ok { fmt.Println("validation error:", validationErr.Message) } else if notFoundErr, ok := err.(*notFoundError); ok { fmt.Println("not found error:", notFoundErr.Message) } else { fmt.Println("unknown error:", err.Error()) } } else { fmt.Println("success") } // The same functionality code with switch switch finalError := err.(type) { case *validationError: fmt.Println("validation error:", finalError.Error()) case *notFoundError: fmt.Println("not found error:", finalError.Error()) default: fmt.Println("unknown error", finalError.Error()) } }
- Pointer receiver. In the method receivers (e.g., (v *validationError) and (v *notFoundError)), the asterisk indicates that the receiver is a pointer. This means these methods are associated with instances of the respective types by reference (i.e., modifying the object directly).
func (v *validationError) Error() string { return v.Message } func (v *notFoundError) Error() string { return v.Message }
- Creating a pointer. When returning errors from the SaveData function, the & symbol is used to create a pointer to an instance of validationError or notFoundError. The asterisk is then used in the return type to indicate that the function returns a pointer to an error.
return &validationError{Message: "validation error"} return ¬FoundError{Message: "data not found"}
- Type assertion.In the main function, when checking the type of the returned error, the asterisk is used in the type assertion (err.(*validationError)) to assert that err is of type *validationError. It's a way of converting the interface type to the specific custom error type.
if validationErr, ok := err.(*validationError); ok { // ... } else if notFoundErr, ok := err.(*notFoundError); ok { // ... }
- Pointer receiver. In the method receivers (e.g., (v *validationError) and (v *notFoundError)), the asterisk indicates that the receiver is a pointer. This means these methods are associated with instances of the respective types by reference (i.e., modifying the object directly).