Chapter 2 Program Structure
Exercise 2.1: Add types, constants, and functions to tempconv for processing temperatures in the Kelvin scale, where zero Kelvin is −273.15°C and a difference of 1K has the same magnitude as 1°C.
// tempconv.go
type Kelvin float64
func (k Kelvin) String() string { return fmt.Sprintf("%g°K", k) }
// conv.go
// KToC converts a Kelvin temperature to Celsius
func KToC(k Kelvin) Celsius { return Celsius(k - 273.15) }
Exercise 2.2: Write a general-purpose unit-conversion program analogous to cf that reads numbers from its command-line arguments or from the standard input if there are no arguments, and converts each number into units like temperature in Celsius and Fahrenheit, length in feet and meters, weight in pounds and kilograms, and the like.
// Copyright © [email protected]
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// See page 43.
//!+
// uc converts its numeric argument to Celsius and Fahrenheit, to Feet and Meter.
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"gopl.io/ch2/tempconv"
)
type Feet float64
type Meter float64
func (f Feet) String() string { return fmt.Sprintf("%g(ft)", f) }
func (m Meter) String() string { return fmt.Sprintf("%g(m)", m) }
func FToM(f Feet) Meter { return Meter(f * 0.3048) }
func MToF(m Meter) Feet { return Feet(m * 3.28083) }
func uc(t float64) {
// temperature
fmt.Println("Temperature Conversion:")
f := tempconv.Fahrenheit(t)
c := tempconv.Celsius(t)
fmt.Printf("%s = %s, %s = %s\n\n",
f, tempconv.FToC(f), c, tempconv.CToF(c))
// length
fmt.Println("Length Conversion:")
lf := Feet(t)
lm := Meter(t)
fmt.Printf("%s = %s, %s = %s\n\n",
lf, FToM(lf), lm, MToF(lm))
}
func main() {
if len(os.Args[1:]) > 0 {
for _, arg := range os.Args[1:] {
t, err := strconv.ParseFloat(arg, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "uc: %v\n", err)
os.Exit(1)
}
uc(t)
}
} else {
for {
input := bufio.NewReader(os.Stdin)
fmt.Fprintf(os.Stdout, "=> ")
s, err := input.ReadString('\n')
if err != nil {
fmt.Fprintf(os.Stderr, "uc: %v\n", err)
os.Exit(1)
}
// strip `\n`
t, err := strconv.ParseFloat(s[:len(s)-1], 64)
if err != nil {
fmt.Fprintf(os.Stderr, "uc: %v\n", err)
os.Exit(1)
}
uc(t)
}
}
}
//!-
Exercise 2.3: Rewrite PopCount to use a loop instead of a single expression. Compare the performance of the two versions. (Section 11.4 shows how to compare the performance of different implementations systematically.)
// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) (ret int) {
for i := 0; i < 8; i++ {
ret += int(pc[byte(x>>uint(i*8))])
}
return
}
Exercise 2.4: Write a version of PopCount that counts bits by shifting its argument through 64 bit positions, testing the right most bit each time. Compare its performance to the table-lookup version.
// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) (ret int) {
for i := 0; i < 64; i++ {
if x&1 == 1 {
ret++
}
x >>= 1
}
return ret
}
Exercise 2.5: The expression x&(x-1) clears the rightmost non-zero bit of x. Write a version of PopCountthat counts bits by using this fact, and assess its performance.
// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) (ret int) {
for x != 0 {
x = x & (x - 1)
ret++
}
return
}
Performance: lookup > x&(x-1) > shift (see popcount_test.go)
Notes
Constants | true false iota nil |
---|---|
Types | int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex128 complex64 bool type rune string err |
Functions | make len cap new append copy close delete complex real i-mage panic recover |
Exported: The case of the first letter of a name determines its visibility across package boundaries. If the name begins with an upper-case letter, it is exported, which means that it is visible and accessible outside of its own package and may be referred to by other parts of the program
Declaration: There are four major kinds of declarations: var,const,type, and func.
Var: A var declaration tends to be reserved for local variables that need an explicit type that differs from that of the initializer expression, or for when the variable will be assigned a value later and its initial value is unimportant.
Pointer: Variables are sometimes described asaddressablevalues. Expressions that denote variables are the only expressions to which the address-of operator & may be applied.
New: Thus new is only a syntactic convenience, not a fundamental notion. The new function is relatively rarely used because the most common unnamed variables are of struct types, for which the struct literal syntax (§4.4.1) is more flexible.
Lifetime: The lifetime of a variable is the interval of time during which it exists as the program executes. The lifetime of a variable is determined only by whether or not it is reachable. The lifetime of a package-level variable is the entire execution of the program. By contrast, local variables have dynamic lifetimes: a new instance is created each time the declaration statement is executed, and the variable lives on until it becomes unreachable, at which point its storage may be recycled. It is perfectly safe for a function to return the address of a local variable.
Escape: A compiler may choose to allocate local variables on the heap or on the stack but, perhaps sur- prisingly, this choice is not determined by whethervarornewwas used to declare the variable.
var global *int func f() { func g() { var x int y := new(int) global = &x } }
Here, x must be heap-allocated because it is still reachable from the variable global afterf has returned, despite being declared as a local variable; we say x escapes from f. Conversely, when g returns, the variable *y becomes unreachable and can be recycled. Since *y does not escape from g, it’s safe for the compiler to allocate *y on the stack, even though it was allocated with new. In any case, the notion of escaping is not something that you need to worry about in order to write correct code, though it’s good to keep in mind during performance optimization, since each variable that escapes requires an extra memory allocation.
Garbage collection is a tremendous help in writing correct programs, but it does not relieve you of the burden of thinking about memory. You don’t need to explicitly allocate and free memory, but to write efficient programs you still need to be aware of the lifetime of variables. For example, keeping unnecessary pointers to short-lived objects within long-lived objects, especially global variables, will prevent the garbage collector from reclaiming the short-lived objects.
Type: A type declaration defines a new named type that has the same underlying type as an existing type. Named types also make it possible to define new behaviors(method) for values of the type.
Package: Each package serves as a separate name space for its declarations. Package-level names like the types and constants declared in one file of a package are visible to all the other files of the package, as if the source code were all in a single file.
Import Path: The language specification doesn’t define where these strings come from or what they mean; it’s up to the tools to interpret them. When using the go tool (Chapter 10), an import path denotes a directory containing one or more Go source files that together make up the package.
Package Name: which is the short (and not necessarily unique) name that appears in its package declaration. By convention, a package’s name matches the last segment of its import path, making it easy to predict that the package name of gopl.io/ch2/tempconv is tempconv.
Note: It is an error to import a package and then not refer to it.
Package Initialization: Package initialization begins by initializing package-level variables in the order in which they are declared, except that dependencies are resolved first. If the package has multiple .go files, they are initialized in the order in which the files are given to the compiler; the go tool sorts .go files by name before invoking the compiler.
var a = b+c // a initialized third, to 3
var b = f() // b initialized second, to 2, by calling f
var c = 1 // c initialized first, to 1
func f() int { return c + 1 }
Example:
// a.go // b.go // c.go
package main package main package main
var a = b var b = c var c = a
// main.go
package main
import "fmt"
func main() {
fmt.Println(a, b, c)
}
// go build:
// a.go:3:5: typechecking loop involving a = b
Typechecking not initialization!
which means go compiler can't resolve the type of a,b,c!
Give a/b/c int type, try Again:
// a.go // b.go // c.go
package main package main package main
var a int = b var b int = c var c int = a
// main.go remains the same as above
// go build
// a.go:3:5: initialization loop:
go compiler refuse to compile, because of the initialization loop condition.
Scope: The scope of a declaration is the part of the source code where a use of the declared name refers to that declaration.
Scope vs Lifetime: Don’t confuse scope with lifetime. The scope of a declaration is a region of the program text; it is a compile-time property. The lifetime of a variable is the range of time during execution when the variable can be referred to by other parts of the program; it is a run-time property.
Block:A syntactic blockis a sequence of statements enclosed in braces like those that surround the body of a function or loop. A name declared inside a syntactic block is not visible outside that block.
Lexical Block:generalize this notion of blocks to include other groupings of declarations that arenot explicitly surroundedby braces in the source code; we’ll call them alllexical blocks. There is a lexical block for theentire source code, called the universe block; for eachpackage; for eachfile; for eachfor,if, andswitch statement; for each_case_in a switch or select statement; and, of course, for each explicit syntactic block.
For loop:The for loop above creates two lexical blocks: theexplicit block for the loop body, and animplicit blockthat additionally encloses the variables declared by the initialization clause, such asi. The scope of a variable declared in the implicit block is the condition, post-statement (i++), and body of the for statement.
// The example below also has three variables named x, each declared in a // different block—one in the function body, one in the for statement’s // block, and one in the loop body—but only two of the blocks are explicit: func main() { x := "hello" for _, x := range x { x := x + 'A' - 'a' fmt.Printf("%c", x) // "HELLO" (one letter per iteration) } }
At the package level, the order in which declarations appear has no effect on their scope, so a declaration may refer to itself or to another that follows it, letting us declare recursive or mutually recursive types and functions. The compiler will report an error if a constant or variable declaration refers to itself, however.