IMPORTANT:
Some of the content here is a personal summary/abbreviation of contents on the Offical Go Documentation. Feel free to refer to the official site if you think some of the sections written here are not clear.
Installing Go
Please follow the Offical Guide on Installing Go.
Packages
(additional reference: https://www.callicoder.com/golang-packages/#:~:text=Go%20Package,following%20syntax%20%2D%20package)
Every Go program is made up of packages, and every Go source file belongs to a package. To declare a source file to be part of a package, we use the following syntax -
1 | package <packagename> |
The above package declaration must be the first line of code in your Go source file. All the functions, types, and variables defined in your Go source file become part of the declared package.
You can choose to export a member defined in your package to outside packages, or keep them private to the same package. Other packages can import and reuse the functions or types that are exported from your package.
Let’s see an example
Almost all the code that we will see in this tutorial series include the following line -
1 | import "fmt" |
fmt
is a core library package that contains functionalities related to formatting and printing output or reading input from various I/O sources. It exports functions like Println()
, Printf()
, Scanf()
etc, for other packages to reuse.
Note:
- By convention, the package name is the same as the last element of the import path. For instance, the
"math/rand"
package comprises files that begin with the statementpackage rand
.
The main
package and main()
function
Go programs start running in the main
package. It is a special package that is used with programs that are meant to be executable. The main()
function is a special function that is the entry point of an executable program.
By convention, Executable programs (the ones with the main package) are called Commands. Others are called simply Packages.
Usually, you would have a main
package at the top level of your project, and then create sub-directories for other packages. For example, you could have the following structure:
1 | myProject/ |
Running a Program
Programs start running in package main
.
For example, using the math/rand
pacakge, we could generate a random number by:
1 | package main |
Note:
- The environment in which these programs are executed is deterministic, so each time you run the example program rand.Intn will return the same number. To see a different number, seed the number generator; see
rand.Seed
.
Imports
To import a go package (a collection of go files), it first needs to be included in your GoPath.
There are two ways to import packages in Go -
1 | // Multiple import statements |
It is generally considered a good style to use the factored import statement.
Notice that the imported package name basically follow the directory hierarchy (as mentioned before, Go’s convention is that - the package name is the same as the last element of the import path). For example, the name of the package imported as math/rand
is rand. It is imported with path math/rand
because It is nested inside the math package as a subdirectory.
Import Paths
(additional reference: https://www.callicoder.com/golang-packages/)
All import paths are relative to the GoPath or GoModule (this will be talked about later) path.
For example, if you have github.com/callicode/packer
as the directory where you initiate your go.module
file, then you can refer to packages relative to that module’s directory:
1 | import ( |
Package Alias
You can use package alias to resolve conflicts between different packages of the same name, or just to give a short name to the imported package
1 | import ( |
Adding 3rd party Packages
Adding 3rd party packages to your project is very easy with Go modules. You can just import the package to any of the source files in your project, and the next time you build/run the project, Go automatically downloads it for you -
1 | package main |
And the output will look like:
1 | $ go run main.go |
Go will also add this new dependency to the go.mod
file.
Exports
In Go, a function/variable/type is exported if its name begins with a capital letter. For example, Pizza()
is an exported name, as is Pi
, which is exported from the math
package.
pizza
and pi
do not start with a capital letter, so they are not exported. This means that they are not accessible from outside the package when using imports.
So, in short:
- Anything (variable, type, or function) that starts with a capital letter is exported, and visible outside the package.
- Anything that does not start with a capital letter is not exported, and is visible only inside the same package.
For example:
1 | package main |
1 | # Output |
Functions
(additional reference: https://www.callicoder.com/golang-functions/)
In Golang, we declare a function using the func
keyword. A function has a name, a list of comma-separated input parameters along with their types, the** result type(s), and a **body.
Following is an example of a simple function called avg that takes two input parameters of type float64 and returns the average of the inputs. The result is also of type float64
-
1 | func avg(x float64, y float64) float64 { |
Notice that the type comes after the variable name.
Function Parameters and Return Types
The input parameters and return type(s) are optional for a function. A function can be declared without any input and output.
The main()
function is an example of such a function -
1 | func main() { |
Here is another example -
1 | func sayHello() { |
Input Type Abbreviation
You need to specify the type only once for multiple consecutive parameters of the same type
If a function has two or more consecutive parameters of the same type, then it suffices to specify the type only once for the last parameter of that type.
For example, we can declare the avg function that we saw in the previous section like this as well -
1 | func avg(x, y float64) float64 { } |
Here is another example -
1 | func printPersonDetails(firstName, lastName string, age int) { } |
Multiple Return Results
Go is capable of returning multiple results. You just need to specify the return types separated by comma inside parentheses, and then return multiple comma-separated values from the function.
1 | package main |
A common approach using this feature is to return an error value from a function if there could be an abnormal behavior with some input.
Let’s see an example - The getStockPriceChange
function that we saw will return ±Inf (Infinity)
if the prevPrice
is 0
. If you want to return an error
instead, you can do so by adding another return value of type error
and return the error
value like so -
1 | func getStockPriceChangeWithError(prevPrice, currentPrice float64) (float64, float64, error) { |
The error
type is a built-in type in Golang. Go programs use error values to indicate an abnormal situation. Don’t worry if you don’t understand about errors for now. You’ll learn more about error handling in a later section.
Named return values
Go’s return values may be named. If so, they are treated as variables defined at the top of the function.
For example:
1 | package main |
Note:
- A good practise would be using those names to document the meaning of the return values.
A return statement without arguments returns the named return values. This is known as a “naked” return.
Naked return statements should be used only in short functions, as with the example shown here. They can harm readability in longer functions.
Blank Identifier
Sometimes you may want to ignore some of the results from a function that returns multiple values.
For example, Let’s say that you’re using the split
function that we defined in the previous section, but you’re only interested in the final result of y
, not the intermediate x
.
Well, you can use a blank identifier instead -
1 | _, y := split(17) |
Q: Why can you not just declare a variable but not use it?
The problem is that in Go, you’ll be forced to use the variables you declared because Go doesn’t allow creating variables that you never use.
Variables
(additional reference: https://www.callicoder.com/golang-variables-zero-values-type-inference/)
Every program needs to store some data/information in memory. The data is stored in memory at a particular memory location.
A variable is just a convenient name given to a memory location where the data is stored. Apart from a name, every variable also has an associated type.
For example, Golang has a data type called int8
. It represents 8-bit integers whose values can range from -128
to 127
. It also defines the operations that can be done on int8
data type such as addition, subtraction, multiplication, division etc.
Declaring Variables
In Golang, We use the var
keyword to declare variables -
1 | var firstName string |
You can also declare multiple variables at once like so -
1 | var ( |
You can even combine multiple variable declarations of the same type with comma -
1 | var ( |
Zero Values
Any variable declared without an initial value will have a zero-value depending on the type of the variable-
Type | Zero Value |
---|---|
bool | false |
string | ”” |
int, int8, int16 etc. | 0 |
float32, float64 | 0.0 |
The example below demonstrates the concept of zero values:
1 | package main |
1 | # Output |
Variable Initialization
Here is how you can initialize variables during declaration -
1 | var firstName string = "Satoshi" |
You can also use multiple declarations like this -
1 | var ( |
Or even combine multiple variable declarations of the same type with comma and initialize them like so -
1 | var ( |
Note:
- All these styles are allowed in both package level declaration and function level declaration
Type Inference
Although Go is a Statically typed language (a language is statically-typed if the type of a variable is known at compile-time instead of at run-time), It doesn’t require you to explicitly specify the type of every variable you declare.
When you declare a variable with an initial value, Golang automatically infers the type of the variable from the value on the right-hand side. So you need not specify the type when you’re initializing the variable at the time of declaration -
1 | package main |
1 | # Output |
In the above example, Golang automatically infers the type of the variable as string from the value on the right-hand side. If you try to reassign the variable to a value of some other type, then the compiler will throw an error -
1 | var name = "Rajeev Singh" // Type inferred as `string` |
This also means that type inference allows us to declare and initialize multiple variables of different data types in a single line like so -
1 | package main |
1 | # Output |
Short Variable Declarations
Inside a function, the :=
short assignment statement can be used in place of a var
declaration with implicit/inferred type.
1 | package main |
Outside a function, every statement needs to begin with a keyword (var
, func
, and so on) and so the :=
construct is not available.
Scope of Variables
A var statement can be at package or function level.
- If you defined a variable at the function level, then it is visible only inside that function.
- If you defined a variable at the package level, then it is visible to all functions inside that package.
However, when you declared a variable inside a function with the same name as the variable defined in the package level, then, from that declaration onwards, the variable defined in the function will be used.
For example:
1 | package main |
Types
Go is a statically typed programming language. Every variable in Golang has an associated type.
Data types classify a related set of data. They define how the data is stored in memory, what are the possible values that a variable of a particular data type can hold, and the operations that can be done on them.
Basic Types
Go’s basic types are
1 | bool |
The example shows variables of several types, and also that variable declarations may be “factored” into blocks, as with import statements.
The int
, uint
, and uintptr
types are usually 32 bits wide on 32-bit systems and 64 bits wide on 64-bit systems. When you need an integer value you should use int
unless you have a specific reason to use a sized or unsigned integer type.
Byte and Rune
Golang has two integer type aliases called byte
and rune
that are aliases for uint8
and int32
data types respectively -
Type | Alias For |
---|---|
byte | uint8 |
rune | int32 |
In Go, the byte
and rune
data types are used to distinguish characters from integer values.
Golang doesn’t have a char
data type. It uses** byte
and rune
to represent character values. The **byte
data type represents ASCII characters and the rune
data type represents a more broader set of Unicode characters that are encoded in UTF-8 format.
Characters are expressed in Golang by enclosing them in single quotes like this: ‘A’.
The default type for character values is rune
. That means, if you don’t declare a type explicitly when declaring a variable with a character value, then Go will infer the type as rune
-
1 | var firstLetter = 'A' // Type inferred as `rune` (Default type for character values) |
You can create a byte
variable by explicitly specifying the type -
1 | var lastLetter byte = 'Z' |
Both byte and rune data types are essentially integers. For example, a byte
variable with value ‘a’ is converted to the integer 97. Similarly, a rune
variable with a unicode value ‘♥’ is converted to the corresponding unicode codepoint U+2665
, where U+
means unicode and the numbers are hexadecimal, which is essentially an integer.
Operations on Numberic Types
Go provides several operators for performing operations on numeric types -
- Arithmetic Operators:
+
,-
,*
,/
,%
- Comparison Operators:
==
,!=
,<
,>
,<=
,>=
- Bitwise Operators:
&
,|
,^
,<<
,>>
- Increment and Decrement Operators:
++
,--
- Assignment Operators:
+=
,-=
,*=
,/=
,%=
,<<=
,>>=
,&=
,|=
,^=
Here is an example demonstrating some of the above operators -
1 | package main |
1 | # Output |
Operations on Boolean Types
You can use the following operators on boolean types -
- Logical Operators:
&&
(logical conjunction, “and”)||
(logical disjunction, “or”)!
(logical negation)
- Equality and Inequality:
==
(“equals”)!=
(“not equals”)
The operators &&
and ||
follow short-circuiting rules. That means, in the expression E1 && E2
, if E1
evaluates to false
then E2
won’t be evaluated. Similarly, in the expression E1 || E2
, if E1
evaluates to true
then E2
won’t be evaluated.
Here is an example of Boolean types-
1 | package main |
1 | # Output |
Operations on Complex Types
You can perform arithmetic operations like addition, subtraction, multiplication, and division on complex numbers -
1 | package main |
1 | # Output |
Strings
In Go, a string is a sequence of bytes
.
Strings in Golang are declared either using double quotes as in "Hello World"
or back ticks as in `Hello World`
.
1 | // Normal String (Can not contain newlines, and can have escape characters like `\n`, `\t` etc) |
Double-quoted strings cannot contain newlines but they can have escape characters like \n
, \t
etc. In double-quoted strings, a \n
character is replaced with a newline, and a \t
character is replaced with a tab space, and so on.
Strings enclosed within back ticks are raw strings. They can span multiple lines. Moreover, Escape characters don’t have any special meaning in raw strings.
1 | package main |
1 | # Output |
There is quite a lot about operations in String, which will be covered later, but not in this section.
Type Conversion
The expression T(v) converts the value v to the type T.
Some numeric conversions:
1 | var i int = 42 |
Or, put more simply:
1 | i := 42 |
Unlike in C, in Go assignment between items of different type requires an explicit conversion.
Additionally, Golang has a strong type system. It doesn’t allow you to mix numeric types in an expression. For example, You cannot add an int
variable to a float64
variable or even an int
variable to an int64
variable. You cannot even perform an assignment between mixed types -
1 | var a int64 = 4 |
Unlike other statically typed languages like C, C++, and Java, Go doesn’t provide any implicit type conversion.
Constants
In Golang, we use the term constant
to represent fixed (unchanging) values such as 5
, 1.34
, true
, "Hello"
etc.
Constants can be character, string, boolean, or numeric values. In fact, all the literals in Golang, be it integer literals like 5
, 1000
, or floating-point literals like 4.76
, 1.89
, or boolean literals like true
, false
, or string literals like "Hello"
, "John"
are constants (cannot be modified). This means that self-made types cannot be declared as constant
s.
Some examples are:
Constants | Examples |
---|---|
integer constants | 1000, 67413 |
floating-point constants | 4.56, 128.372 |
boolean constants | true, false |
rune constants | ‘C’, ‘ä’ |
complex constants | 2.7i, 3 + 5i |
string constants | “Hello”, “Rajeev” |
Declaring a Constant
Literals are basically constants without a name. To declare a constant and give it a name, you can use the const
keyword like so -
1 | const sunRisesInTheEast = true |
You can also specify a type in the declaration like this -
1 | const a int = 1234 |
Multiple declarations in a single statement is also possible -
1 | const country, code = "India", 91 |
Constants declarations are very similar to variable declaratios, except that constans cannot be declared using the :=
syntax, but with =
.
Typed and Untyped Constants
This basically has the same mechanism of a typed and an untyped variable. So it is not covered here again. Even the default types used for type inference are the same.
If you really would like to see the details on constants, please visit https://www.callicoder.com/golang-typed-untyped-constants/ for more details.
Control Flow Statements
Go has only one looping construct, the for loop.
For Loop
The basic for loop has three components separated by semicolons (same as Java):
- the init statement: executed before the first iteration
- the condition expression: evaluated before every iteration
- the post statement: executed at the end of every iteration
The init statement will often be a short variable declaration, and the variables declared there are visible only in the scope of the for statement. Also, alike Java, the init and post statements are optional. For example:
1 | package main |
Unlike other languages like C, Java, or JavaScript there are no parentheses surrounding the three components of the for statement and the braces { }
for code block are always required.
“While” Loop
There is no actual while
loop in Go. However, since the init and post statements are optional, the for
loop abbreviated could be used as a while loop:
1 | package main |
If you omit the loop condition it loops forever, so an infinite loop is compactly expressed.
1 | package main |
Break/Continue in For Loop
You can use break
statement to break out of a loop before its normal termination. Here is an example -
1 | package main |
Similarly, the continue
statement is used to stop running the loop body midway and continue to the next iteration of the loop.
1 | package main |
if
/else if
/else
Control Flow
Go’s if
/else if
/else
statements are like its for loops; the expression need not be surrounded by parentheses ( )
but the braces { }
are required.
1 | package main |
The else if
is similar to if
, except that there can be multiple of them:
1 | package main |
if
/else if
Short Declaration
Like for, the if/else if
statement can start with a short statement to execute before the condition.
Variables declared by the statement are only in scope until the end of the entire if
/else if
/else
loop. For example:
1 | func IfWithShortStatement (input int)(){ |
Note that, if you’re using a short statement, then you can’t use parentheses. So the following code will generate a syntax error -
1 | // You can't use parentheses when `if` contains a short statement |
Switch Statement
A switch statement is a shorter way to write a sequence of if
- else
statements. It runs the first case whose value is equal to the condition expression.
Go’s switch is like the one in C, C++, Java, JavaScript, and PHP, except that Go only runs the selected case, not all the cases that follow. In effect, the break
statement that is needed at the end of each case in those languages is provided automatically in Go.
Another important difference is that Go’s switch cases need not be constants, and the values involved need not be integers.
1 | package main |
Switch Without A Condition
Switch without a condition is the same as switch true
.
This construct can be a clean way to write long if-then-else chains.
1 | package main |
Defer
A defer statement defers the execution of a function until the surrounding function returns (it is like the finally
cause in a try
block in Java)
The deferred call’s arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.
1 | // Go program to illustrate the |
1 | Output: |
Stacking Defers
Deferred function calls are pushed onto a stack. When a function returns, its** deferred calls are executed in last-in-first-out order**.
For example, 9
will be the first result that you will see:
1 | package main |
Pointers
(additional reference: https://www.callicoder.com/golang-pointers/a)
Go has pointers. A pointer holds the memory address of a value.
1 | // A pointer of type T |
The type *T
is a pointer to a T
value. Its zero value is nil
. For example:
1 | // a pointer of type int |
The above pointer can only store the memory address of int
variables.
Initializing a Pointer
To initialize a pointer with the memory address of another variable you can use &
. The address of a variable can be retrieved using the &
operator:
1 | i := 42 |
Just like the other types, pointers can also be inferred by Go:
1 | i := 42 |
Dereferening a Pointer
The *
operator on a variable (not a type) denotes the pointer’s underlying value by going to that address and fetching its value.
1 | fmt.Println(*p) // read i through the pointer p |
This is known as “dereferencing” or “indirecting”.
However, you if use a pointer on another pointer, then:
1 | p2 = &p1 // where p1 is already a pointer pointing to a variable |
You can not only access the value of the pointed variable using *
operator, but you can change it as well. The following example sets the value stored in the variable a
through the pointer p -
1 | package main |
1 | # Output |
Creating a Pointer using the built-in new()
function
You can also create a pointer using the built-in new()
function. The new()
function takes a type as an argument, allocates enough memory to accommodate a value of that type, initializes it with the zero value and returns a pointer to it.
Here is an example -
1 | package main |
1 | # Output |
However, unlike C, Go has no pointer arithmetic. For example, you cannot increment/decrement a pointer to move to the next/previous memory address. You cannot add or subtract an integer value to/from a pointer. You can also compare two pointers using relational operators <
, >
etc. Any such operation will result in a compile time error:
1 | package main |
You can, however, compare two pointers of the same type for equality using ==
operator. This basically compares the address stored, if they have the same address, then it returns true
, otherwise false
.
1 | package main |
Structs
(additional reference: https://www.callicoder.com/golang-structs/)
A struct is a user-defined type that contains a collection of named fields/properties. It is used to group related data together to form a single unit.
If you’re coming from an object-oriented background, you can think of a struct as a lightweight class that supports composition but NOT inheritance.
Defining a Struct Type
You can define a new struct
type like this -
1 | type Person struct { // note this is a curly brace |
The type
keyword introduces a new type. It is followed by the name of the type (Person
) and the keyword struct
to indicate that we’re defining a struct
(another option would be interface
, which will be covered later). The struct
contains a list of fields inside the curly braces { }. Each field has a name and a type.
Instantiating a Struct Type
Just like other data types, you can declare a variable of a struct type like this -
1 | // Declares a variable of type 'Person' |
The above code creates a variable of type Person
that is by default set to zero. For a struct
, zero means all the fields are set to their corresponding zero value. So the fields FirstName
and LastName
are set to ""
, and Age
is set to 0
.
You can initialize a variable of a struct
type by passing in values required by that struct with { }
-
1 | // Initialize a struct by supplying the value of all the struct fields. |
Note that you need to pass the field values in the same order in which they are declared in the struct (if you don’t specify the variable names). Also, you can’t initialize only a subset of fields with the above syntax -
1 | var p = Person{"Rajeev"} // Compiler Error: too few values in struct initializer |
However, Go also supports the name: value
syntax for initializing a struct
, so that the order of fields becomes irrelevant.
1 | var p = Person{LastName: "Singh", Age: 25, FirstName: "Rajeev"} |
The name: value
syntax allows you to initialize only a subset of fields. All the uninitialized fields are set to their corresponding zero value:
1 | var p = Person{FirstName: "Alien"} // LastName: "", Age: 0 |
Accessing fields of a Struct
This is same as accessing the field of a Java object - you use the .
operator:
1 | package main |
Pointer to Structs
Similar to those built-in struct
s, you can get a pointer to a struct
using the &
operator on it -
1 | package main |
Notice that. to access the field X
of a struct when we have the struct pointer p
we could write (*p).X
. However, that notation is cumbersome, so the language permits us instead to write just p.X
, without the explicit dereference (it only dereferences for one level, so nested pointers still need *
).
Creating a struct
using the built-in new()
function
You can also use the built-in new()
function to create an instance of a struct, but alike its previous usage, it returns to you the address of that struct
. The new()
function allocates enough memory to fit all the struct fields, sets each of them to their zero value and returns a pointer to the newly allocated struct
-
1 | package main |
Exported and Unexported Fields in a Struct
Any struct
type that starts with a capital letter is exported and accessible from outside packages. Similarly, any struct
field that starts with a capital letter is exported (visible outside package).
On the contrary, all the names starting with a small letter are visible only inside the same package.
Let’s see an example. Consider the following package hierarchy of our Go program -
1 | package model |
Then, to initialize the Customer
struct, you would need to explicitly use the name: value
declaration:
1 | package main |
In the above example, if you use c := model.Customer{1,"Rajjev Singh"}
, it will give you Compile Error
.
Structs and Short Declarations
Now, since pointers are used to store a reference to a piece of data, (short) variable declarations without specifying a pointer actually make a new copy of the variable:
1 | // the below three all have the same value but different memory address |
This is the same with using a struct
:
1 | package main |
Equality ==
This means that if you use ==
between two struct
s, it compares the actual value of the fields in that struct
. This is diffrent from Java, where ==
would be used to compare the memory address (you can compare addresses if you are comparing two pointers).
1 | package main |
1 | # Output |
Arrays
(additional reference: https://www.callicoder.com/golang-arrays/)
An array is a fixed-size collection of elements of the same type. The elements of the array are stored sequentially and can be accessed using their index. (Similar to other programming languages)
Declaring an Array
The type [n]T
is an array of n values of type T
.
The expression:
1 | var a [10]int |
declares a variable a
as an array of ten integers (int
).
An array’s length is part of its type, so arrays cannot be resized. This seems limiting, but don’t worry; Go provides a convenient way of working with arrays.
Now let’s see a complete example -
1 | package main |
1 | # Output |
By default, all the array elements are initialized with the zero value of the corresponding array type.
You can declare and initialize an array at the same time like this (similar to Java) -
1 | // Declaring and initializing an array at the same time |
You can also let Go infer the size of an array at initialization time:
1 | arr := []int{1,2,3,4,5} // infers the size to 5, uses a SLICE, rather an array |
Modifying an Array
This is the same in any other language, that elements in an array can be modified with an index:
1 | arr[1] = 5 // changes the second element to 5 |
Slices
An array has a fixed size. A slice, on the other hand, is a dynamically-sized, flexible view into the elements of an array. In practice, slices are much more common than arrays. (Internally, a Slice is just a reference to an existing array, if it is constructed from an array.)
Declaring a Slice
There are two (actually four) ways to declare a slice. You can either create a brand new slice, or create a slice based on an existing array.
Creating from an Existing Array
A slice is formed by specifying two indices, a low and high bound, separated by a colon:
1 | arr[low : high] // where arr is an array |
This selects a half-open range which includes the first element, but excludes the last one (alike Python).
The following expression creates a slice which includes elements 1 through 3 of a
:
1 | a[1:4] // takes elements 1 to 3, inclusive |
For example:
1 | package main |
However, since this slice is constructed from an array, under the hood it actually stores the references to the array. This means that changing the elements of a slice modifies the corresponding elements of its underlying array. Additionlly, other slices that share the same underlying array will see those changes.
Creating a Brand New Slice
A slice literal is like an array literal without the length.
The type []T
can also be seen as a slice with elements of type T
. Note the only difference is that you do not specify the size/length, which would be an array creation.
For example:
1 | // Creating a slice using a slice literal |
Under the hood, when you create a slice using a slice literal, it first creates an array and then returns a slice reference to it.
Slicing Defaults
When slicing, you may omit the high or low bounds to use their defaults instead:
- zero for the low bound
- length of the slice for the high bound
For example, if you have an array that looks like
1 | var a = [10]int {1,2,3,4....} // some numbers omitted |
these slice expressions are equivalent:
1 | a[0:10] |
Creating a Slice from another Slice
This is a third way to create a slice, however, there are some details that is different.
length and a capacity
The length of a slice is the number of elements it contains. The capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice.
The length and capacity of a slice s can be obtained using the expressions len(s)
and cap(s)
, where s
is a slice.
Now, if you created a slice from another slice, the range of elements you can retrieve is bounded by the capacity of that parent slice.
1 | package main |
Creating a Slice with make
Slices can be created with the built-in make function; this is how you create dynamically-sized arrays.
The make function allocates a zeroed array and returns a slice that refers to that array:
1 | a := make([]int, 5) // len(a)=5, filled with zero-value of 0 |
To specify a capacity, pass a third argument to make:
1 | b := make([]int, 0, 5) // len(b)=0, cap(b)=5 |
Functions for Slice
(addtional reference: https://www.callicoder.com/golang-slices/#slice-functions)
Appending to a slice
The append() function appends new elements at the end of a given slice. Following is the signature of append function.
1 | func append(s []T, x ...T) []T |
It takes a slice and a variable number of arguments x …T
. It then returns a new slice containing all the elements from the given slice as well as the new elements.
If the given slice doesn’t have sufficient capacity to accommodate new elements then a new underlying array is allocated with bigger capacity. All the elements from the underlying array of the existing slice are copied to this new array, and then the new elements are appended.
However, if the slice has enough capacity to accommodate new elements, then the append()
function re-uses its underlying array and appends new elements to the same array, it will also overwrite the existing element if it was there.
Copying a Slice
The copy()
function copies elements from one slice to another (by creating another array and then copying the value over). Its signature looks like this -
1 | func copy(dst, src []T) int |
It takes two slices - a destination slice, and a source slice. It then copies elements from the source to the destination and returns the number of elements that are copied.
The number of elements copied will be the minimum of len(src)
and len(dst)
, not cap
.
For example:
1 | package main |
Iterating over a Slice
You can iterate over a slice in the same way you iterate over an array. Following are two ways of iterating over a slice:
- the conventional for loop, which will not be demonstrated here as it is trivial
- using the
range
with an enhancedfor
loop
Using range
The following shows an example of using a range
on a slice. However, it is the same for an arary:
1 | package main |
You can skip the index or value by assigning to _
.
1 | for i, _ := range pow |
If you only want the index, you can omit the second variable.
1 | for i := range pow // same as for i,_ := range pow |
Maps
(additional reference: https://www.callicoder.com/golang-maps/)
A map is an unordered collection of key-value pairs. It maps keys to values. The keys are unique within a map while the values may not be.
Declaring a Map
(additional reference: https://www.callicoder.com/golang-maps/)
A map is declared using the following syntax -
1 | var m map[KeyType]ValueType |
For example, Here is how you can declare a map of string keys to int values -
1 | var m map[string]int |
The zero value of a map is nil
(like null
in other languages). A nil
map has no keys, nor can keys be added. So, any attempt to add keys to a nil map will result in a runtime error.
Let’s see an example -
1 | package main |
1 | # Output |
If you uncomment the statement m["one hundred"] = 100
, the program will generate the following error -
1 | panic: assignment to entry in nil map |
It is, therefore, necessary to initialize a map before adding items to it.
Initializing a Map
There are two ways to initialize a map. You can either use the make
function, or you can use a map literal to initalize a map.
Initializing a map using the built-in make()
function
You can initialize a map using the built-in make()
function. You just need to pass the type of the map to the make()
function as in the example below. The function will return an initialized and ready to use map -
1 | // Initializing a map using the built-in make() function |
Let’s see a complete example -
1 | package main |
1 | # Output |
Initializing a map using a map literal
A map literal is a very convenient way to initialize a map with some data. An empty map using a map literal by leaving the curly braces empty -
1 | // Initialize an empty map |
The above statement is functionally identical to using the make()
function.
To initialize with some values, you just need to pass the key-value pairs separated by colon inside curly braces like this:
1 | var m = map[string]int{ |
Adding items (key-value pairs) to a map
You can add new items to an initialized map using the following syntax -
1 | m[key] = value |
The following example initializes a map using the make()
function and adds some new items to it -
1 | package main |
1 | # Output |
If you try to add a key that already exists in the map, then it will simply be overridden by the new value.
Retrieving the value associated with a given key in a map
You can retrieve the value assigned to a key in a map using the syntax m[key]
. If the key exists in the map, you’ll get the assigned value. Otherwise, you’ll get the zero value of the map’s value type.
Let’s check out an example to understand this -
1 | package main |
1 | # Output |
In the above example, since the key "Jack"
doesn’t exist in the map, we get the zero value of the map’s value type. Since the map’s value type is string, we get " "
.
Unlike other languages, we do not get a runtime error in Golang if the key doesn’t exist in the map.
But what if you want to check for the existence of a key?
Checking if a key exists in a map
When you retrieve the value assigned to a given key using the syntax map[key]
, it returns an additional boolean value as well which is true
if the key exists in the map, and false
if it doesn’t exist.
So you can check for the existence of a key in a map by using the following two-value assignment -
1 | value, ok := m[key] |
The boolean variable ok will be true
if the key exists, and false
otherwise.
Consider the following map for example. It maps employeeIds to names -
1 | var employees = map[int]string{ |
Accessing the key 1001 will return “Rajeev” and true, since the key 1001 exists in the map -
1 | name, ok := employees[1001] // "Rajeev", true |
However, If you try to access a key that doesn’t exist, then the map will return an empty string ""
(zero value of strings), and false -
1 | name, ok := employees[1010] // "", false |
Deleting a key from a map
You can delete a key from a map using the built-in delete()
function. The syntax looks like this -
1 | // Delete the `key` from the `map` |
The delete()
function doesn’t return any value. Also, it doesn’t do anything if the key doesn’t exist in the map.
Here is a complete example -
1 | package main |
1 | # Output |
Maps are reference types
Maps are reference types. When you assign a map to a new variable, they both refer to the same underlying data structure. Therefore changes done by one variable will be visible to the other -
1 | package main |
1 | # Output |
The same concept applies when you pass a map to a function. Any changes done to the map inside the function is also visible to the caller.
Iterating over a map
You can iterate over a map using range form of the for loop. It gives you the key, value pair in every iteration -
1 | package main |
1 | # Output |
Note that, A map is an unordered collection and therefore the iteration order of a map is not guaranteed to be the same every time you iterate over it.
Function Values
Functions are values too. They can be passed around just like other values.
Function values may be used as function arguments and return values.
1 | package main |
Function closures
Go functions may be closures. A closure is a returned function value that references variables from outside its body. The function may access and assign to the referenced variables; in this sense the function is “bound” to the variables.
For example, the adder function returns a closure. Each closure is bound to its own sum variable.
1 | package main |
Methods
(additional reference: https://www.callicoder.com/golang-methods-tutorial/)
Technically, Go is not an object-oriented programming language. It doesn’t have classes, objects, and inheritance.
However, Go has types
. And, you can define methods
on types. This allows for an object-oriented style of programming in Go.
A method is nothing but a function with a special receiver argument.
The receiver argument has a name
and a type
. It appears between the func keyword and the method name -
1 | func (receiver Type) MethodName(parameterList) (returnTypes) { |
The receiver can be either a struct type or a non-struct type.
Let’s see an example to understand how to define a method on a type and how to invoke such a method -
1 | package main |
Methods with Pointer receivers
All the examples that we saw in the previous sections had a value receiver.
With a value receiver, the method operates on a copy of the value passed to it. Therefore, any modifications done to the receiver inside the method is not visible to the caller.
Go also allows you to define a method with a pointer receiver -
1 | // Method with pointer receiver |
Methods with pointer receivers can modify the value to which the receiver points. Such modifications are visible to the caller of the method as well -
1 | package main |
1 | Point p = {3 4} |
Function equivalent of a Method
Just like methods with value receivers, we can also write methods with pointer receivers as functions. The following example shows how to rewrite the previous example with functions -
1 | package main |
Methods and Pointer indirection
Methods with Pointer receivers vs Functions with Pointer arguments
A method with a pointer receiver can accept both a pointer and a value as the receiver argument. But, a function with a pointer argument can only accept a pointer -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35package main
import (
"fmt"
)
type Point struct {
X, Y float64
}
// Method with Pointer receiver, actually BOTH value and pointer will be accepted
func (p *Point) Translate(dx, dy float64) {
p.X = p.X + dx
p.Y = p.Y + dy
}
// Function with Pointer argument, ONLY a pointer will be accepted
func TranslateFunc(p *Point, dx, dy float64) {
p.X = p.X + dx
p.Y = p.Y + dy
}
func main() {
p := Point{3, 4}
ptr := &p
fmt.Println("Point p = ", p)
// Calling a Method with Pointer receiver
p.Translate(2, 6) // Valid
ptr.Translate(5, 10) // Valid
// Calling a Function with a Pointer argument
TranslateFunc(ptr, 20, 30) // Valid
TranslateFunc(p, 20, 30) // Not Valid
}Notice how both
p.Translate()
andptr.Translate()
calls are valid. Since Go knows that the methodTranslate()
has a pointer receiver, It interprets the statementp.Translate()
as(&p).Translate()
. It’s a syntactic sugar provider by Go for convenience.Methods with Value receivers vs Functions with Value arguments
A method with a value receiver can accept both a value and a pointer as the receiver argument. But, a function with a value argument can only accept a value -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33package main
import (
"fmt"
)
// Struct type - `Point`
type Point struct {
X, Y float64
}
func (p Point) IsAbove(y float64) bool {
return p.Y > y
}
func IsAboveFunc(p Point, y float64) bool {
return p.Y > y
}
func main() {
p := Point{0, 4}
ptr := &p
fmt.Println("Point p = ", p)
// Calling a Method with Value receiver
p.IsAbove(1) // Valid
ptr.IsAbove(1) // Valid
// Calling a Function with a Value argument
IsAboveFunc(p, 1) // Valid
IsAboveFunc(ptr, 1) // Not Valid
}In the above example, both
p.IsAbove()
andptr.IsAbove()
statements are valid. Go knows that the method IsAbove() has a value receiver. So for convenience, It interprets the statementptr.IsAbove()
as(*ptr).IsAbove()
Method definition restrictions
Note that, For you to be able to define a method on a receiver, the receiver type must be defined in the same package.
Go doesn’t allow you to define a method on a receiver type that is defined in some other package (this includes built-in types such as int as well).
In all the previous examples, the structs and the methods were defined in the same package main
. Therefore, they worked. However, if you try to define a method on a type defined in some other package, the compiler will complain.
Let’s see an example. Consider the following package hierarchy -
1 | src/example |
Here are the contents of person.go
-
1 | package model |
Interface
(additional reference: https://www.callicoder.com/golang-interfaces/)
An interface in Go is a type defined using a set of method signatures. The interface defines the behavior for similar type of objects.
For example, Here is an interface that defines the behavior for Geometrical shapes:
1 | // Go Interface - `Shape` |
An interface is declared using the type
keyword, followed by the name of the interface and the keyword interface. Then, we specify a set of method signatures inside curly braces.
Implementing an interface in Go
To implement an interface, you just need to implement all the methods declared in the interface.
Go Interfaces are implemented implicitly
Unlike other languages like Java, you don’t need to explicitly specify that a type implements an interface using something like an implements keyword. You just implement all the methods declared in the interface and you’re done.
Here is one Struct types that implement the Shape
interface:
1 | // Go Interface - `Shape` |
1 | // Struct type `Rectangle` - implements the `Shape` interface by implementing all its methods. |
Using an interface type with concrete values
An interface in itself is not that useful unless we use it with a concrete type that implements all its methods.
Let’s see how an interface can be used with concrete values.
- An interface type can hold any value that implements all its methods
1 | // this means you can do this |
- Using Interface types as arguments to functions
1 | // Generic function to calculate the total area of multiple shapes of different types |
- Using Interface types as fields
1 | // Interface types can also be used as fields |
Interface values: How does an interface type work with concrete values?
Under the hood, an interface value can be thought of as a tuple consisting of a value and a concrete type:
1 | // interface |
Let’s see an example to understand more:
1 | package main |
1 | # Output |
Checkout the output of the above program and notice how the variable s has information about the value as well as the type of the Shape that is assigned to it.
When we call a method on an interface value, a method of the same name on its underlying type (actual type) is executed.
For example, in the above program, when we call the method Area()
on the variable s, it executes the Area()
method of its underlying type/actual type.
What if you have a nil value
for an Interface?
If the concrete value inside the interface itself is nil
(but its type is not nil
), the method will be called with a nil receiver
.
In some languages this would trigger a null pointer exception, but in Go it is common to write methods that gracefully handle being called with a nil receiver
(as with the method M
in this example.)
Note that an interface value that holds a nil concrete value is itself non-nil.
1 | package main |
What if you have a nil type
for an Interface?
You cannot create a variable with a value but not type. So the only case is that you have neither value nor type for an interface variable.
Calling a method on such a nil interface
is a run-time error because there is no type inside the interface tuple to indicate which concrete method to call.
1 | package main |
The empty Interface
The interface type that specifies zero methods is known as the empty interface:
1 | interface{} |
An empty interface may hold values of any type. (Every type implements at least zero methods.)
Empty interfaces are used by code that handles values of unknown type. For example, fmt.Print
takes any number of arguments of type interface{}
.
To use this, you don’t need to explicitly creat an interface but just use it in place:
1 | var i interface{} // this variable will be of anyType |
For example:
1 | package main |
Type Assertions
Type assertion provides access to and assess an interface value’s underlying concrete value.
1 | t := i.(T) // T is the type that I want to assert variable i has |
This statement asserts that the interface value i
holds the concrete type T
and assigns the underlying T
value to the variable t
.
If i
does not hold a T
, the statement will trigger a panic.
To test whether an interface value holds a specific type, a type assertion can return two values: the underlying type value and a boolean value that reports whether the assertion succeeded.
1 | t, ok := i.(T) |
If i
holds a T
, then t will be the underlying value and ok
will be true
.
If not, ok will be false and t
will be the zero value of type T
, and no panic occurs.
Note the similarity between this syntax and that of reading from a map/array/slice.
Type switches
A type switch is a construct that permits several type assertions in series.
A type switch is like a regular switch statement, but the cases in a type switch specify types (not values), and those values are compared against the type of the value held by the given interface value.
1 | switch v := i.(type) { |
The declaration in a type switch has the same syntax as a type assertion i.(T)
, but the specific type T
is replaced with the keyword type
. (This only works if you use a switch statement.)
Common Interfaces
Stringer Interface
One of the most ubiquitous interfaces is Stringer defined by the fmt
package. This could be understood similar to the String toString()
method of a Java object, which would be invoked when println()
is called.
1 | type Stringer interface { |
A Stringer
is a type that can describe itself as a string. The fmt
package (and many others) look for this interface to print values.
1 | package main |
Error Interface
Go programs express error state with error values.
The error
type is a built-in interface similar to fmt.Stringer
:
1 | type error interface { |
(As with fmt.Stringer
, the fmt
package looks for the error interface when printing error values.)
Functions often return an error
value, and calling code should handle errors by testing whether the error equals nil
.
1 | i, err := strconv.Atoi("42") |
A nil
error denotes success; a non-nil error denotes failure.
Reader Interface
The io
package specifies the io.Reader
interface, which represents the read end of a stream of data.
The Go standard library contains many implementations of these interfaces, including files, network connections, compressors, ciphers, and others.
The io.Reader
interface has a Read
method:
1 | func (T) Read(b []byte) (n int, err error) |
Read populates the given byte slice with data and returns the number of bytes populated and an error value. It returns an io.EOF
error when the stream ends.
The following example consumes output 8 bytes at a time:
1 | package main |
Go Routines
(additional reference: https://www.geeksforgeeks.org/goroutines-concurrency-in-golang/)
Go language provides a special feature known as a Goroutines
. A Goroutine is a function or method which executes independently and simultaneously in connection with any other Goroutines present in your program. Or in other words, every concurrently executing activity in Go language is known as a Goroutines. You can consider a Goroutine like a light weighted thread. The cost of creating Goroutines is very small as compared to the thread.
Every program contains at least a single Goroutine and that Goroutine is known as the main Goroutine. All the Goroutines are working under the main Goroutines if the main Goroutine terminated, then all the goroutine present in the program also terminated. Goroutine always works in the background.
Creating a Goroutine
You can create your own Goroutine simply by using go
keyword as a prefixing to the function or method call as shown in the below syntax:
1 | func name(){ |
Example:
1 | // Go program to illustrate |
1 | # Output: |
Notice that this does not work as expected. This is because that when you call go display("Welcome")
, sometime is needed to initialize and execute the Goroutine. However, this time does not block the program from going forward. This means that before go display("Welcome")
happens, the program executes the normal function call display("GeeksforGeeks")
and finishes before go display("Welcome")
happens. Once display("GeeksforGeeks")
finished executing, the program finishes, so go display("Welcome")
never actually executes.
To fix this, (there are many ways to fix this) you could give some time for that Goroutine to execute before the program exits:
1 | func main() { // though this technically does not have the effect we wanted |
1 | # Output |
To achieve the effect of concurrency that we wanted, we should:
1 | package main |
1 | # Output: |
Anonymous Goroutine
In Go language, you can also start Goroutine for an anonymous function or in other words, you can create an in-place Goroutine simply by using go
keyword as a prefix of that function as shown in the below Syntax:
1 | // Anonymous function call |
Example:
1 | // Go program to illustrate how |
1 | # Output: |
Channels
(additional reference: https://www.geeksforgeeks.org/channel-in-golang/?ref=lbp)
Channels are a typed conduit through which you can send and receive values with the channel operator, <-
. (Like a Java BlockingQueue
)
Creating a Channel
In Go language, a channel is created using chan
keyword and it can only transfer data of the same type, different types of data are not allowed to transport from the same channel.
1 | var Channel_name chan Type |
You can also create a channel using make()
function using a shorthand declaration.
1 | channel_name:= make(chan Type) |
Example:
1 | // Go program to illustrate |
1 | Output: |
Sending and Receiving Data from a Channel
In Go language, channel work with two principal operations one is sending and another one is receiving, both the operations collectively known as communication. And the direction of <-
operator indicates whether the data is received or send. In the channel, the send and receive operation block until another side is not ready by default. It allows goroutine to synchronize with each other without explicit locks or condition variables.
1 | ch <- v // Send v to channel ch. |
(The data flows in the direction of the arrow.)
Like maps and slices, channels must be created and initialized before use:
1 | ch := make(chan int) |
By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.
Closing a Channel
You can also close a channel with the help of close()
function. This is an in-built function and sets a flag which indicates that** no more value will be sent or read to this channel**.
1 | close(ch) |
Also, you can check the channel is open or close with the help of the given syntax:
1 | ele, ok:= <- Mychannel // ele would be the element read from it |
Here, if the value of ok
is true
which means the channel is open so, read operations can be performed. And if the value of is false which means the channel is closed so, read operations are not going to perform.
Example:
1 | // Go program to illustrate how |
1 | # Output: |
You can also use an enhanced for loop to interate over the values stored in a channel until it is closed:
1 | for item := range Chnl { |
Example:
1 | // Go program to illustrate how to |
1 | # Output: |
Buffered Channels
Channels can be buffered. Provide the buffer length as the second argument to make to initialize a buffered channel:
1 | ch := make(chan int, 100) // means it can buffer/store 100 elements |
Sends to a buffered channel block only when the buffer is full. Receives block when the buffer is empty.
You can find out the elements stored and the buffer length/capacity of a channel with len()
and cap()
function:
1 | len(ch) // returns the number of elements currently stored in a channel ch |
Select and Case Statement in Channel
Select and case statement in Channel: In go language, select statement is just like a switch statement without any input parameter. This select statement is used in the channel to perform a single operation out of multiple operations provided by the case block.
A select blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.
1 | package main |
However, you can also use the default
case in a select is run if no other case is ready. This means you can use a default case to try a send or receive without blocking by placing it in a loop:
1 | // the syntax |
Example:
1 | package main |
sync.Mutex
But what if we don’t need communication? What if we just want to make sure only one goroutine can access a variable at a time to avoid conflicts?
This concept is called mutual exclusion, and the conventional name for the data structure that provides it is mutex. (This can be thought of being the synchronized
keyword in Java).
Go’s standard library provides mutual exclusion with sync.Mutex
and its two methods:
1 | Lock() // locks to only allow one goroutine to execute whatever lies below |
We can define a block of code to be executed in mutual exclusion by surrounding it with a call to Lock
and Unlock
as shown on the Inc
method.
We can also use defer
to ensure the mutex will be unlocked as in the Value method.
1 | package main |