The Basics: Part I
Variables
Simple declarations
var x int = 42
var (
y = 2
z = 2.01
)
The short variable declaration operator
x := 42
This is designed to allow for concise declaration and initialization of variables within the scope of a function.
Debugging method: printing out a variable’s type and value:
x := 42.0
fmt.Printf("%T %[1]v", x)
The output would be float64 42
. In Go’s fmt
package, the notation []
is for specifying argument indexes. In this case, the [1]
in the formatting verb %v
makes it use the first argument provided to %T
.
Initialization
In Go, all variables are initialized upon declaration, and, by default, “zero”.
Numerical types get 0, string gets ""
, an empty string with the length of 0. bool
gets false
, and everything else gets nil
.
Constants
Only numbers, strings and booleans can be constants and they are immutable.
const (
a = 1
b float64 = 2.0
c = "c"
d = a == b // false
)
Program: calculating averages
var sum float64
var n int
for {
var val float64
if _, err := fmt.Scanln(&val); err != nil {
break
}
sum += val
n++
}
fmt.Printf("The average is %v", sum/float64(n))
To end the for-loop and get the result, simply hit control+D(EOF) to cause an error.
Strings
In Go, these types are related to strings: byte
(synonym for uint8
), rune
(synonym for int32
for characters, 4 bytes) and string, which is immutable.
Strings in Go are all Unicode. They are,
- physically, a sequence of bytes(UTF-8 encoding).
- logically, a sequence of runes(Unicode).
Bytes vs. Runes & String length
To compare, we can test with the code below.
s := "Hello, 世界"
fmt.Printf("%T %[1]v %d\n", []rune(s), len([]rune(s)))
fmt.Printf("%T %[1]v %d\n", []byte(s), len([]byte(s)))
fmt.Println(len(s))
// Output
[]int32 [72 101 108 108 111 44 32 19990 30028] 9
[]uint8 [72 101 108 108 111 44 32 228 184 150 231 149 140] 13
13
We are casting the string to slice of runes and bytes, then printing out their values.
The string cast to runes is a slice of the Unicode code points for each character in the string, and the string cast to bytes is a slice of the byte values for each character, UTF-8 encoded.
And, based on the output, the length of a string is the length of a slice of bytes that’s necessary to encode the string in UTF-8.
String structure
In Go, the internal string representation is a pointer and a length(a descriptor).
Strings can share underlying storages. For example:
s := "hello, world"
hello := s[:5]
world := s[7:]
hello
and world
are substrings in string s
. When creating these strings, part of the string s
is indexed, and they point to the same characters in memory and can be reused.
When slicing a string using
s[start:end]
, The start index is inclusive, while the end index is exclusive.
Also, strings are passed by reference, thus they are not copied.
func main() {
s := "the quick brown fox"
pluralize(s)
fmt.Println(s)
}
func pluralize(str string) {
str += "es"
}
However, this does NOT work and will print the quick brown fox
.
Although strings are passed by reference in Go, they are still immutable. str += "es"
creates a new string in memory and points str
to it, while s
still points to the original value.
Program: Replacing substring
if len(os.Args) < 3 {
_, _ = fmt.Fprintln(os.Stderr, "Invalid syntax.")
os.Exit(-1)
}
var s string
old, new := os.Args[1], os.Args[2]
scan := bufio.NewScanner(os.Stdin)
for scan.Scan() {
s += scan.Text()
}
fmt.Println(strings.ReplaceAll(s, old, new))
The program is fairly simple.
Running the program, instead of typing the input, we could also supply a text file like this:
go run . <search> <replace> < textfile
or, like this:
cat textfile | go run . <search> <replace>
Composite Types
Arrays
Arrays are typed by size, and passed by value.
var a [3]int{0, 0, 0}
func do (a [3]int) {
a[0] = 1
}
do(a)
fmt.Println(a) // {0, 0, 0} (unchanged)
Slices
Much like strings, slices have descriptors(pointer and length), and they point to somewhere in the memory. Slices are passed by reference.
A slice has an array behind it(a pointer that points to a backing array), and is copied when it outgrows the array. Similarly, when it’s modified, another chunk of memory is allocated, then assigned back to the original slice with a modified descriptor.
var a = []int{1, 2}
a = append(a, 3)
b := a // {1, 2, 3}
By b := a
, b
becomes an alias of a
. The a
’s descriptor(not value) is copied to b
, and they point to the same address in memory and share storage. And, also because slices are mutable, we have:
func do(a []int) {
a[0]--
}
do(b) // a: {0, 2, 3}
b[0]-- // a: {-1, 2, 3}
a[0] == -1 // true
“The off-by-one bug”
Slices are indexed like [8:11], but the end index is exclusive(written [8, 11) in math). I found the figure below help a lot.
Slices vs. Arrays
Most Go APIs takes slices as inputs, not arrays.
Slices | Arrays |
---|---|
Variable length | Fixed length |
Passed by reference | Copied and passed by value |
Not comparable | Comparable (==) |
Cannot be used as map key | Can be used as map key |
Maps
In Go, the type used for a map key must be comparable, that’s not slices or functions, and also not maps, maps cannot be compared.
Maps are passed by reference(descriptor).
var m map[string]int // nil
fmt.Println(m["the"]) // 0
m["the"] = 1 // PANIC
This declares a map variable without creating a map. m
is a nil map. Nil maps can be read from, but cannot be assigned any values, or it will panic.
p := make(map[string]int) // or: p := map[string]int{}
m = p
m["the"] = 1
fmt.Println(p["the"]) // 1
n = map[string]int{
"the": 1
}
m == n // Syntax Error
Maps have a special two-result lookup function, the second return value indicates if the key is defined.
if w, ok := m["the"]; ok {
// w is not the default value
}
Nil pointers
nil
is a predeclared identifier which represents the concept of absence. nil
is generally safe for built-in functions in Go.
Link: GopherCon 2016: Francesc Campoy - Understanding nil
Program: word count
scan := bufio.NewScanner(os.Stdin)
scan.Split(bufio.ScanWords)
words := map[string]int{}
for scan.Scan() {
words[scan.Text()]++
}
type kv struct {
key string
val int
}
var ss []kv
for word, count := range words {
ss = append(ss, kv{word, count})
}
sort.Slice(ss, func(i, j int) bool {
return ss[i].val > ss[j].val
})
for _, kv := range ss {
fmt.Println(kv.key, kv.val)
}
fmt.Println(len(ss), "unique words.")
Next: The Basics: Part II