The Go Language
Getting Started
First, download go for your machine, or using a package manager. There's installation instructions if you want to do any additional configuration.
If you already have Go installed on your computer, you can use it to install other versions of Go.
go install golang.org/dl/go1.17@latest && go1.17 download
Be sure to remember to prepend ~/sdk/go1.17/bin to your shell's command search path, as this is where the binary executable will end up being installed in.
For the latest version of Go, on the development branch, you can follow a similar process using gotip.
go install golang.org/dl/gotip@latest && gotip download
You can actually check out the official tour of Go, as well, which you can run locally.
Install the Go tour:
go install golang.org/x/website/tour@latest
This will install the tour in $GOPATH/bin.
Your First Program
A heads up, oddly enough, the type comes after the variable name, something I learned from reading an article on Go's declaration syntax
Strings
Creating a string representation of an interface:
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("The person named %v is %v years old", p.Name, p.Age)
}
Packages
Programs in go are made up of multiple packages, and will run starting from the code defined in the main package.
Slices
Slices are a bit tricky, it is explained very well in Rob Pike's blog post on slices.
Also, felt this was worth writing down for my own safe-keeping:
It is idiomatic to use a pointer receiver for a method that modifies a slice.
--Rob Pike
len()
For locally defined slices the length is cached, so there is no runtime overhead for calls to len(a) provided that a is not defined beyond the local scope. For globally defined slices, the length is not cached, as its length can be modified elsewhere, which requires it to be re-evaluated each time a call to len(a) is made .
Arrays
Arrays are used less often than slices, but sometimes you find yourself in a situation where you'd like to create a fixed size array, using the values contained within a slice. Here is the idiomatic way for you to do precisely that:
var fixed [3]int
sliced := []int{1, 2, 3, 5, 8, 13}
copy(fixed[:], sliced)
This syntax takes advantage of the faxt that copy will only copy the minimum of len(src) and len(dst) bytes.
Scanners
If you need to read a file in line by line, the most idiomatic way to do so is by using bufio.Scanner
file := os.Open("file.txt")
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
var txtlines []string
for scanner.Scan() {
txtlines = append(txtlines, scanner.Text())
}
file.Close()
for _, eachline := range txtlines {
fmt.Println(eachline)
}
JSON
Make an HTTP request, receive JSON in the body of the response, and unmarshall that JSON into a Go struct named result:
response, err := http.Get("example.com/api/gimmejson")
if err != nil {
return
}
defer func(Body io.ReadCloser) {
err = Body.Close()
if err != nil {
return
}
}(response.Body)
data, err := io.ReadAll(response.Body)
if err != nil {
return
}
err = json.Unmarshal(data, result)
return
Open a file for reading:
file, err := os.Open("file.txt")
if err != nil {
return
}
os.Stdin and os.Stdout are the standard input and output streams, which means they can be passed in to functions like os.Read and os.Write just as you would with any other parameter.
Installing Go Modules
You can install Go code using the install subcommand.
# Option 1
go install example.com/user/hello@latest
# Option 2
go install .
# Option 3
go install
For convenience, go commands accept paths relative to the working directory, and default to the package in the current working directory if no other path is given. So in our working directory, the commands shown above are all equivalent:
Defer, Panic, and Recover
There's an interesting article about callback functions using defer, but I'll leave that for another time.
Vim Plugin
If you want to edit your Go projects in Vim, there's a very healthy ecosystem to support you.
" Install the libraries needed for `vim-go`
:GoInstallBinaries
" Getting help
:help vim-go
I've included some useful commands below:
" Run the code in the current buffer
:GoRun
" Compile the code
:GoBuild
" Install the coe
:GoInstall
" Test the code
:GoTest
" Test a single function
:GoTestFunc
" See dependencies of the current package
:GoDeps
" See all source files in cwd
:GoFiles
" Rename an identifier
:GoRename
" Format the document according to the go style guide
:GoFmt
" Resolve all needed package imports & remove all unused packages
:GoImports
" Import the package `math` #study #rise&grind
:GoImport math
" Drop the package `math` #jk2cool4school
:GoDrop math
" Pull up documentation for the function `Printf` package `fmt`
:GoDoc fmt Printf
Style
Below are some notes I took while reading "The Go Programming Language"
The letters of acronyms and initialisms like ASCII and HTML are always rendered in the same case, so you might want to call a function htmlEscape, HTMLEscape, or escapeHTML, but should avoid calling it escapeHtml.
Syntax and Semantics
A declaration names a program entity and specifies some or all of its properties. In Go, the four main types of declarations are var, const, type, and func, but every .go file begins with a package declaration, followed by import declarations, and finally, zero-or-more package-level declarations.
In Go, there is no such thing as an unitialized variable. If a value is not provided for a variable at its declaration, the variable will have its value initialized to the zero-value corresponding to that variable's underlying type.
The := operator performs short variable declaration. Unlike the = operator, which performs assignment, the := operator performs declaration, which is distinct from assignment.
The zero value for a pointer of any type is nil. If there is a variable p, which is a pointer type variable, the test p != nil is true if p points to a variable. Two pointers are equal if and only if they point to the same variable, or are both equal to nil.
The expression new(T) creates an unnamed variable of type T, initializes it to the zero-value of type T, and returns its address, which is a value of type *T.
Functions
Linked List
Note: The example below was written mainly just for practice. github.com/dustin/go-humanize provides a far more robust implementation of this functionality.
File I/O
Creating an empty file:
f, err := os.Create("file.txt")
if err != nil {
log.Fatal(err)
}
log.Println(f)
f.Close()
Truncating an existing file:
err := os.Truncate("file.txt", 100)
if err != nil {
log.Fatal(err)
}
This will truncate file.txt to its first 100 bytes, erasing all content beyond the 100th bytes stored in file.txt. Similarly, to truncate a file to be empty, you can use os.Truncate("file.txt", 0).
If file.txt is less than 100 bytes to begin with, the original contents will persist, and the remaining bytes are filled with the zero-value for a byte.
Getting metadata about a file:
info, err := os.Stat("file.txt")
if err != nil {
log.Fatal(err)
}
// Examples of what you can do with `info`
fmt.Println("File name:", info.Name())
fmt.Println("Size in bytes:", info.Size())
fmt.Println("Permissions:", info.Mode())
fmt.Println("Last modified:", info.ModTime())
fmt.Println("Is Directory: ", info.IsDir())
fmt.Printf("System interface type: %T\n", info.Sys())
fmt.Printf("System info: %+v\n\n", info.Sys())
Moving a file:
old := "dir1/old.txt"
new := "dir2/new.txt"
err := os.Rename(old, new)
if err != nil {
log.Fatal(err)
}
Deleting a file:
err := os.Remove("file.txt")
if err != nil {
log.Fatal(err)
}
Opening and closing a file:
// Open 'read.txt' in read-only mode.
f, err := os.Open("read.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
// Open 'write.txt' in write-only mode.
f, err = os.OpenFile("write.txt", os.O_WRONLY|os.O_CREATE, 0666)
Checking if a file exists:
info, err := os.Stat("file.txt")
if err != nil {
if os.IsNotExist(err) {
log.Fatal("file does not exist")
} else {
log.Fatal(err)
}
}
fmt.Println("file exists")
Checking file permissions:
info, err := os.Stat("file.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println("Permissions:", info.Mode())
fmt.Println(
"User has read access:",
info.Mode().Perm(os.FileMode(0400)),
)
fmt.Println(
"User has write access:",
info.Mode().Perm(os.FileMode(0200)),
)
fmt.Println(
"User has execute access:",
info.Mode().Perm(os.FileMode(0100)),
)
Changing the timestamps of a file:
// Change perrmissions using Linux style
err := os.Chmod("file.txt", 0777)
if err != nil {
log.Println(err)
}
// Change ownership
err = os.Chown("file.txt", os.Getuid(), os.Getgid())
if err != nil {
log.Println(err)
}
// Change timestamps
twoDaysFromNow := time.Now().Add(48 * time.Hour)
lastAccessTime := twoDaysFromNow
lastModifyTime := twoDaysFromNow
err = os.Chtimes("file.txt", lastAccessTime, lastModifyTime)
if err != nil {
log.Println(err)
}
Creating a hard link:
err := os.Link("file.txt", "hardlink.txt")
if err != nil {
log.Fatal(err)
}
Creating a symbolic link:
// Create a symlink
err = os.Symlink("file.txt", "symlink.txt")
if err != nil {
log.Fatal(err)
}
Changing the owner of a file:
err = os.Lchown("file.txt", os.Getuid(), os.Getgid())
if err != nil {
log.Fatal(err)
}
Copy a file:
// Open new file 'original.txt'
original, err := os.Open("original.txt")
if err != nil {
log.Fatal(err)
}
defer original.Close()
// Create new file 'replica.txt'
replica, err := os.Create("replica.txt")
if err != nil {
log.Fatal(err)
}
defer replica.Close()
// Copy the bytes to destination from source
_, err := io.Copy(replica, original)
if err != nil {
log.Fatal(err)
}
// Commit the file contents and flush memory to disk
err = replica.Sync()
if err != nil {
log.Fatal(err)
}
Seek to a position in a file:
file, _ := os.Open("test.txt")
defer file.Close()
// Offset is how many bytes to move
// Offset can be positive or negative
var offset int64 = 5
// Whence is the point of reference for offset
// 0 = Beginning of file
// 1 = Current position
// 2 = End of file
var whence int = 0
newPosition, err := file.Seek(offset, whence)
if err != nil {
log.Fatal(err)
}
fmt.Println("Just moved to 5:", newPosition)
// Go back 2 bytes from current position
newPosition, err = file.Seek(-2, 1)
if err != nil {
log.Fatal(err)
}
fmt.Println("Just moved back two:", newPosition)
// Find the current position by getting the
// return value from Seek after moving 0 bytes
currentPosition, err := file.Seek(0, 1)
fmt.Println("Current position:", currentPosition)
// Go to beginning of file
newPosition, err = file.Seek(0, 0)
if err != nil {
log.Fatal(err)
}
fmt.Println("Position after seeking 0,0:", newPosition)
Write to a file:
// Open a new file for writing only
file, err := os.OpenFile(
"test.txt",
os.O_WRONLY|os.O_TRUNC|os.O_CREATE,
0666,
)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Write bytes to file
byteSlice := []byte("Bytes!\n")
bytesWritten, err := file.Write(byteSlice)
if err != nil {
log.Fatal(err)
}
log.Printf("Wrote %d bytes.\n", bytesWritten)
Dump a byte slice to a file:
err := io.WriteFile("test.txt", []byte("Hi\n"), 0666)
if err != nil {
log.Fatal(err)
}
In the examples below, assume that file has already been opened and is ready to be used.
Read up to n bytes from a file:
// Read up to len(b) bytes from the File
// Zero bytes written means end of file
// End of file returns error type io.EOF
byteSlice := make([]byte, 16)
bytesRead, err := file.Read(byteSlice)
if err != nil {
log.Fatal(err)
}
log.Printf("Number of bytes read: %d\n", bytesRead)
log.Printf("Data read: %s\n", byteSlice)
Read exactly n bytes from a file:
// The file.Read() function will happily read a tiny file in to a large
// byte slice, but io.ReadFull() will return an
// error if the file is smaller than the byte slice.
byteSlice := make([]byte, 2)
numBytesRead, err := io.ReadFull(file, byteSlice)
if err != nil {
log.Fatal(err)
}
log.Printf("Number of bytes read: %d\n", numBytesRead)
log.Printf("Data read: %s\n", byteSlice)
Read at least n bytes from a file:
byteSlice := make([]byte, 512)
minBytes := 8
// io.ReadAtLeast() will return an error if it cannot
// find at least minBytes to read. It will read as
// many bytes as byteSlice can hold.
numBytesRead, err := io.ReadAtLeast(file, byteSlice, minBytes)
if err != nil {
log.Fatal(err)
}
log.Printf("Number of bytes read: %d\n", numBytesRead)
log.Printf("Data read: %s\n", byteSlice)
Read all bytes from a file:
data, err := io.ReadAll(file)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Data as hex: %x\n", data)
fmt.Printf("Data as string: %s\n", data)
fmt.Println("Number of bytes read:", len(data))
Note: os.File.Read(), io.ReadFull(), and io.ReadAtLeast() all require a fixed byte slice.
Read a file into memory:
// Read file to byte slice
data, err := io.ReadFile("file.txt")
if err != nil {
log.Fatal(err)
}
log.Printf("Data read: %s\n", data)
Use a buffered reader:
// Open 'file.txt' wrapped by a buffered reader.
file, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
br := bufio.NewReader(file)
// Get the next bytes without advancing the cursor.
data := make([]byte, 5)
data, err = br.Peek(5)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Peeked at 5 bytes: %s\n", data)
// Read in data and advance the cursor.
n, err := br.Read(data)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Bytes read: %d: %s\n", n, data)
// Read one byte.
b, err := br.ReadByte()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Read one byte: %c\n", b)
// Read up to and including delimiter
bts, err := br.ReadBytes('\n')
if err != nil {
log.Fatal(err)
}
fmt.Printf("Bytes read: %s\n", bts)
// Read text up to and including delimiter
line, err := br.ReadString('\n')
if err != nil {
log.Fatal(err)
}
fmt.Println("Bytes read: %s\n", line)
Use a buffered writer:
// Create a buffered writer from the file
bw := bufio.NewWriter(file)
// Write bytes to buffer.
written, err := bw.Write(
[]byte{65, 66, 67},
)
if err != nil {
log.Fatal(err)
}
log.Printf("Bytes written: %d\n", written)
// Write string to buffer.
// Also available are WriteRune() and WriteByte()
written, err = bw.WriteString("Buffered string\n")
if err != nil {
log.Fatal(err)
}
log.Printf("Bytes written: %d\n", written)
// Write the current contents of buffer from memory to disk.
bw.Flush()
Imports
import (
"bufio"
"bytes"
"io"
"os"
)
Templates
To do
Consider the following template file file.html, located within the directory tmpl:
<p>Hello, {{.Name}}! Welcome to {{.Location}}.</p>
The .Name and .Location variables can be replaced in a Go file using the template package:
type Doc struct {
Name string
Location string
}
func main() {
t, _ := template.ParseFiles("tmpl/file.html")
fmt.Println(dr.Name)
t.Execute(os.Stdout, dr)
}
The output should be:
<p>Hello, Doctor! Welcome to GitHub Hospital.</p>
Interfaces
To do
An interface defines a set of methods that, if implemented by a type, cause that type to be a valid instance of that interface.
Concurrency
channels, go routines,
Networking
Todo.
Reading/Writing
Todo.
Printing
Discovered in the article Effective Slices:
For maps, Printf and friends sort the output lexicographically by key.
When printing a struct, the modified format %+v annotates the fields of the structure with their names, and for any value the alternate format %#v prints the value in full Go syntax.
type T struct {
a int
b float64
c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
Produces the following output:
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}