From 30090035dea568fd70cbcef194a06fbe32ae2199 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 28 Oct 2015 14:20:59 -0400 Subject: [PATCH] Update to gobook@c6d7a22edd03e7738c38d5baa2174ff325d8d863 --- ch1/lissajous/main.go | 21 ++- ch10/cross/main.go | 19 +++ ch10/jpeg/main.go | 52 +++++++ ch11/echo/echo.go | 41 +++++ ch11/echo/echo_test.go | 45 ++++++ ch11/storage1/storage.go | 43 ++++++ ch11/storage2/quota_test.go | 56 +++++++ ch11/storage2/storage.go | 51 +++++++ ch11/word1/word.go | 21 +++ ch11/word1/word_test.go | 43 ++++++ ch11/word2/word.go | 29 ++++ ch11/word2/word_test.go | 148 ++++++++++++++++++ ch12/display/display.go | 90 +++++++++++ ch12/display/display_test.go | 259 ++++++++++++++++++++++++++++++++ ch12/format/format.go | 44 ++++++ ch12/format/format_test.go | 24 +++ ch12/methods/methods.go | 29 ++++ ch12/methods/methods_test.go | 49 ++++++ ch12/params/params.go | 90 +++++++++++ ch12/search/main.go | 60 ++++++++ ch12/sexpr/decode.go | 162 ++++++++++++++++++++ ch12/sexpr/encode.go | 97 ++++++++++++ ch12/sexpr/pretty.go | 183 ++++++++++++++++++++++ ch12/sexpr/sexpr_test.go | 74 +++++++++ ch13/bzip/bzip2.c | 3 + ch13/bzip/bzip2.go | 3 + ch13/bzip/bzip2_test.go | 1 + ch13/bzipper/main.go | 4 + ch13/equal/equal.go | 127 ++++++++++++++++ ch13/equal/equal_test.go | 133 ++++++++++++++++ ch13/unsafeptr/main.go | 38 +++++ ch2/boiling/main.go | 22 +++ ch2/cf/main.go | 32 ++++ ch2/echo4/main.go | 27 ++++ ch2/ftoc/main.go | 22 +++ ch2/popcount/main.go | 31 ++++ ch2/popcount/popcount_test.go | 83 ++++++++++ ch2/tempconv/conv.go | 16 ++ ch2/tempconv/tempconv.go | 23 +++ ch2/tempconv0/celsius.go | 27 ++++ ch2/tempconv0/tempconv_test.go | 45 ++++++ ch3/basename1/main.go | 44 ++++++ ch3/basename2/main.go | 36 +++++ ch3/comma/main.go | 40 +++++ ch3/mandelbrot/main.go | 84 +++++++++++ ch3/netflag/netflag.go | 30 ++++ ch3/printints/main.go | 33 ++++ ch3/surface/main.go | 62 ++++++++ ch4/append/main.go | 79 ++++++++++ ch4/autoescape/main.go | 30 ++++ ch4/charcount/main.go | 56 +++++++ ch4/dedup/main.go | 33 ++++ ch4/embed/main.go | 46 ++++++ ch4/github/github.go | 35 +++++ ch4/github/search.go | 53 +++++++ ch4/graph/main.go | 43 ++++++ ch4/issues/main.go | 52 +++++++ ch4/issueshtml/main.go | 52 +++++++ ch4/issuesreport/main.go | 89 +++++++++++ ch4/movie/main.go | 104 +++++++++++++ ch4/nonempty/main.go | 47 ++++++ ch4/rev/main.go | 60 ++++++++ ch4/sha256/main.go | 25 +++ ch4/treesort/sort.go | 50 ++++++ ch4/treesort/sort_test.go | 23 +++ ch5/defer1/defer.go | 48 ++++++ ch5/defer2/defer.go | 51 +++++++ ch5/fetch/main.go | 54 +++++++ ch5/findlinks1/main.go | 3 + ch5/findlinks2/main.go | 69 +++++++++ ch5/findlinks3/findlinks.go | 56 +++++++ ch5/links/links.go | 67 +++++++++ ch5/outline/main.go | 36 +++++ ch5/outline2/outline.go | 80 ++++++++++ ch5/squares/main.go | 30 ++++ ch5/sum/main.go | 33 ++++ ch5/title1/title.go | 82 ++++++++++ ch5/title2/title.go | 72 +++++++++ ch5/title3/title.go | 99 ++++++++++++ ch5/toposort/main.go | 69 +++++++++ ch5/trace/main.go | 40 +++++ ch5/wait/wait.go | 50 ++++++ ch6/coloredpoint/main.go | 89 +++++++++++ ch6/geometry/geometry.go | 42 ++++++ ch6/intset/intset.go | 73 +++++++++ ch6/intset/intset_test.go | 50 ++++++ ch6/urlvalues/main.go | 53 +++++++ ch7/bytecounter/main.go | 35 +++++ ch7/eval/ast.go | 40 +++++ ch7/eval/check.go | 58 +++++++ ch7/eval/coverage_test.go | 48 ++++++ ch7/eval/eval.go | 70 +++++++++ ch7/eval/eval_test.go | 113 ++++++++++++++ ch7/eval/parse.go | 160 ++++++++++++++++++++ ch7/eval/print.go | 51 +++++++ ch7/http1/main.go | 46 ++++++ ch7/http2/main.go | 48 ++++++ ch7/http3/main.go | 61 ++++++++ ch7/http3a/main.go | 54 +++++++ ch7/http4/main.go | 47 ++++++ ch7/sleep/sleep.go | 25 +++ ch7/sorting/main.go | 167 ++++++++++++++++++++ ch7/surface/surface.go | 112 ++++++++++++++ ch7/tempconv/tempconv.go | 66 ++++++++ ch7/tempflag/tempflag.go | 24 +++ ch7/xmlselect/main.go | 56 +++++++ ch8/cake/cake.go | 89 +++++++++++ ch8/cake/cake_test.go | 74 +++++++++ ch8/chat/chat.go | 96 ++++++++++++ ch8/clock1/clock.go | 43 ++++++ ch8/clock2/clock.go | 42 ++++++ ch8/countdown1/countdown.go | 29 ++++ ch8/countdown2/countdown.go | 46 ++++++ ch8/countdown3/countdown.go | 51 +++++++ ch8/crawl1/findlinks.go | 72 +++++++++ ch8/crawl2/findlinks.go | 64 ++++++++ ch8/crawl3/findlinks.go | 61 ++++++++ ch8/du1/main.go | 79 ++++++++++ ch8/du2/main.go | 94 ++++++++++++ ch8/du3/main.go | 118 +++++++++++++++ ch8/du4/main.go | 145 ++++++++++++++++++ ch8/netcat1/netcat.go | 32 ++++ ch8/netcat2/netcat.go | 33 ++++ ch8/netcat3/netcat.go | 39 +++++ ch8/pipeline1/main.go | 37 +++++ ch8/pipeline2/main.go | 38 +++++ ch8/pipeline3/main.go | 42 ++++++ ch8/reverb1/reverb.go | 51 +++++++ ch8/reverb2/reverb.go | 51 +++++++ ch8/spinner/main.go | 38 +++++ ch8/thumbnail/main.go | 42 ++++++ ch8/thumbnail/thumbnail.go | 86 +++++++++++ ch8/thumbnail/thumbnail_test.go | 148 ++++++++++++++++++ ch9/bank1/bank.go | 31 ++++ ch9/bank1/bank_test.go | 36 +++++ ch9/bank2/bank.go | 28 ++++ ch9/bank2/bank_test.go | 28 ++++ ch9/bank3/bank.go | 30 ++++ ch9/bank3/bank_test.go | 28 ++++ ch9/memo1/memo.go | 40 +++++ ch9/memo1/memo_test.go | 67 +++++++++ ch9/memo2/memo.go | 44 ++++++ ch9/memo2/memo_test.go | 23 +++ ch9/memo3/memo.go | 48 ++++++ ch9/memo3/memo_test.go | 23 +++ ch9/memo4/memo.go | 61 ++++++++ ch9/memo4/memo_test.go | 23 +++ ch9/memo5/memo.go | 88 +++++++++++ ch9/memo5/memo_test.go | 25 +++ ch9/memotest/memotest.go | 102 +++++++++++++ 150 files changed, 8559 insertions(+), 6 deletions(-) create mode 100644 ch10/cross/main.go create mode 100644 ch10/jpeg/main.go create mode 100644 ch11/echo/echo.go create mode 100644 ch11/echo/echo_test.go create mode 100644 ch11/storage1/storage.go create mode 100644 ch11/storage2/quota_test.go create mode 100644 ch11/storage2/storage.go create mode 100644 ch11/word1/word.go create mode 100644 ch11/word1/word_test.go create mode 100644 ch11/word2/word.go create mode 100644 ch11/word2/word_test.go create mode 100644 ch12/display/display.go create mode 100644 ch12/display/display_test.go create mode 100644 ch12/format/format.go create mode 100644 ch12/format/format_test.go create mode 100644 ch12/methods/methods.go create mode 100644 ch12/methods/methods_test.go create mode 100644 ch12/params/params.go create mode 100644 ch12/search/main.go create mode 100644 ch12/sexpr/decode.go create mode 100644 ch12/sexpr/encode.go create mode 100644 ch12/sexpr/pretty.go create mode 100644 ch12/sexpr/sexpr_test.go create mode 100644 ch13/equal/equal.go create mode 100644 ch13/equal/equal_test.go create mode 100644 ch13/unsafeptr/main.go create mode 100644 ch2/boiling/main.go create mode 100644 ch2/cf/main.go create mode 100644 ch2/echo4/main.go create mode 100644 ch2/ftoc/main.go create mode 100644 ch2/popcount/main.go create mode 100644 ch2/popcount/popcount_test.go create mode 100644 ch2/tempconv/conv.go create mode 100644 ch2/tempconv/tempconv.go create mode 100644 ch2/tempconv0/celsius.go create mode 100644 ch2/tempconv0/tempconv_test.go create mode 100644 ch3/basename1/main.go create mode 100644 ch3/basename2/main.go create mode 100644 ch3/comma/main.go create mode 100644 ch3/mandelbrot/main.go create mode 100644 ch3/netflag/netflag.go create mode 100644 ch3/printints/main.go create mode 100644 ch3/surface/main.go create mode 100644 ch4/append/main.go create mode 100644 ch4/autoescape/main.go create mode 100644 ch4/charcount/main.go create mode 100644 ch4/dedup/main.go create mode 100644 ch4/embed/main.go create mode 100644 ch4/github/github.go create mode 100644 ch4/github/search.go create mode 100644 ch4/graph/main.go create mode 100644 ch4/issues/main.go create mode 100644 ch4/issueshtml/main.go create mode 100644 ch4/issuesreport/main.go create mode 100644 ch4/movie/main.go create mode 100644 ch4/nonempty/main.go create mode 100644 ch4/rev/main.go create mode 100644 ch4/sha256/main.go create mode 100644 ch4/treesort/sort.go create mode 100644 ch4/treesort/sort_test.go create mode 100644 ch5/defer1/defer.go create mode 100644 ch5/defer2/defer.go create mode 100644 ch5/fetch/main.go create mode 100644 ch5/findlinks2/main.go create mode 100644 ch5/findlinks3/findlinks.go create mode 100644 ch5/links/links.go create mode 100644 ch5/outline/main.go create mode 100644 ch5/outline2/outline.go create mode 100644 ch5/squares/main.go create mode 100644 ch5/sum/main.go create mode 100644 ch5/title1/title.go create mode 100644 ch5/title2/title.go create mode 100644 ch5/title3/title.go create mode 100644 ch5/toposort/main.go create mode 100644 ch5/trace/main.go create mode 100644 ch5/wait/wait.go create mode 100644 ch6/coloredpoint/main.go create mode 100644 ch6/geometry/geometry.go create mode 100644 ch6/intset/intset.go create mode 100644 ch6/intset/intset_test.go create mode 100644 ch6/urlvalues/main.go create mode 100644 ch7/bytecounter/main.go create mode 100644 ch7/eval/ast.go create mode 100644 ch7/eval/check.go create mode 100644 ch7/eval/coverage_test.go create mode 100644 ch7/eval/eval.go create mode 100644 ch7/eval/eval_test.go create mode 100644 ch7/eval/parse.go create mode 100644 ch7/eval/print.go create mode 100644 ch7/http1/main.go create mode 100644 ch7/http2/main.go create mode 100644 ch7/http3/main.go create mode 100644 ch7/http3a/main.go create mode 100644 ch7/http4/main.go create mode 100644 ch7/sleep/sleep.go create mode 100644 ch7/sorting/main.go create mode 100644 ch7/surface/surface.go create mode 100644 ch7/tempconv/tempconv.go create mode 100644 ch7/tempflag/tempflag.go create mode 100644 ch7/xmlselect/main.go create mode 100644 ch8/cake/cake.go create mode 100644 ch8/cake/cake_test.go create mode 100644 ch8/chat/chat.go create mode 100644 ch8/clock1/clock.go create mode 100644 ch8/clock2/clock.go create mode 100644 ch8/countdown1/countdown.go create mode 100644 ch8/countdown2/countdown.go create mode 100644 ch8/countdown3/countdown.go create mode 100644 ch8/crawl1/findlinks.go create mode 100644 ch8/crawl2/findlinks.go create mode 100644 ch8/crawl3/findlinks.go create mode 100644 ch8/du1/main.go create mode 100644 ch8/du2/main.go create mode 100644 ch8/du3/main.go create mode 100644 ch8/du4/main.go create mode 100644 ch8/netcat1/netcat.go create mode 100644 ch8/netcat2/netcat.go create mode 100644 ch8/netcat3/netcat.go create mode 100644 ch8/pipeline1/main.go create mode 100644 ch8/pipeline2/main.go create mode 100644 ch8/pipeline3/main.go create mode 100644 ch8/reverb1/reverb.go create mode 100644 ch8/reverb2/reverb.go create mode 100644 ch8/spinner/main.go create mode 100644 ch8/thumbnail/main.go create mode 100644 ch8/thumbnail/thumbnail.go create mode 100644 ch8/thumbnail/thumbnail_test.go create mode 100644 ch9/bank1/bank.go create mode 100644 ch9/bank1/bank_test.go create mode 100644 ch9/bank2/bank.go create mode 100644 ch9/bank2/bank_test.go create mode 100644 ch9/bank3/bank.go create mode 100644 ch9/bank3/bank_test.go create mode 100644 ch9/memo1/memo.go create mode 100644 ch9/memo1/memo_test.go create mode 100644 ch9/memo2/memo.go create mode 100644 ch9/memo2/memo_test.go create mode 100644 ch9/memo3/memo.go create mode 100644 ch9/memo3/memo_test.go create mode 100644 ch9/memo4/memo.go create mode 100644 ch9/memo4/memo_test.go create mode 100644 ch9/memo5/memo.go create mode 100644 ch9/memo5/memo_test.go create mode 100644 ch9/memotest/memotest.go diff --git a/ch1/lissajous/main.go b/ch1/lissajous/main.go index 8614d7b..be50e70 100644 --- a/ch1/lissajous/main.go +++ b/ch1/lissajous/main.go @@ -13,17 +13,21 @@ import ( "image/color" "image/gif" "io" - //!-main - "log" - //!+main "math" "math/rand" - //!-main - "net/http" - //!+main "os" ) +//!-main +// Packages not needed by version in book. +import ( + "log" + "net/http" + "time" +) + +//!+main + var palette = []color.Color{color.White, color.Black} const ( @@ -33,6 +37,11 @@ const ( func main() { //!-main + // The sequence of images is deterministic unless we seed + // the pseudo-random number generator using the current time. + // Thanks to Randall McPherson for pointing out the omission. + rand.Seed(time.Now().UTC().UnixNano()) + if len(os.Args) > 1 && os.Args[1] == "web" { //!+http handler := func(w http.ResponseWriter, r *http.Request) { diff --git a/ch10/cross/main.go b/ch10/cross/main.go new file mode 100644 index 0000000..1de5cf5 --- /dev/null +++ b/ch10/cross/main.go @@ -0,0 +1,19 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 295. + +// The cross command prints the values of GOOS and GOARCH for this target. +package main + +import ( + "fmt" + "runtime" +) + +//!+ +func main() { + fmt.Println(runtime.GOOS, runtime.GOARCH) +} + +//!- diff --git a/ch10/jpeg/main.go b/ch10/jpeg/main.go new file mode 100644 index 0000000..ca84efb --- /dev/null +++ b/ch10/jpeg/main.go @@ -0,0 +1,52 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 287. + +//!+main + +// The jpeg command reads a PNG image from the standard input +// and writes it as a JPEG image to the standard output. +package main + +import ( + "fmt" + "image" + "image/jpeg" + _ "image/png" // register PNG decoder + "io" + "os" +) + +func main() { + if err := toJPEG(os.Stdin, os.Stdout); err != nil { + fmt.Fprintf(os.Stderr, "jpeg: %v\n", err) + os.Exit(1) + } +} + +func toJPEG(in io.Reader, out io.Writer) error { + img, kind, err := image.Decode(in) + if err != nil { + return err + } + fmt.Fprintln(os.Stderr, "Input format =", kind) + return jpeg.Encode(out, img, &jpeg.Options{Quality: 95}) +} + +//!-main + +/* +//!+with +$ go build gopl.io/ch3/mandelbrot +$ go build gopl.io/ch10/jpeg +$ ./mandelbrot | ./jpeg >mandelbrot.jpg +Input format = png +//!-with + +//!+without +$ go build gopl.io/ch10/jpeg +$ ./mandelbrot | ./jpeg >mandelbrot.jpg +jpeg: image: unknown format +//!-without +*/ diff --git a/ch11/echo/echo.go b/ch11/echo/echo.go new file mode 100644 index 0000000..82b5a3e --- /dev/null +++ b/ch11/echo/echo.go @@ -0,0 +1,41 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 308. +//!+ + +// Echo prints its command-line arguments. +package main + +import ( + "flag" + "fmt" + "io" + "os" + "strings" +) + +var ( + n = flag.Bool("n", false, "omit trailing newline") + s = flag.String("s", " ", "separator") +) + +var out io.Writer = os.Stdout // modified during testing + +func main() { + flag.Parse() + if err := echo(!*n, *s, flag.Args()); err != nil { + fmt.Fprintf(os.Stderr, "echo: %v\n", err) + os.Exit(1) + } +} + +func echo(newline bool, sep string, args []string) error { + fmt.Fprint(out, strings.Join(args, sep)) + if newline { + fmt.Fprintln(out) + } + return nil +} + +//!- diff --git a/ch11/echo/echo_test.go b/ch11/echo/echo_test.go new file mode 100644 index 0000000..7d2f47a --- /dev/null +++ b/ch11/echo/echo_test.go @@ -0,0 +1,45 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// Test of echo command. Run with: go test gopl.io/ch11/echo + +//!+ +package main + +import ( + "bytes" + "fmt" + "testing" +) + +func TestEcho(t *testing.T) { + var tests = []struct { + newline bool + sep string + args []string + want string + }{ + {true, "", []string{}, "\n"}, + {false, "", []string{}, ""}, + {true, "\t", []string{"one", "two", "three"}, "one\ttwo\tthree\n"}, + {true, ",", []string{"a", "b", "c"}, "a,b,c\n"}, + {false, ":", []string{"1", "2", "3"}, "1:2:3"}, + } + + for _, test := range tests { + descr := fmt.Sprintf("echo(%v, %q, %q)", + test.newline, test.sep, test.args) + + out = new(bytes.Buffer) // captured output + if err := echo(test.newline, test.sep, test.args); err != nil { + t.Errorf("%s failed: %v", descr, err) + continue + } + got := out.(*bytes.Buffer).String() + if got != test.want { + t.Errorf("%s = %q, want %q", descr, got, test.want) + } + } +} + +//!- diff --git a/ch11/storage1/storage.go b/ch11/storage1/storage.go new file mode 100644 index 0000000..9c2c81c --- /dev/null +++ b/ch11/storage1/storage.go @@ -0,0 +1,43 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 311. + +// Package storage is part of a hypothetical cloud storage server. +//!+main +package storage + +import ( + "fmt" + "log" + "net/smtp" +) + +func bytesInUse(username string) int64 { return 0 /* ... */ } + +// Email sender configuration. +// NOTE: never put passwords in source code! +const sender = "notifications@example.com" +const password = "correcthorsebatterystaple" +const hostname = "smtp.example.com" + +const template = `Warning: you are using %d bytes of storage, +%d%% of your quota.` + +func CheckQuota(username string) { + used := bytesInUse(username) + const quota = 1000000000 // 1GB + percent := 100 * used / quota + if percent < 90 { + return // OK + } + msg := fmt.Sprintf(template, used, percent) + auth := smtp.PlainAuth("", sender, password, hostname) + err := smtp.SendMail(hostname+":587", auth, sender, + []string{username}, []byte(msg)) + if err != nil { + log.Printf("smtp.SendMail(%s) failed: %s", username, err) + } +} + +//!-main diff --git a/ch11/storage2/quota_test.go b/ch11/storage2/quota_test.go new file mode 100644 index 0000000..513a051 --- /dev/null +++ b/ch11/storage2/quota_test.go @@ -0,0 +1,56 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +//!+test +package storage + +import ( + "strings" + "testing" +) + +func TestCheckQuotaNotifiesUser(t *testing.T) { + var notifiedUser, notifiedMsg string + notifyUser = func(user, msg string) { + notifiedUser, notifiedMsg = user, msg + } + + const user = "joe@example.org" + + // Simulate a 980MB-used condition for this user. + // NOTE: this differs slightly from the printed version. + usage["joe@example.org"] = 980000000 + + CheckQuota(user) + if notifiedUser == "" && notifiedMsg == "" { + t.Fatalf("notifyUser not called") + } + if notifiedUser != user { + t.Errorf("wrong user (%s) notified, want %s", + notifiedUser, user) + } + const wantSubstring = "98% of your quota" + if !strings.Contains(notifiedMsg, wantSubstring) { + t.Errorf("unexpected notification message <<%s>>, "+ + "want substring %q", notifiedMsg, wantSubstring) + } +} + +//!-test + +/* +//!+defer +func TestCheckQuotaNotifiesUser(t *testing.T) { + // Save and restore original notifyUser. + saved := notifyUser + defer func() { notifyUser = saved }() + + // Install the test's fake notifyUser. + var notifiedUser, notifiedMsg string + notifyUser = func(user, msg string) { + notifiedUser, notifiedMsg = user, msg + } + // ...rest of test... +} +//!-defer +*/ diff --git a/ch11/storage2/storage.go b/ch11/storage2/storage.go new file mode 100644 index 0000000..e9ca221 --- /dev/null +++ b/ch11/storage2/storage.go @@ -0,0 +1,51 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 312. + +// Package storage is part of a hypothetical cloud storage server. +//!+main +package storage + +import ( + "fmt" + "log" + "net/smtp" +) + +// NOTE: this differs slightly from the printed version. +var usage = make(map[string]int64) + +func bytesInUse(username string) int64 { return usage[username] } + +// E-mail sender configuration. +// NOTE: never put passwords in source code! +const sender = "notifications@example.com" +const password = "correcthorsebatterystaple" +const hostname = "smtp.example.com" + +const template = `Warning: you are using %d bytes of storage, +%d%% of your quota.` + +//!+factored +var notifyUser = func(username, msg string) { + auth := smtp.PlainAuth("", sender, password, hostname) + err := smtp.SendMail(hostname+":587", auth, sender, + []string{username}, []byte(msg)) + if err != nil { + log.Printf("smtp.SendEmail(%s) failed: %s", username, err) + } +} + +func CheckQuota(username string) { + used := bytesInUse(username) + const quota = 1000000000 // 1GB + percent := 100 * used / quota + if percent < 90 { + return // OK + } + msg := fmt.Sprintf(template, used, percent) + notifyUser(username, msg) +} + +//!-factored diff --git a/ch11/word1/word.go b/ch11/word1/word.go new file mode 100644 index 0000000..048abc9 --- /dev/null +++ b/ch11/word1/word.go @@ -0,0 +1,21 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 303. +//!+ + +// Package word provides utilities for word games. +package word + +// IsPalindrome reports whether s reads the same forward and backward. +// (Our first attempt.) +func IsPalindrome(s string) bool { + for i := range s { + if s[i] != s[len(s)-1-i] { + return false + } + } + return true +} + +//!- diff --git a/ch11/word1/word_test.go b/ch11/word1/word_test.go new file mode 100644 index 0000000..bb53404 --- /dev/null +++ b/ch11/word1/word_test.go @@ -0,0 +1,43 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +//!+test +package word + +import "testing" + +func TestPalindrome(t *testing.T) { + if !IsPalindrome("detartrated") { + t.Error(`IsPalindrome("detartrated") = false`) + } + if !IsPalindrome("kayak") { + t.Error(`IsPalindrome("kayak") = false`) + } +} + +func TestNonPalindrome(t *testing.T) { + if IsPalindrome("palindrome") { + t.Error(`IsPalindrome("palindrome") = true`) + } +} + +//!-test + +// The tests below are expected to fail. +// See package gopl.io/ch11/word2 for the fix. + +//!+more +func TestFrenchPalindrome(t *testing.T) { + if !IsPalindrome("été") { + t.Error(`IsPalindrome("été") = false`) + } +} + +func TestCanalPalindrome(t *testing.T) { + input := "A man, a plan, a canal: Panama" + if !IsPalindrome(input) { + t.Errorf(`IsPalindrome(%q) = false`, input) + } +} + +//!-more diff --git a/ch11/word2/word.go b/ch11/word2/word.go new file mode 100644 index 0000000..846c8d2 --- /dev/null +++ b/ch11/word2/word.go @@ -0,0 +1,29 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 305. +//!+ + +// Package word provides utilities for word games. +package word + +import "unicode" + +// IsPalindrome reports whether s reads the same forward and backward. +// Letter case is ignored, as are non-letters. +func IsPalindrome(s string) bool { + var letters []rune + for _, r := range s { + if unicode.IsLetter(r) { + letters = append(letters, unicode.ToLower(r)) + } + } + for i := range letters { + if letters[i] != letters[len(letters)-1-i] { + return false + } + } + return true +} + +//!- diff --git a/ch11/word2/word_test.go b/ch11/word2/word_test.go new file mode 100644 index 0000000..4b17337 --- /dev/null +++ b/ch11/word2/word_test.go @@ -0,0 +1,148 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package word + +import ( + "fmt" + "math/rand" + "time" +) + +//!+bench + +import "testing" + +//!-bench + +//!+test +func TestIsPalindrome(t *testing.T) { + var tests = []struct { + input string + want bool + }{ + {"", true}, + {"a", true}, + {"aa", true}, + {"ab", false}, + {"kayak", true}, + {"detartrated", true}, + {"A man, a plan, a canal: Panama", true}, + {"Evil I did dwell; lewd did I live.", true}, + {"Able was I ere I saw Elba", true}, + {"été", true}, + {"Et se resservir, ivresse reste.", true}, + {"palindrome", false}, // non-palindrome + {"desserts", false}, // semi-palindrome + } + for _, test := range tests { + if got := IsPalindrome(test.input); got != test.want { + t.Errorf("IsPalindrome(%q) = %v", test.input, got) + } + } +} + +//!-test + +//!+bench +func BenchmarkIsPalindrome(b *testing.B) { + for i := 0; i < b.N; i++ { + IsPalindrome("A man, a plan, a canal: Panama") + } +} + +//!-bench + +//!+example + +func ExampleIsPalindrome() { + fmt.Println(IsPalindrome("A man, a plan, a canal: Panama")) + fmt.Println(IsPalindrome("palindrome")) + // Output: + // true + // false +} + +//!-example + +/* +//!+random +import "math/rand" + +//!-random +*/ + +//!+random +// randomPalindrome returns a palindrome whose length and contents +// are derived from the pseudo-random number generator rng. +func randomPalindrome(rng *rand.Rand) string { + n := rng.Intn(25) // random length up to 24 + runes := make([]rune, n) + for i := 0; i < (n+1)/2; i++ { + r := rune(rng.Intn(0x1000)) // random rune up to '\u0999' + runes[i] = r + runes[n-1-i] = r + } + return string(runes) +} + +func TestRandomPalindromes(t *testing.T) { + // Initialize a pseudo-random number generator. + seed := time.Now().UTC().UnixNano() + t.Logf("Random seed: %d", seed) + rng := rand.New(rand.NewSource(seed)) + + for i := 0; i < 1000; i++ { + p := randomPalindrome(rng) + if !IsPalindrome(p) { + t.Errorf("IsPalindrome(%q) = false", p) + } + } +} + +//!-random + +/* +// Answer for Exercicse 11.1: Modify randomPalindrome to exercise +// IsPalindrome's handling of punctuation and spaces. + +// WARNING: the conversion r -> upper -> lower doesn't preserve +// the value of r in some cases, e.g., µ Μ, ſ S, ı I + +// randomPalindrome returns a palindrome whose length and contents +// are derived from the pseudo-random number generator rng. +func randomNoisyPalindrome(rng *rand.Rand) string { + n := rng.Intn(25) // random length up to 24 + runes := make([]rune, n) + for i := 0; i < (n+1)/2; i++ { + r := rune(rng.Intn(0x200)) // random rune up to \u99 + runes[i] = r + r1 := r + if unicode.IsLetter(r) && unicode.IsLower(r) { + r = unicode.ToUpper(r) + if unicode.ToLower(r) != r1 { + fmt.Printf("cap? %c %c\n", r1, r) + } + } + runes[n-1-i] = r + } + return "?" + string(runes) + "!" +} + +func TestRandomNoisyPalindromes(t *testing.T) { + // Initialize a pseudo-random number generator. + seed := time.Now().UTC().UnixNano() + t.Logf("Random seed: %d", seed) + rng := rand.New(rand.NewSource(seed)) + + n := 0 + for i := 0; i < 1000; i++ { + p := randomNoisyPalindrome(rng) + if !IsPalindrome(p) { + t.Errorf("IsNoisyPalindrome(%q) = false", p) + n++ + } + } + fmt.Fprintf(os.Stderr, "fail = %d\n", n) +} +*/ diff --git a/ch12/display/display.go b/ch12/display/display.go new file mode 100644 index 0000000..933a5b0 --- /dev/null +++ b/ch12/display/display.go @@ -0,0 +1,90 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 333. + +// Package display provides a means to display structured data. +package display + +import ( + "fmt" + "reflect" + "strconv" +) + +//!+Display + +func Display(name string, x interface{}) { + fmt.Printf("Display %s (%T):\n", name, x) + display(name, reflect.ValueOf(x)) +} + +//!-Display + +// formatAtom formats a value without inspecting its internal structure. +// It is a copy of the the function in gopl.io/ch11/format. +func formatAtom(v reflect.Value) string { + switch v.Kind() { + case reflect.Invalid: + return "invalid" + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + return strconv.FormatInt(v.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return strconv.FormatUint(v.Uint(), 10) + // ...floating-point and complex cases omitted for brevity... + case reflect.Bool: + if v.Bool() { + return "true" + } + return "false" + case reflect.String: + return strconv.Quote(v.String()) + case reflect.Chan, reflect.Func, reflect.Ptr, + reflect.Slice, reflect.Map: + return v.Type().String() + " 0x" + + strconv.FormatUint(uint64(v.Pointer()), 16) + default: // reflect.Array, reflect.Struct, reflect.Interface + return v.Type().String() + " value" + } +} + +//!+display +func display(path string, v reflect.Value) { + switch v.Kind() { + case reflect.Invalid: + fmt.Printf("%s = invalid\n", path) + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + display(fmt.Sprintf("%s[%d]", path, i), v.Index(i)) + } + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name) + display(fieldPath, v.Field(i)) + } + case reflect.Map: + for _, key := range v.MapKeys() { + display(fmt.Sprintf("%s[%s]", path, + formatAtom(key)), v.MapIndex(key)) + } + case reflect.Ptr: + if v.IsNil() { + fmt.Printf("%s = nil\n", path) + } else { + display(fmt.Sprintf("(*%s)", path), v.Elem()) + } + case reflect.Interface: + if v.IsNil() { + fmt.Printf("%s = nil\n", path) + } else { + fmt.Printf("%s.type = %s\n", path, v.Elem().Type()) + display(path+".value", v.Elem()) + } + default: // basic types, channels, funcs + fmt.Printf("%s = %s\n", path, formatAtom(v)) + } +} + +//!-display diff --git a/ch12/display/display_test.go b/ch12/display/display_test.go new file mode 100644 index 0000000..2bd9065 --- /dev/null +++ b/ch12/display/display_test.go @@ -0,0 +1,259 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package display + +import ( + "io" + "net" + "os" + "reflect" + "sync" + "testing" + + "gopl.io/ch7/eval" +) + +// NOTE: we can't use !+..!- comments to excerpt these tests +// into the book because it defeats the Example mechanism, +// which requires the // Output comment to be at the end +// of the function. + +func ExampleExpr() { + e, _ := eval.Parse("sqrt(A / pi)") + Display("e", e) + // Output: + // Display e (eval.call): + // e.fn = "sqrt" + // e.args[0].type = eval.binary + // e.args[0].value.op = 47 + // e.args[0].value.x.type = eval.Var + // e.args[0].value.x.value = "A" + // e.args[0].value.y.type = eval.Var + // e.args[0].value.y.value = "pi" +} + +func ExampleSlice() { + Display("slice", []*int{new(int), nil}) + // Output: + // Display slice ([]*int): + // (*slice[0]) = 0 + // slice[1] = nil +} + +func ExampleNilInterface() { + var w io.Writer + Display("w", w) + // Output: + // Display w (): + // w = invalid +} + +func ExamplePtrToInterface() { + var w io.Writer + Display("&w", &w) + // Output: + // Display &w (*io.Writer): + // (*&w) = nil +} + +func ExampleStruct() { + Display("x", struct{ x interface{} }{3}) + // Output: + // Display x (struct { x interface {} }): + // x.x.type = int + // x.x.value = 3 +} + +func ExampleInterface() { + var i interface{} = 3 + Display("i", i) + // Output: + // Display i (int): + // i = 3 +} + +func ExamplePtrToInterface2() { + var i interface{} = 3 + Display("&i", &i) + // Output: + // Display &i (*interface {}): + // (*&i).type = int + // (*&i).value = 3 +} + +func ExampleArray() { + Display("x", [1]interface{}{3}) + // Output: + // Display x ([1]interface {}): + // x[0].type = int + // x[0].value = 3 +} + +func ExampleMovie() { + //!+movie + type Movie struct { + Title, Subtitle string + Year int + Color bool + Actor map[string]string + Oscars []string + Sequel *string + } + //!-movie + //!+strangelove + strangelove := Movie{ + Title: "Dr. Strangelove", + Subtitle: "How I Learned to Stop Worrying and Love the Bomb", + Year: 1964, + Color: false, + Actor: map[string]string{ + "Dr. Strangelove": "Peter Sellers", + "Grp. Capt. Lionel Mandrake": "Peter Sellers", + "Pres. Merkin Muffley": "Peter Sellers", + "Gen. Buck Turgidson": "George C. Scott", + "Brig. Gen. Jack D. Ripper": "Sterling Hayden", + `Maj. T.J. "King" Kong`: "Slim Pickens", + }, + + Oscars: []string{ + "Best Actor (Nomin.)", + "Best Adapted Screenplay (Nomin.)", + "Best Director (Nomin.)", + "Best Picture (Nomin.)", + }, + } + //!-strangelove + Display("strangelove", strangelove) + + // We don't use an Output: comment since displaying + // a map is nondeterministic. + /* + //!+output + Display strangelove (display.Movie): + strangelove.Title = "Dr. Strangelove" + strangelove.Subtitle = "How I Learned to Stop Worrying and Love the Bomb" + strangelove.Year = 1964 + strangelove.Color = false + strangelove.Actor["Gen. Buck Turgidson"] = "George C. Scott" + strangelove.Actor["Brig. Gen. Jack D. Ripper"] = "Sterling Hayden" + strangelove.Actor["Maj. T.J. \"King\" Kong"] = "Slim Pickens" + strangelove.Actor["Dr. Strangelove"] = "Peter Sellers" + strangelove.Actor["Grp. Capt. Lionel Mandrake"] = "Peter Sellers" + strangelove.Actor["Pres. Merkin Muffley"] = "Peter Sellers" + strangelove.Oscars[0] = "Best Actor (Nomin.)" + strangelove.Oscars[1] = "Best Adapted Screenplay (Nomin.)" + strangelove.Oscars[2] = "Best Director (Nomin.)" + strangelove.Oscars[3] = "Best Picture (Nomin.)" + strangelove.Sequel = nil + //!-output + */ +} + +// This test ensures that the program terminates without crashing. +func Test(t *testing.T) { + // Some other values (YMMV) + Display("os.Stderr", os.Stderr) + // Output: + // Display os.Stderr (*os.File): + // (*(*os.Stderr).file).fd = 2 + // (*(*os.Stderr).file).name = "/dev/stderr" + // (*(*os.Stderr).file).nepipe = 0 + + var w io.Writer = os.Stderr + Display("&w", &w) + // Output: + // Display &w (*io.Writer): + // (*&w).type = *os.File + // (*(*(*&w).value).file).fd = 2 + // (*(*(*&w).value).file).name = "/dev/stderr" + // (*(*(*&w).value).file).nepipe = 0 + + var locker sync.Locker = new(sync.Mutex) + Display("(&locker)", &locker) + // Output: + // Display (&locker) (*sync.Locker): + // (*(&locker)).type = *sync.Mutex + // (*(*(&locker)).value).state = 0 + // (*(*(&locker)).value).sema = 0 + + Display("locker", locker) + // Output: + // Display locker (*sync.Mutex): + // (*locker).state = 0 + // (*locker).sema = 0 + // (*(&locker)) = nil + + locker = nil + Display("(&locker)", &locker) + // Output: + // Display (&locker) (*sync.Locker): + // (*(&locker)) = nil + + ips, _ := net.LookupHost("golang.org") + Display("ips", ips) + // Output: + // Display ips ([]string): + // ips[0] = "173.194.68.141" + // ips[1] = "2607:f8b0:400d:c06::8d" + + // Even metarecursion! (YMMV) + Display("rV", reflect.ValueOf(os.Stderr)) + // Output: + // Display rV (reflect.Value): + // (*rV.typ).size = 8 + // (*rV.typ).ptrdata = 8 + // (*rV.typ).hash = 871609668 + // (*rV.typ)._ = 0 + // ... + + // a pointer that points to itself + type P *P + var p P + p = &p + if false { + Display("p", p) + // Output: + // Display p (display.P): + // ...stuck, no output... + } + + // a map that contains itself + type M map[string]M + m := make(M) + m[""] = m + if false { + Display("m", m) + // Output: + // Display m (display.M): + // ...stuck, no output... + } + + // a slice that contains itself + type S []S + s := make(S, 1) + s[0] = s + if false { + Display("s", s) + // Output: + // Display s (display.S): + // ...stuck, no output... + } + + // a linked list that eats its own tail + type Cycle struct { + Value int + Tail *Cycle + } + var c Cycle + c = Cycle{42, &c} + if false { + Display("c", c) + // Output: + // Display c (display.Cycle): + // c.Value = 42 + // (*c.Tail).Value = 42 + // (*(*c.Tail).Tail).Value = 42 + // ...ad infinitum... + } +} diff --git a/ch12/format/format.go b/ch12/format/format.go new file mode 100644 index 0000000..02f839a --- /dev/null +++ b/ch12/format/format.go @@ -0,0 +1,44 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 332. + +// Package format provides an Any function that can format any value. +//!+ +package format + +import ( + "reflect" + "strconv" +) + +// Any formats any value as a string. +func Any(value interface{}) string { + return formatAtom(reflect.ValueOf(value)) +} + +// formatAtom formats a value without inspecting its internal structure. +func formatAtom(v reflect.Value) string { + switch v.Kind() { + case reflect.Invalid: + return "invalid" + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + return strconv.FormatInt(v.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return strconv.FormatUint(v.Uint(), 10) + // ...floating-point and complex cases omitted for brevity... + case reflect.Bool: + return strconv.FormatBool(v.Bool()) + case reflect.String: + return strconv.Quote(v.String()) + case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map: + return v.Type().String() + " 0x" + + strconv.FormatUint(uint64(v.Pointer()), 16) + default: // reflect.Array, reflect.Struct, reflect.Interface + return v.Type().String() + " value" + } +} + +//!- diff --git a/ch12/format/format_test.go b/ch12/format/format_test.go new file mode 100644 index 0000000..77c0846 --- /dev/null +++ b/ch12/format/format_test.go @@ -0,0 +1,24 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package format_test + +import ( + "fmt" + "testing" + "time" + + "gopl.io/ch12/format" +) + +func Test(t *testing.T) { + // The pointer values are just examples, and may vary from run to run. + //!+time + var x int64 = 1 + var d time.Duration = 1 * time.Nanosecond + fmt.Println(format.Any(x)) // "1" + fmt.Println(format.Any(d)) // "1" + fmt.Println(format.Any([]int64{x})) // "[]int64 0x8202b87b0" + fmt.Println(format.Any([]time.Duration{d})) // "[]time.Duration 0x8202b87e0" + //!-time +} diff --git a/ch12/methods/methods.go b/ch12/methods/methods.go new file mode 100644 index 0000000..3b6b560 --- /dev/null +++ b/ch12/methods/methods.go @@ -0,0 +1,29 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 351. + +// Package methods provides a function to print the methods of any value. +package methods + +import ( + "fmt" + "reflect" + "strings" +) + +//!+print +// Print prints the method set of the value x. +func Print(x interface{}) { + v := reflect.ValueOf(x) + t := v.Type() + fmt.Printf("type %s\n", t) + + for i := 0; i < v.NumMethod(); i++ { + methType := v.Method(i).Type() + fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name, + strings.TrimPrefix(methType.String(), "func")) + } +} + +//!-print diff --git a/ch12/methods/methods_test.go b/ch12/methods/methods_test.go new file mode 100644 index 0000000..0b509fe --- /dev/null +++ b/ch12/methods/methods_test.go @@ -0,0 +1,49 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package methods_test + +import ( + "strings" + "time" + + "gopl.io/ch12/methods" +) + +func ExamplePrintDuration() { + methods.Print(time.Hour) + // Output: + // type time.Duration + // func (time.Duration) Hours() float64 + // func (time.Duration) Minutes() float64 + // func (time.Duration) Nanoseconds() int64 + // func (time.Duration) Seconds() float64 + // func (time.Duration) String() string +} + +func ExamplePrintReplacer() { + methods.Print(new(strings.Replacer)) + // Output: + // type *strings.Replacer + // func (*strings.Replacer) Replace(string) string + // func (*strings.Replacer) WriteString(io.Writer, string) (int, error) +} + +/* +//!+output +methods.Print(time.Hour) +// Output: +// type time.Duration +// func (time.Duration) Hours() float64 +// func (time.Duration) Minutes() float64 +// func (time.Duration) Nanoseconds() int64 +// func (time.Duration) Seconds() float64 +// func (time.Duration) String() string + +methods.Print(new(strings.Replacer)) +// Output: +// type *strings.Replacer +// func (*strings.Replacer) Replace(string) string +// func (*strings.Replacer) WriteString(io.Writer, string) (int, error) +//!-output +*/ diff --git a/ch12/params/params.go b/ch12/params/params.go new file mode 100644 index 0000000..8bf48cb --- /dev/null +++ b/ch12/params/params.go @@ -0,0 +1,90 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 349. + +// Package params provides a reflection-based parser for URL parameters. +package params + +import ( + "fmt" + "net/http" + "reflect" + "strconv" + "strings" +) + +//!+Unpack + +// Unpack populates the fields of the struct pointed to by ptr +// from the HTTP request parameters in req. +func Unpack(req *http.Request, ptr interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + + // Build map of fields keyed by effective name. + fields := make(map[string]reflect.Value) + v := reflect.ValueOf(ptr).Elem() // the struct variable + for i := 0; i < v.NumField(); i++ { + fieldInfo := v.Type().Field(i) // a reflect.StructField + tag := fieldInfo.Tag // a reflect.StructTag + name := tag.Get("http") + if name == "" { + name = strings.ToLower(fieldInfo.Name) + } + fields[name] = v.Field(i) + } + + // Update struct field for each parameter in the request. + for name, values := range req.Form { + f := fields[name] + if !f.IsValid() { + continue // ignore unrecognized HTTP parameters + } + for _, value := range values { + if f.Kind() == reflect.Slice { + elem := reflect.New(f.Type().Elem()).Elem() + if err := populate(elem, value); err != nil { + return fmt.Errorf("%s: %v", name, err) + } + f.Set(reflect.Append(f, elem)) + } else { + if err := populate(f, value); err != nil { + return fmt.Errorf("%s: %v", name, err) + } + } + } + } + return nil +} + +//!-Unpack + +//!+populate +func populate(v reflect.Value, value string) error { + switch v.Kind() { + case reflect.String: + v.SetString(value) + + case reflect.Int: + i, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + v.SetInt(i) + + case reflect.Bool: + b, err := strconv.ParseBool(value) + if err != nil { + return err + } + v.SetBool(b) + + default: + return fmt.Errorf("unsupported kind %s", v.Type()) + } + return nil +} + +//!-populate diff --git a/ch12/search/main.go b/ch12/search/main.go new file mode 100644 index 0000000..9a94e82 --- /dev/null +++ b/ch12/search/main.go @@ -0,0 +1,60 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 348. + +// Search is a demo of the params.Unpack function. +package main + +import ( + "fmt" + "log" + "net/http" +) + +//!+ + +import "gopl.io/ch12/params" + +// search implements the /search URL endpoint. +func search(resp http.ResponseWriter, req *http.Request) { + var data struct { + Labels []string `http:"l"` + MaxResults int `http:"max"` + Exact bool `http:"x"` + } + data.MaxResults = 10 // set default + if err := params.Unpack(req, &data); err != nil { + http.Error(resp, err.Error(), http.StatusBadRequest) // 400 + return + } + + // ...rest of handler... + fmt.Fprintf(resp, "Search: %+v\n", data) +} + +//!- + +func main() { + http.HandleFunc("/search", search) + log.Fatal(http.ListenAndServe(":12345", nil)) +} + +/* +//!+output +$ go build gopl.io/ch12/search +$ ./search & +$ ./fetch 'http://localhost:12345/search' +Search: {Labels:[] MaxResults:10 Exact:false} +$ ./fetch 'http://localhost:12345/search?l=golang&l=programming' +Search: {Labels:[golang programming] MaxResults:10 Exact:false} +$ ./fetch 'http://localhost:12345/search?l=golang&l=programming&max=100' +Search: {Labels:[golang programming] MaxResults:100 Exact:false} +$ ./fetch 'http://localhost:12345/search?x=true&l=golang&l=programming' +Search: {Labels:[golang programming] MaxResults:10 Exact:true} +$ ./fetch 'http://localhost:12345/search?q=hello&x=123' +x: strconv.ParseBool: parsing "123": invalid syntax +$ ./fetch 'http://localhost:12345/search?q=hello&max=lots' +max: strconv.ParseInt: parsing "lots": invalid syntax +//!-output +*/ diff --git a/ch12/sexpr/decode.go b/ch12/sexpr/decode.go new file mode 100644 index 0000000..696e5bc --- /dev/null +++ b/ch12/sexpr/decode.go @@ -0,0 +1,162 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 344. + +// Package sexpr provides a means for converting Go objects to and +// from S-expressions. +package sexpr + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "text/scanner" +) + +//!+Unmarshal +// Unmarshal parses S-expression data and populates the variable +// whose address is in the non-nil pointer out. +func Unmarshal(data []byte, out interface{}) (err error) { + lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}} + lex.scan.Init(bytes.NewReader(data)) + lex.next() // get the first token + defer func() { + // NOTE: this is not an example of ideal error handling. + if x := recover(); x != nil { + err = fmt.Errorf("error at %s: %v", lex.scan.Position, x) + } + }() + read(lex, reflect.ValueOf(out).Elem()) + return nil +} + +//!-Unmarshal + +//!+lexer +type lexer struct { + scan scanner.Scanner + token rune // the current token +} + +func (lex *lexer) next() { lex.token = lex.scan.Scan() } +func (lex *lexer) text() string { return lex.scan.TokenText() } + +func (lex *lexer) consume(want rune) { + if lex.token != want { // NOTE: Not an example of good error handling. + panic(fmt.Sprintf("got %q, want %q", lex.text(), want)) + } + lex.next() +} + +//!-lexer + +// The read function is a decoder for a small subset of well-formed +// S-expressions. For brevity of our example, it takes many dubious +// shortcuts. +// +// The parser assumes +// - that the S-expression input is well-formed; it does no error checking. +// - that the S-expression input corresponds to the type of the variable. +// - that all numbers in the input are non-negative decimal integers. +// - that all keys in ((key value) ...) struct syntax are unquoted symbols. +// - that the input does not contain dotted lists such as (1 2 . 3). +// - that the input does not contain Lisp reader macros such 'x and #'x. +// +// The reflection logic assumes +// - that v is always a variable of the appropriate type for the +// S-expression value. For example, v must not be a boolean, +// interface, channel, or function, and if v is an array, the input +// must have the correct number of elements. +// - that v in the top-level call to read has the zero value of its +// type and doesn't need clearing. +// - that if v is a numeric variable, it is a signed integer. + +//!+read +func read(lex *lexer, v reflect.Value) { + switch lex.token { + case scanner.Ident: + // The only valid identifiers are + // "nil" and struct field names. + if lex.text() == "nil" { + v.Set(reflect.Zero(v.Type())) + lex.next() + return + } + case scanner.String: + s, _ := strconv.Unquote(lex.text()) // NOTE: ignoring errors + v.SetString(s) + lex.next() + return + case scanner.Int: + i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errors + v.SetInt(int64(i)) + lex.next() + return + case '(': + lex.next() + readList(lex, v) + lex.next() // consume ')' + return + } + panic(fmt.Sprintf("unexpected token %q", lex.text())) +} + +//!-read + +//!+readlist +func readList(lex *lexer, v reflect.Value) { + switch v.Kind() { + case reflect.Array: // (item ...) + for i := 0; !endList(lex); i++ { + read(lex, v.Index(i)) + } + + case reflect.Slice: // (item ...) + for !endList(lex) { + item := reflect.New(v.Type().Elem()).Elem() + read(lex, item) + v.Set(reflect.Append(v, item)) + } + + case reflect.Struct: // ((name value) ...) + for !endList(lex) { + lex.consume('(') + if lex.token != scanner.Ident { + panic(fmt.Sprintf("got token %q, want field name", lex.text())) + } + name := lex.text() + lex.next() + read(lex, v.FieldByName(name)) + lex.consume(')') + } + + case reflect.Map: // ((key value) ...) + v.Set(reflect.MakeMap(v.Type())) + for !endList(lex) { + lex.consume('(') + key := reflect.New(v.Type().Key()).Elem() + read(lex, key) + value := reflect.New(v.Type().Elem()).Elem() + read(lex, value) + v.SetMapIndex(key, value) + lex.consume(')') + } + + default: + panic(fmt.Sprintf("cannot decode list into %v", v.Type())) + } +} + +func endList(lex *lexer) bool { + switch lex.token { + case scanner.EOF: + panic("end of file") + case ')': + return true + } + return false +} + +//!-readlist diff --git a/ch12/sexpr/encode.go b/ch12/sexpr/encode.go new file mode 100644 index 0000000..f2a53a0 --- /dev/null +++ b/ch12/sexpr/encode.go @@ -0,0 +1,97 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 339. + +package sexpr + +import ( + "bytes" + "fmt" + "reflect" +) + +//!+Marshal +// Marshal encodes a Go value in S-expression form. +func Marshal(v interface{}) ([]byte, error) { + var buf bytes.Buffer + if err := encode(&buf, reflect.ValueOf(v)); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +//!-Marshal + +// encode writes to buf an S-expression representation of v. +//!+encode +func encode(buf *bytes.Buffer, v reflect.Value) error { + switch v.Kind() { + case reflect.Invalid: + buf.WriteString("nil") + + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + fmt.Fprintf(buf, "%d", v.Int()) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + fmt.Fprintf(buf, "%d", v.Uint()) + + case reflect.String: + fmt.Fprintf(buf, "%q", v.String()) + + case reflect.Ptr: + return encode(buf, v.Elem()) + + case reflect.Array, reflect.Slice: // (value ...) + buf.WriteByte('(') + for i := 0; i < v.Len(); i++ { + if i > 0 { + buf.WriteByte(' ') + } + if err := encode(buf, v.Index(i)); err != nil { + return err + } + } + buf.WriteByte(')') + + case reflect.Struct: // ((name value) ...) + buf.WriteByte('(') + for i := 0; i < v.NumField(); i++ { + if i > 0 { + buf.WriteByte(' ') + } + fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name) + if err := encode(buf, v.Field(i)); err != nil { + return err + } + buf.WriteByte(')') + } + buf.WriteByte(')') + + case reflect.Map: // ((key value) ...) + buf.WriteByte('(') + for i, key := range v.MapKeys() { + if i > 0 { + buf.WriteByte(' ') + } + buf.WriteByte('(') + if err := encode(buf, key); err != nil { + return err + } + buf.WriteByte(' ') + if err := encode(buf, v.MapIndex(key)); err != nil { + return err + } + buf.WriteByte(')') + } + buf.WriteByte(')') + + default: // float, complex, bool, chan, func, interface + return fmt.Errorf("unsupported type: %s", v.Type()) + } + return nil +} + +//!-encode diff --git a/ch12/sexpr/pretty.go b/ch12/sexpr/pretty.go new file mode 100644 index 0000000..1c5716d --- /dev/null +++ b/ch12/sexpr/pretty.go @@ -0,0 +1,183 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package sexpr + +// This file implements the algorithm described in Derek C. Oppen's +// 1979 Stanford technical report, "Pretty Printing". + +import ( + "bytes" + "fmt" + "reflect" +) + +func MarshalIndent(v interface{}) ([]byte, error) { + p := printer{width: margin} + if err := pretty(&p, reflect.ValueOf(v)); err != nil { + return nil, err + } + return p.Bytes(), nil +} + +const margin = 80 + +type token struct { + kind rune // one of "s ()" (string, blank, start, end) + str string + size int +} + +type printer struct { + tokens []*token // FIFO buffer + stack []*token // stack of open ' ' and '(' tokens + rtotal int // total number of spaces needed to print stream + + bytes.Buffer + indents []int + width int // remaining space +} + +func (p *printer) string(str string) { + tok := &token{kind: 's', str: str, size: len(str)} + if len(p.stack) == 0 { + p.print(tok) + } else { + p.tokens = append(p.tokens, tok) + p.rtotal += len(str) + } +} +func (p *printer) pop() (top *token) { + last := len(p.stack) - 1 + top, p.stack = p.stack[last], p.stack[:last] + return +} +func (p *printer) begin() { + if len(p.stack) == 0 { + p.rtotal = 1 + } + t := &token{kind: '(', size: -p.rtotal} + p.tokens = append(p.tokens, t) + p.stack = append(p.stack, t) // push + p.string("(") +} +func (p *printer) end() { + p.string(")") + p.tokens = append(p.tokens, &token{kind: ')'}) + x := p.pop() + x.size += p.rtotal + if x.kind == ' ' { + p.pop().size += p.rtotal + } + if len(p.stack) == 0 { + for _, tok := range p.tokens { + p.print(tok) + } + p.tokens = nil + } +} +func (p *printer) space() { + last := len(p.stack) - 1 + x := p.stack[last] + if x.kind == ' ' { + x.size += p.rtotal + p.stack = p.stack[:last] // pop + } + t := &token{kind: ' ', size: -p.rtotal} + p.tokens = append(p.tokens, t) + p.stack = append(p.stack, t) + p.rtotal++ +} +func (p *printer) print(t *token) { + switch t.kind { + case 's': + p.WriteString(t.str) + p.width -= len(t.str) + case '(': + p.indents = append(p.indents, p.width) + case ')': + p.indents = p.indents[:len(p.indents)-1] // pop + case ' ': + if t.size > p.width { + p.width = p.indents[len(p.indents)-1] - 1 + fmt.Fprintf(&p.Buffer, "\n%*s", margin-p.width, "") + } else { + p.WriteByte(' ') + p.width-- + } + } +} +func (p *printer) stringf(format string, args ...interface{}) { + p.string(fmt.Sprintf(format, args...)) +} + +func pretty(p *printer, v reflect.Value) error { + switch v.Kind() { + case reflect.Invalid: + p.string("nil") + + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + p.stringf("%d", v.Int()) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + p.stringf("%d", v.Uint()) + + case reflect.String: + p.stringf("%q", v.String()) + + case reflect.Array, reflect.Slice: // (value ...) + p.begin() + for i := 0; i < v.Len(); i++ { + if i > 0 { + p.space() + } + if err := pretty(p, v.Index(i)); err != nil { + return err + } + } + p.end() + + case reflect.Struct: // ((name value ...) + p.begin() + for i := 0; i < v.NumField(); i++ { + if i > 0 { + p.space() + } + p.begin() + p.string(v.Type().Field(i).Name) + p.space() + if err := pretty(p, v.Field(i)); err != nil { + return err + } + p.end() + } + p.end() + + case reflect.Map: // ((key value ...) + p.begin() + for i, key := range v.MapKeys() { + if i > 0 { + p.space() + } + p.begin() + if err := pretty(p, key); err != nil { + return err + } + p.space() + if err := pretty(p, v.MapIndex(key)); err != nil { + return err + } + p.end() + } + p.end() + + case reflect.Ptr: + return pretty(p, v.Elem()) + + default: // float, complex, bool, chan, func, interface + return fmt.Errorf("unsupported type: %s", v.Type()) + } + return nil +} diff --git a/ch12/sexpr/sexpr_test.go b/ch12/sexpr/sexpr_test.go new file mode 100644 index 0000000..25b0bf7 --- /dev/null +++ b/ch12/sexpr/sexpr_test.go @@ -0,0 +1,74 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package sexpr + +import ( + "reflect" + "testing" +) + +// Test verifies that encoding and decoding a complex data value +// produces an equal result. +// +// The test does not make direct assertions about the encoded output +// because the output depends on map iteration order, which is +// nondeterministic. The output of the t.Log statements can be +// inspected by running the test with the -v flag: +// +// $ go test -v gopl.io/ch12/sexpr +// +func Test(t *testing.T) { + type Movie struct { + Title, Subtitle string + Year int + Actor map[string]string + Oscars []string + Sequel *string + } + strangelove := Movie{ + Title: "Dr. Strangelove", + Subtitle: "How I Learned to Stop Worrying and Love the Bomb", + Year: 1964, + Actor: map[string]string{ + "Dr. Strangelove": "Peter Sellers", + "Grp. Capt. Lionel Mandrake": "Peter Sellers", + "Pres. Merkin Muffley": "Peter Sellers", + "Gen. Buck Turgidson": "George C. Scott", + "Brig. Gen. Jack D. Ripper": "Sterling Hayden", + `Maj. T.J. "King" Kong`: "Slim Pickens", + }, + Oscars: []string{ + "Best Actor (Nomin.)", + "Best Adapted Screenplay (Nomin.)", + "Best Director (Nomin.)", + "Best Picture (Nomin.)", + }, + } + + // Encode it + data, err := Marshal(strangelove) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + t.Logf("Marshal() = %s\n", data) + + // Decode it + var movie Movie + if err := Unmarshal(data, &movie); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + t.Logf("Unmarshal() = %+v\n", movie) + + // Check equality. + if !reflect.DeepEqual(movie, strangelove) { + t.Fatal("not equal") + } + + // Pretty-print it: + data, err = MarshalIndent(strangelove) + if err != nil { + t.Fatal(err) + } + t.Logf("MarshalIdent() = %s\n", data) +} diff --git a/ch13/bzip/bzip2.c b/ch13/bzip/bzip2.c index 3c77f0d..12e8d87 100644 --- a/ch13/bzip/bzip2.c +++ b/ch13/bzip/bzip2.c @@ -1,4 +1,7 @@ // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 362. //!+ /* This file is gopl.io/ch13/bzip/bzip2.c, */ diff --git a/ch13/bzip/bzip2.go b/ch13/bzip/bzip2.go index e357622..a3bdcf6 100644 --- a/ch13/bzip/bzip2.go +++ b/ch13/bzip/bzip2.go @@ -1,4 +1,7 @@ // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 362. //!+ // Package bzip provides a writer that uses bzip2 compression (bzip.org). diff --git a/ch13/bzip/bzip2_test.go b/ch13/bzip/bzip2_test.go index a7f88da..c52aba0 100644 --- a/ch13/bzip/bzip2_test.go +++ b/ch13/bzip/bzip2_test.go @@ -1,4 +1,5 @@ // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ package bzip_test diff --git a/ch13/bzipper/main.go b/ch13/bzipper/main.go index dd705c2..a8ebad1 100644 --- a/ch13/bzipper/main.go +++ b/ch13/bzipper/main.go @@ -1,4 +1,8 @@ // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 365. + //!+ // Bzipper reads input, bzip2-compresses it, and writes it out. diff --git a/ch13/equal/equal.go b/ch13/equal/equal.go new file mode 100644 index 0000000..ec4b40c --- /dev/null +++ b/ch13/equal/equal.go @@ -0,0 +1,127 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 359. + +// Package equal provides a deep equivalence relation for arbitrary values. +package equal + +import ( + "reflect" + "unsafe" +) + +//!+ +func equal(x, y reflect.Value, seen map[comparison]bool) bool { + if !x.IsValid() || !y.IsValid() { + return x.IsValid() == y.IsValid() + } + if x.Type() != y.Type() { + return false + } + + // ...cycle check omitted (shown later)... + + //!- + //!+cyclecheck + // cycle check + if x.CanAddr() && y.CanAddr() { + xptr := unsafe.Pointer(x.UnsafeAddr()) + yptr := unsafe.Pointer(y.UnsafeAddr()) + if xptr == yptr { + return true // identical references + } + c := comparison{xptr, yptr, x.Type()} + if seen[c] { + return true // already seen + } + seen[c] = true + } + //!-cyclecheck + //!+ + switch x.Kind() { + case reflect.Bool: + return x.Bool() == y.Bool() + + case reflect.String: + return x.String() == y.String() + + // ...numeric cases omitted for brevity... + + //!- + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64: + return x.Int() == y.Int() + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64, reflect.Uintptr: + return x.Uint() == y.Uint() + + case reflect.Float32, reflect.Float64: + return x.Float() == y.Float() + + case reflect.Complex64, reflect.Complex128: + return x.Complex() == y.Complex() + //!+ + case reflect.Chan, reflect.UnsafePointer, reflect.Func: + return x.Pointer() == y.Pointer() + + case reflect.Ptr, reflect.Interface: + return equal(x.Elem(), y.Elem(), seen) + + case reflect.Array, reflect.Slice: + if x.Len() != y.Len() { + return false + } + for i := 0; i < x.Len(); i++ { + if !equal(x.Index(i), y.Index(i), seen) { + return false + } + } + return true + + // ...struct and map cases omitted for brevity... + //!- + case reflect.Struct: + for i, n := 0, x.NumField(); i < n; i++ { + if !equal(x.Field(i), y.Field(i), seen) { + return false + } + } + return true + + case reflect.Map: + if x.Len() != y.Len() { + return false + } + for _, k := range x.MapKeys() { + if !equal(x.MapIndex(k), y.MapIndex(k), seen) { + return false + } + } + return true + //!+ + } + panic("unreachable") +} + +//!- + +//!+comparison +// Equal reports whether x and y are deeply equal. +//!-comparison +// +// Map keys are always compared with ==, not deeply. +// (This matters for keys containing pointers or interfaces.) +//!+comparison +func Equal(x, y interface{}) bool { + seen := make(map[comparison]bool) + return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen) +} + +type comparison struct { + x, y unsafe.Pointer + t reflect.Type +} + +//!-comparison diff --git a/ch13/equal/equal_test.go b/ch13/equal/equal_test.go new file mode 100644 index 0000000..7dcc0d9 --- /dev/null +++ b/ch13/equal/equal_test.go @@ -0,0 +1,133 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package equal + +import ( + "bytes" + "fmt" + "testing" +) + +func TestEqual(t *testing.T) { + one, oneAgain, two := 1, 1, 2 + + type CyclePtr *CyclePtr + var cyclePtr1, cyclePtr2 CyclePtr + cyclePtr1 = &cyclePtr1 + cyclePtr2 = &cyclePtr2 + + type CycleSlice []CycleSlice + var cycleSlice CycleSlice + cycleSlice = append(cycleSlice, cycleSlice) + + ch1, ch2 := make(chan int), make(chan int) + var ch1ro <-chan int = ch1 + + type mystring string + + var iface1, iface1Again, iface2 interface{} = &one, &oneAgain, &two + + for _, test := range []struct { + x, y interface{} + want bool + }{ + // basic types + {1, 1, true}, + {1, 2, false}, // different values + {1, 1.0, false}, // different types + {"foo", "foo", true}, + {"foo", "bar", false}, + {mystring("foo"), "foo", false}, // different types + // slices + {[]string{"foo"}, []string{"foo"}, true}, + {[]string{"foo"}, []string{"bar"}, false}, + {[]string{}, []string(nil), true}, + // slice cycles + {cycleSlice, cycleSlice, true}, + // maps + { + map[string][]int{"foo": {1, 2, 3}}, + map[string][]int{"foo": {1, 2, 3}}, + true, + }, + { + map[string][]int{"foo": {1, 2, 3}}, + map[string][]int{"foo": {1, 2, 3, 4}}, + false, + }, + { + map[string][]int{}, + map[string][]int(nil), + true, + }, + // pointers + {&one, &one, true}, + {&one, &two, false}, + {&one, &oneAgain, true}, + {new(bytes.Buffer), new(bytes.Buffer), true}, + // pointer cycles + {cyclePtr1, cyclePtr1, true}, + {cyclePtr2, cyclePtr2, true}, + {cyclePtr1, cyclePtr2, true}, // they're deeply equal + // functions + {(func())(nil), (func())(nil), true}, + {(func())(nil), func() {}, false}, + {func() {}, func() {}, false}, + // arrays + {[...]int{1, 2, 3}, [...]int{1, 2, 3}, true}, + {[...]int{1, 2, 3}, [...]int{1, 2, 4}, false}, + // channels + {ch1, ch1, true}, + {ch1, ch2, false}, + {ch1ro, ch1, false}, // NOTE: not equal + // interfaces + {&iface1, &iface1, true}, + {&iface1, &iface2, false}, + {&iface1Again, &iface1, true}, + } { + if Equal(test.x, test.y) != test.want { + t.Errorf("Equal(%v, %v) = %t", + test.x, test.y, !test.want) + } + } +} + +func ExampleEqual() { + //!+ + fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true" + fmt.Println(Equal([]string{"foo"}, []string{"bar"})) // "false" + fmt.Println(Equal([]string(nil), []string{})) // "true" + fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true" + //!- + + // Output: + // true + // false + // true + // true +} + +func ExampleEqualCycle() { + //!+cycle + // Circular linked lists a -> b -> a and c -> c. + type link struct { + value string + tail *link + } + a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"} + a.tail, b.tail, c.tail = b, a, c + fmt.Println(Equal(a, a)) // "true" + fmt.Println(Equal(b, b)) // "true" + fmt.Println(Equal(c, c)) // "true" + fmt.Println(Equal(a, b)) // "false" + fmt.Println(Equal(a, c)) // "false" + //!-cycle + + // Output: + // true + // true + // true + // false + // false +} diff --git a/ch13/unsafeptr/main.go b/ch13/unsafeptr/main.go new file mode 100644 index 0000000..f906946 --- /dev/null +++ b/ch13/unsafeptr/main.go @@ -0,0 +1,38 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 357. + +// Package unsafeptr demonstrates basic use of unsafe.Pointer. +package main + +import ( + "fmt" + "unsafe" +) + +func main() { + //!+main + var x struct { + a bool + b int16 + c []int + } + + // equivalent to pb := &x.b + pb := (*int16)(unsafe.Pointer( + uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b))) + *pb = 42 + + fmt.Println(x.b) // "42" + //!-main +} + +/* +//!+wrong + // NOTE: subtly incorrect! + tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b) + pb := (*int16)(unsafe.Pointer(tmp)) + *pb = 42 +//!-wrong +*/ diff --git a/ch2/boiling/main.go b/ch2/boiling/main.go new file mode 100644 index 0000000..b7a1dbe --- /dev/null +++ b/ch2/boiling/main.go @@ -0,0 +1,22 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 29. +//!+ + +// Boiling prints the boiling point of water. +package main + +import "fmt" + +const boilingF = 212.0 + +func main() { + var f = boilingF + var c = (f - 32) * 5 / 9 + fmt.Printf("boiling point = %g°F or %g°C\n", f, c) + // Output: + // boiling point = 212°F or 100°C +} + +//!- diff --git a/ch2/cf/main.go b/ch2/cf/main.go new file mode 100644 index 0000000..882101f --- /dev/null +++ b/ch2/cf/main.go @@ -0,0 +1,32 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 43. +//!+ + +// Cf converts its numeric argument to Celsius and Fahrenheit. +package main + +import ( + "fmt" + "os" + "strconv" + + "gopl.io/ch2/tempconv" +) + +func main() { + for _, arg := range os.Args[1:] { + t, err := strconv.ParseFloat(arg, 64) + if err != nil { + fmt.Fprintf(os.Stderr, "cf: %v\n", err) + os.Exit(1) + } + f := tempconv.Fahrenheit(t) + c := tempconv.Celsius(t) + fmt.Printf("%s = %s, %s = %s\n", + f, tempconv.FToC(f), c, tempconv.CToF(c)) + } +} + +//!- diff --git a/ch2/echo4/main.go b/ch2/echo4/main.go new file mode 100644 index 0000000..5a029fd --- /dev/null +++ b/ch2/echo4/main.go @@ -0,0 +1,27 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 33. +//!+ + +// Echo4 prints its command-line arguments. +package main + +import ( + "flag" + "fmt" + "strings" +) + +var n = flag.Bool("n", false, "omit trailing newline") +var sep = flag.String("s", " ", "separator") + +func main() { + flag.Parse() + fmt.Print(strings.Join(flag.Args(), *sep)) + if !*n { + fmt.Println() + } +} + +//!- diff --git a/ch2/ftoc/main.go b/ch2/ftoc/main.go new file mode 100644 index 0000000..3f3abd0 --- /dev/null +++ b/ch2/ftoc/main.go @@ -0,0 +1,22 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 29. +//!+ + +// Ftoc prints two Fahrenheit-to-Celsius conversions. +package main + +import "fmt" + +func main() { + const freezingF, boilingF = 32.0, 212.0 + fmt.Printf("%g°F = %g°C\n", freezingF, fToC(freezingF)) // "32°F = 0°C" + fmt.Printf("%g°F = %g°C\n", boilingF, fToC(boilingF)) // "212°F = 100°C" +} + +func fToC(f float64) float64 { + return (f - 32) * 5 / 9 +} + +//!- diff --git a/ch2/popcount/main.go b/ch2/popcount/main.go new file mode 100644 index 0000000..c994e08 --- /dev/null +++ b/ch2/popcount/main.go @@ -0,0 +1,31 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 45. + +// (Package doc comment intentionally malformed to demonstrate golint.) +//!+ +package popcount + +// pc[i] is the population count of i. +var pc [256]byte + +func init() { + for i := range pc { + pc[i] = pc[i/2] + byte(i&1) + } +} + +// PopCount returns the population count (number of set bits) of x. +func PopCount(x uint64) int { + return int(pc[byte(x>>(0*8))] + + pc[byte(x>>(1*8))] + + pc[byte(x>>(2*8))] + + pc[byte(x>>(3*8))] + + pc[byte(x>>(4*8))] + + pc[byte(x>>(5*8))] + + pc[byte(x>>(6*8))] + + pc[byte(x>>(7*8))]) +} + +//!- diff --git a/ch2/popcount/popcount_test.go b/ch2/popcount/popcount_test.go new file mode 100644 index 0000000..504548d --- /dev/null +++ b/ch2/popcount/popcount_test.go @@ -0,0 +1,83 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package popcount_test + +import ( + "testing" + + "gopl.io/ch2/popcount" +) + +// -- Alternative implementations -- + +func BitCount(x uint64) int { + // Hacker's Delight, Figure 5-2. + x = x - ((x >> 1) & 0x5555555555555555) + x = (x & 0x3333333333333333) + ((x >> 2) & 0x3333333333333333) + x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0f + x = x + (x >> 8) + x = x + (x >> 16) + x = x + (x >> 32) + return int(x & 0x7f) +} + +func PopCountByClearing(x uint64) int { + n := 0 + for x != 0 { + x = x & (x - 1) // clear rightmost non-zero bit + n++ + } + return n +} + +func PopCountByShifting(x uint64) int { + n := 0 + for i := uint(0); i < 64; i++ { + if x&(1< a, a.go => a, a/b/c.go => c, a/b.c.go => b.c +func basename(s string) string { + // Discard last '/' and everything before. + for i := len(s) - 1; i >= 0; i-- { + if s[i] == '/' { + s = s[i+1:] + break + } + } + // Preserve everything before last '.'. + for i := len(s) - 1; i >= 0; i-- { + if s[i] == '.' { + s = s[:i] + break + } + } + return s +} + +//!- diff --git a/ch3/basename2/main.go b/ch3/basename2/main.go new file mode 100644 index 0000000..93c471c --- /dev/null +++ b/ch3/basename2/main.go @@ -0,0 +1,36 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 72. + +// Basename2 reads file names from stdin and prints the base name of each one. +package main + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +func main() { + input := bufio.NewScanner(os.Stdin) + for input.Scan() { + fmt.Println(basename(input.Text())) + } + // NOTE: ignoring potential errors from input.Err() +} + +// basename removes directory components and a trailing .suffix. +// e.g., a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c +//!+ +func basename(s string) string { + slash := strings.LastIndex(s, "/") // -1 if "/" not found + s = s[slash+1:] + if dot := strings.LastIndex(s, "."); dot >= 0 { + s = s[:dot] + } + return s +} + +//!- diff --git a/ch3/comma/main.go b/ch3/comma/main.go new file mode 100644 index 0000000..6f0f0e9 --- /dev/null +++ b/ch3/comma/main.go @@ -0,0 +1,40 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 73. + +// Comma prints its argument numbers with a comma at each power of 1000. +// +// Example: +// $ go build gopl.io/ch3/comma +// $ ./comma 1 12 123 1234 1234567890 +// 1 +// 12 +// 123 +// 1,234 +// 1,234,567,890 +// +package main + +import ( + "fmt" + "os" +) + +func main() { + for i := 1; i < len(os.Args); i++ { + fmt.Printf(" %s\n", comma(os.Args[i])) + } +} + +//!+ +// comma inserts commas in a non-negative decimal integer string. +func comma(s string) string { + n := len(s) + if n <= 3 { + return s + } + return comma(s[:n-3]) + "," + s[n-3:] +} + +//!- diff --git a/ch3/mandelbrot/main.go b/ch3/mandelbrot/main.go new file mode 100644 index 0000000..805922d --- /dev/null +++ b/ch3/mandelbrot/main.go @@ -0,0 +1,84 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 61. +//!+ + +// Mandelbrot emits a PNG image of the Mandelbrot fractal. +package main + +import ( + "image" + "image/color" + "image/png" + "math/cmplx" + "os" +) + +func main() { + const ( + xmin, ymin, xmax, ymax = -2, -2, +2, +2 + width, height = 1024, 1024 + ) + + img := image.NewRGBA(image.Rect(0, 0, width, height)) + for py := 0; py < height; py++ { + y := float64(py)/height*(ymax-ymin) + ymin + for px := 0; px < width; px++ { + x := float64(px)/width*(xmax-xmin) + xmin + z := complex(x, y) + // Image point (px, py) represents complex value z. + img.Set(px, py, mandelbrot(z)) + } + } + png.Encode(os.Stdout, img) // NOTE: ignoring errors +} + +func mandelbrot(z complex128) color.Color { + const iterations = 200 + const contrast = 15 + + var v complex128 + for n := uint8(0); n < iterations; n++ { + v = v*v + z + if cmplx.Abs(v) > 2 { + return color.Gray{255 - contrast*n} + } + } + return color.Black +} + +//!- + +// Some other interesting functions: + +func acos(z complex128) color.Color { + v := cmplx.Acos(z) + blue := uint8(real(v)*128) + 127 + red := uint8(imag(v)*128) + 127 + return color.YCbCr{192, blue, red} +} + +func sqrt(z complex128) color.Color { + v := cmplx.Sqrt(z) + blue := uint8(real(v)*128) + 127 + red := uint8(imag(v)*128) + 127 + return color.YCbCr{128, blue, red} +} + +// f(x) = x^4 - 1 +// +// z' = z - f(z)/f'(z) +// = z - (z^4 - 1) / (4 * z^3) +// = z - (z - 1/z^3) / 4 +func newton(z complex128) color.Color { + const iterations = 37 + const contrast = 7 + for i := uint8(0); i < iterations; i++ { + z -= (z - 1/(z*z*z)) / 4 + if cmplx.Abs(z*z*z*z-1) < 1e-6 { + return color.Gray{255 - contrast*i} + } + } + return color.Black +} diff --git a/ch3/netflag/netflag.go b/ch3/netflag/netflag.go new file mode 100644 index 0000000..dd4aaff --- /dev/null +++ b/ch3/netflag/netflag.go @@ -0,0 +1,30 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 77. + +// Netflag demonstrates an integer type used as a bit field. +package main + +import ( + "fmt" + . "net" +) + +//!+ +func IsUp(v Flags) bool { return v&FlagUp == FlagUp } +func TurnDown(v *Flags) { *v &^= FlagUp } +func SetBroadcast(v *Flags) { *v |= FlagBroadcast } +func IsCast(v Flags) bool { return v&(FlagBroadcast|FlagMulticast) != 0 } + +func main() { + var v Flags = FlagMulticast | FlagUp + fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true" + TurnDown(&v) + fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false" + SetBroadcast(&v) + fmt.Printf("%b %t\n", v, IsUp(v)) // "10010 false" + fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true" +} + +//!- diff --git a/ch3/printints/main.go b/ch3/printints/main.go new file mode 100644 index 0000000..6dfa047 --- /dev/null +++ b/ch3/printints/main.go @@ -0,0 +1,33 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 74. + +// Printints demonstrates the use of bytes.Buffer to format a string. +package main + +import ( + "bytes" + "fmt" +) + +//!+ +// intsToString is like fmt.Sprintf(values) but adds commas. +func intsToString(values []int) string { + var buf bytes.Buffer + buf.WriteByte('[') + for i, v := range values { + if i > 0 { + buf.WriteString(", ") + } + fmt.Fprintf(&buf, "%d", v) + } + buf.WriteByte(']') + return buf.String() +} + +func main() { + fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]" +} + +//!- diff --git a/ch3/surface/main.go b/ch3/surface/main.go new file mode 100644 index 0000000..07b26d9 --- /dev/null +++ b/ch3/surface/main.go @@ -0,0 +1,62 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 58. +//!+ + +// Surface computes an SVG rendering of a 3-D surface function. +package main + +import ( + "fmt" + "math" +) + +const ( + width, height = 600, 320 // canvas size in pixels + cells = 100 // number of grid cells + xyrange = 30.0 // axis ranges (-xyrange..+xyrange) + xyscale = width / 2 / xyrange // pixels per x or y unit + zscale = height * 0.4 // pixels per z unit + angle = math.Pi / 6 // angle of x, y axes (=30°) +) + +var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°) + +func main() { + fmt.Printf("", width, height) + for i := 0; i < cells; i++ { + for j := 0; j < cells; j++ { + ax, ay := corner(i+1, j) + bx, by := corner(i, j) + cx, cy := corner(i, j+1) + dx, dy := corner(i+1, j+1) + fmt.Printf("\n", + ax, ay, bx, by, cx, cy, dx, dy) + } + } + fmt.Println("") +} + +func corner(i, j int) (float64, float64) { + // Find point (x,y) at corner of cell (i,j). + x := xyrange * (float64(i)/cells - 0.5) + y := xyrange * (float64(j)/cells - 0.5) + + // Compute surface height z. + z := f(x, y) + + // Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy). + sx := width/2 + (x-y)*cos30*xyscale + sy := height/2 + (x+y)*sin30*xyscale - z*zscale + return sx, sy +} + +func f(x, y float64) float64 { + r := math.Hypot(x, y) // distance from (0,0) + return math.Sin(r) / r +} + +//!- diff --git a/ch4/append/main.go b/ch4/append/main.go new file mode 100644 index 0000000..ef95048 --- /dev/null +++ b/ch4/append/main.go @@ -0,0 +1,79 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 88. + +// Append illustrates the behavior of the built-in append function. +package main + +import "fmt" + +func appendslice(x []int, y ...int) []int { + var z []int + zlen := len(x) + len(y) + if zlen <= cap(x) { + // There is room to expand the slice. + z = x[:zlen] + } else { + // There is insufficient space. + // Grow by doubling, for amortized linear complexity. + zcap := zlen + if zcap < 2*len(x) { + zcap = 2 * len(x) + } + z = make([]int, zlen, zcap) + copy(z, x) + } + copy(z[len(x):], y) + return z +} + +//!+append +func appendInt(x []int, y int) []int { + var z []int + zlen := len(x) + 1 + if zlen <= cap(x) { + // There is room to grow. Extend the slice. + z = x[:zlen] + } else { + // There is insufficient space. Allocate a new array. + // Grow by doubling, for amortized linear complexity. + zcap := zlen + if zcap < 2*len(x) { + zcap = 2 * len(x) + } + z = make([]int, zlen, zcap) + copy(z, x) // a built-in function; see text + } + z[len(x)] = y + return z +} + +//!-append + +//!+growth +func main() { + var x, y []int + for i := 0; i < 10; i++ { + y = appendInt(x, i) + fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y) + x = y + } +} + +//!-growth + +/* +//!+output +0 cap=1 [0] +1 cap=2 [0 1] +2 cap=4 [0 1 2] +3 cap=4 [0 1 2 3] +4 cap=8 [0 1 2 3 4] +5 cap=8 [0 1 2 3 4 5] +6 cap=8 [0 1 2 3 4 5 6] +7 cap=8 [0 1 2 3 4 5 6 7] +8 cap=16 [0 1 2 3 4 5 6 7 8] +9 cap=16 [0 1 2 3 4 5 6 7 8 9] +//!-output +*/ diff --git a/ch4/autoescape/main.go b/ch4/autoescape/main.go new file mode 100644 index 0000000..d39e57d --- /dev/null +++ b/ch4/autoescape/main.go @@ -0,0 +1,30 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 117. + +// Autoescape demonstrates automatic HTML escaping in html/template. +package main + +import ( + "html/template" + "log" + "os" +) + +//!+ +func main() { + const templ = `

A: {{.A}}

B: {{.B}}

` + t := template.Must(template.New("escape").Parse(templ)) + var data struct { + A string // untrusted plain text + B template.HTML // trusted HTML + } + data.A = "Hello!" + data.B = "Hello!" + if err := t.Execute(os.Stdout, data); err != nil { + log.Fatal(err) + } +} + +//!- diff --git a/ch4/charcount/main.go b/ch4/charcount/main.go new file mode 100644 index 0000000..ae7a4db --- /dev/null +++ b/ch4/charcount/main.go @@ -0,0 +1,56 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 97. +//!+ + +// Charcount computes counts of Unicode characters. +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "unicode" + "unicode/utf8" +) + +func main() { + counts := make(map[rune]int) // counts of Unicode characters + var utflen [utf8.UTFMax + 1]int // count of lengths of UTF-8 encodings + invalid := 0 // count of invalid UTF-8 characters + + in := bufio.NewReader(os.Stdin) + for { + r, n, err := in.ReadRune() // returns rune, nbytes, error + if err == io.EOF { + break + } + if err != nil { + fmt.Fprintf(os.Stderr, "charcount: %v\n", err) + os.Exit(1) + } + if r == unicode.ReplacementChar && n == 1 { + invalid++ + continue + } + counts[r]++ + utflen[n]++ + } + fmt.Printf("rune\tcount\n") + for c, n := range counts { + fmt.Printf("%q\t%d\n", c, n) + } + fmt.Print("\nlen\tcount\n") + for i, n := range utflen { + if i > 0 { + fmt.Printf("%d\t%d\n", i, n) + } + } + if invalid > 0 { + fmt.Printf("\n%d invalid UTF-8 characters\n", invalid) + } +} + +//!- diff --git a/ch4/dedup/main.go b/ch4/dedup/main.go new file mode 100644 index 0000000..bf32e93 --- /dev/null +++ b/ch4/dedup/main.go @@ -0,0 +1,33 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 96. + +// Dedup prints only one instance of each line; duplicates are removed. +package main + +import ( + "bufio" + "fmt" + "os" +) + +//!+ +func main() { + seen := make(map[string]bool) // a set of strings + input := bufio.NewScanner(os.Stdin) + for input.Scan() { + line := input.Text() + if !seen[line] { + seen[line] = true + fmt.Println(line) + } + } + + if err := input.Err(); err != nil { + fmt.Fprintf(os.Stderr, "dedup: %v\n", err) + os.Exit(1) + } +} + +//!- diff --git a/ch4/embed/main.go b/ch4/embed/main.go new file mode 100644 index 0000000..f85cfcf --- /dev/null +++ b/ch4/embed/main.go @@ -0,0 +1,46 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 106. + +// Embed demonstrates basic struct embedding. +package main + +import "fmt" + +type Point struct{ X, Y int } + +type Circle struct { + Point + Radius int +} + +type Wheel struct { + Circle + Spokes int +} + +func main() { + var w Wheel + //!+ + w = Wheel{Circle{Point{8, 8}, 5}, 20} + + w = Wheel{ + Circle: Circle{ + Point: Point{X: 8, Y: 8}, + Radius: 5, + }, + Spokes: 20, // NOTE: trailing comma necessary here (and at Radius) + } + + fmt.Printf("%#v\n", w) + // Output: + // Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20} + + w.X = 42 + + fmt.Printf("%#v\n", w) + // Output: + // Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20} + //!- +} diff --git a/ch4/github/github.go b/ch4/github/github.go new file mode 100644 index 0000000..053b954 --- /dev/null +++ b/ch4/github/github.go @@ -0,0 +1,35 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 110. +//!+ + +// Package github provides a Go API for the GitHub issue tracker. +// See https://developer.github.com/v3/search/#search-issues. +package github + +import "time" + +const IssuesURL = "https://api.github.com/search/issues" + +type IssuesSearchResult struct { + TotalCount int `json:"total_count"` + Items []*Issue +} + +type Issue struct { + Number int + HTMLURL string `json:"html_url"` + Title string + State string + User *User + CreatedAt time.Time `json:"created_at"` + Body string // in Markdown format +} + +type User struct { + Login string + HTMLURL string `json:"html_url"` +} + +//!- diff --git a/ch4/github/search.go b/ch4/github/search.go new file mode 100644 index 0000000..070666c --- /dev/null +++ b/ch4/github/search.go @@ -0,0 +1,53 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +//!+ + +package github + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +// SearchIssues queries the GitHub issue tracker. +func SearchIssues(terms []string) (*IssuesSearchResult, error) { + q := url.QueryEscape(strings.Join(terms, " ")) + resp, err := http.Get(IssuesURL + "?q=" + q) + if err != nil { + return nil, err + } + //!- + // For long-term stability, instead of http.Get, use the + // variant below which adds an HTTP request header indicating + // that only version 3 of the GitHub API is acceptable. + // + // req, err := http.NewRequest("GET", IssuesURL+"?q="+q, nil) + // if err != nil { + // return nil, err + // } + // req.Header.Set( + // "Accept", "application/vnd.github.v3.text-match+json") + // resp, err := http.DefaultClient.Do(req) + //!+ + + // We must close resp.Body on all execution paths. + // (Chapter 5 presents 'defer', which makes this simpler.) + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("search query failed: %s", resp.Status) + } + + var result IssuesSearchResult + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + resp.Body.Close() + return nil, err + } + resp.Body.Close() + return &result, nil +} + +//!- diff --git a/ch4/graph/main.go b/ch4/graph/main.go new file mode 100644 index 0000000..e47d90c --- /dev/null +++ b/ch4/graph/main.go @@ -0,0 +1,43 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 99. + +// Graph shows how to use a map of maps to represent a directed graph. +package main + +import "fmt" + +//!+ +var graph = make(map[string]map[string]bool) + +func addEdge(from, to string) { + edges := graph[from] + if edges == nil { + edges = make(map[string]bool) + graph[from] = edges + } + edges[to] = true +} + +func hasEdge(from, to string) bool { + return graph[from][to] +} + +//!- + +func main() { + addEdge("a", "b") + addEdge("c", "d") + addEdge("a", "d") + addEdge("d", "a") + fmt.Println(hasEdge("a", "b")) + fmt.Println(hasEdge("c", "d")) + fmt.Println(hasEdge("a", "d")) + fmt.Println(hasEdge("d", "a")) + fmt.Println(hasEdge("x", "b")) + fmt.Println(hasEdge("c", "d")) + fmt.Println(hasEdge("x", "d")) + fmt.Println(hasEdge("d", "x")) + +} diff --git a/ch4/issues/main.go b/ch4/issues/main.go new file mode 100644 index 0000000..b1b6dbf --- /dev/null +++ b/ch4/issues/main.go @@ -0,0 +1,52 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 112. +//!+ + +// Issues prints a table of GitHub issues matching the search terms. +package main + +import ( + "fmt" + "log" + "os" + + "gopl.io/ch4/github" +) + +//!+ +func main() { + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%d issues:\n", result.TotalCount) + for _, item := range result.Items { + fmt.Printf("#%-5d %9.9s %.55s\n", + item.Number, item.User.Login, item.Title) + } +} + +//!- + +/* +//!+textoutput +$ go build gopl.io/ch4/issues +$ ./issues repo:golang/go is:open json decoder +13 issues: +#5680 eaigner encoding/json: set key converter on en/decoder +#6050 gopherbot encoding/json: provide tokenizer +#8658 gopherbot encoding/json: use bufio +#8462 kortschak encoding/json: UnmarshalText confuses json.Unmarshal +#5901 rsc encoding/json: allow override type marshaling +#9812 klauspost encoding/json: string tag not symmetric +#7872 extempora encoding/json: Encoder internally buffers full output +#9650 cespare encoding/json: Decoding gives errPhase when unmarshalin +#6716 gopherbot encoding/json: include field name in unmarshal error me +#6901 lukescott encoding/json, encoding/xml: option to treat unknown fi +#6384 joeshaw encoding/json: encode precise floating point integers u +#6647 btracey x/tools/cmd/godoc: display type kind of each named type +#4237 gjemiller encoding/base64: URLEncoding padding is optional +//!-textoutput +*/ diff --git a/ch4/issueshtml/main.go b/ch4/issueshtml/main.go new file mode 100644 index 0000000..9e2c36d --- /dev/null +++ b/ch4/issueshtml/main.go @@ -0,0 +1,52 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 115. + +// Issueshtml prints an HTML table of issues matching the search terms. +package main + +import ( + "log" + "os" + + "gopl.io/ch4/github" +) + +//!+template +import "html/template" + +var issueList = template.Must(template.New("issuelist").Parse(` +

{{.TotalCount}} issues

+ + + + + + + +{{range .Items}} + + + + + + +{{end}} +
#StateUserTitle
{{.Number}}{{.State}}{{.User.Login}}{{.Title}}
+`)) + +//!-template + +//!+ +func main() { + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + if err := issueList.Execute(os.Stdout, result); err != nil { + log.Fatal(err) + } +} + +//!- diff --git a/ch4/issuesreport/main.go b/ch4/issuesreport/main.go new file mode 100644 index 0000000..2d41afd --- /dev/null +++ b/ch4/issuesreport/main.go @@ -0,0 +1,89 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 113. + +// Issuesreport prints a report of issues matching the search terms. +package main + +import ( + "log" + "os" + "text/template" + "time" + + "gopl.io/ch4/github" +) + +//!+template +const templ = `{{.TotalCount}} issues: +{{range .Items}}---------------------------------------- +Number: {{.Number}} +User: {{.User.Login}} +Title: {{.Title | printf "%.64s"}} +Age: {{.CreatedAt | daysAgo}} days +{{end}}` + +//!-template + +//!+daysAgo +func daysAgo(t time.Time) int { + return int(time.Since(t).Hours() / 24) +} + +//!-daysAgo + +//!+exec +var report = template.Must(template.New("issuelist"). + Funcs(template.FuncMap{"daysAgo": daysAgo}). + Parse(templ)) + +func main() { + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + if err := report.Execute(os.Stdout, result); err != nil { + log.Fatal(err) + } +} + +//!-exec + +func noMust() { + //!+parse + report, err := template.New("report"). + Funcs(template.FuncMap{"daysAgo": daysAgo}). + Parse(templ) + if err != nil { + log.Fatal(err) + } + //!-parse + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + if err := report.Execute(os.Stdout, result); err != nil { + log.Fatal(err) + } +} + +/* +//!+output +$ go build gopl.io/ch4/issuesreport +$ ./issuesreport repo:golang/go is:open json decoder +13 issues: +---------------------------------------- +Number: 5680 +User: eaigner +Title: encoding/json: set key converter on en/decoder +Age: 750 days +---------------------------------------- +Number: 6050 +User: gopherbot +Title: encoding/json: provide tokenizer +Age: 695 days +---------------------------------------- +... +//!-output +*/ diff --git a/ch4/movie/main.go b/ch4/movie/main.go new file mode 100644 index 0000000..6429410 --- /dev/null +++ b/ch4/movie/main.go @@ -0,0 +1,104 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 108. + +// Movie prints Movies as JSON. +package main + +import ( + "encoding/json" + "fmt" + "log" +) + +//!+ +type Movie struct { + Title string + Year int `json:"released"` + Color bool `json:"color,omitempty"` + Actors []string +} + +var movies = []Movie{ + {Title: "Casablanca", Year: 1942, Color: false, + Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}}, + {Title: "Cool Hand Luke", Year: 1967, Color: true, + Actors: []string{"Paul Newman"}}, + {Title: "Bullitt", Year: 1968, Color: true, + Actors: []string{"Steve McQueen", "Jacqueline Bisset"}}, + // ... +} + +//!- + +func main() { + { + //!+Marshal + data, err := json.Marshal(movies) + if err != nil { + log.Fatalf("JSON marshaling failed: %s", err) + } + fmt.Printf("%s\n", data) + //!-Marshal + } + + { + //!+MarshalIndent + data, err := json.MarshalIndent(movies, "", " ") + if err != nil { + log.Fatalf("JSON marshaling failed: %s", err) + } + fmt.Printf("%s\n", data) + //!-MarshalIndent + + //!+Unmarshal + var titles []struct{ Title string } + if err := json.Unmarshal(data, &titles); err != nil { + log.Fatalf("JSON unmarshaling failed: %s", err) + } + fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]" + //!-Unmarshal + } +} + +/* +//!+output +[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr +id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac +tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true," +Actors":["Steve McQueen","Jacqueline Bisset"]}] +//!-output +*/ + +/* +//!+indented +[ + { + "Title": "Casablanca", + "released": 1942, + "Actors": [ + "Humphrey Bogart", + "Ingrid Bergman" + ] + }, + { + "Title": "Cool Hand Luke", + "released": 1967, + "color": true, + "Actors": [ + "Paul Newman" + ] + }, + { + "Title": "Bullitt", + "released": 1968, + "color": true, + "Actors": [ + "Steve McQueen", + "Jacqueline Bisset" + ] + } +] +//!-indented +*/ diff --git a/ch4/nonempty/main.go b/ch4/nonempty/main.go new file mode 100644 index 0000000..2a20c84 --- /dev/null +++ b/ch4/nonempty/main.go @@ -0,0 +1,47 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 91. + +//!+nonempty + +// Nonempty is an example of an in-place slice algorithm. +package main + +import "fmt" + +// nonempty returns a slice holding only the non-empty strings. +// The underlying array is modified during the call. +func nonempty(strings []string) []string { + i := 0 + for _, s := range strings { + if s != "" { + strings[i] = s + i++ + } + } + return strings[:i] +} + +//!-nonempty + +func main() { + //!+main + data := []string{"one", "", "three"} + fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]` + fmt.Printf("%q\n", data) // `["one" "three" "three"]` + //!-main +} + +//!+alt +func nonempty2(strings []string) []string { + out := strings[:0] // zero-length slice of original + for _, s := range strings { + if s != "" { + out = append(out, s) + } + } + return out +} + +//!-alt diff --git a/ch4/rev/main.go b/ch4/rev/main.go new file mode 100644 index 0000000..039b58a --- /dev/null +++ b/ch4/rev/main.go @@ -0,0 +1,60 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 86. + +// Rev reverses a slice. +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +func main() { + //!+array + a := [...]int{0, 1, 2, 3, 4, 5} + reverse(a[:]) + fmt.Println(a) // "[5 4 3 2 1 0]" + //!-array + + //!+slice + s := []int{0, 1, 2, 3, 4, 5} + // Rotate s left by two positions. + reverse(s[:2]) + reverse(s[2:]) + reverse(s) + fmt.Println(s) // "[2 3 4 5 0 1]" + //!-slice + + // Interactive test of reverse. + input := bufio.NewScanner(os.Stdin) +outer: + for input.Scan() { + var ints []int + for _, s := range strings.Fields(input.Text()) { + x, err := strconv.ParseInt(s, 10, 64) + if err != nil { + fmt.Fprintln(os.Stderr, err) + continue outer + } + ints = append(ints, int(x)) + } + reverse(ints) + fmt.Printf("%v\n", ints) + } + // NOTE: ignoring potential errors from input.Err() +} + +//!+rev +// reverse reverses a slice of ints in place. +func reverse(s []int) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} + +//!-rev diff --git a/ch4/sha256/main.go b/ch4/sha256/main.go new file mode 100644 index 0000000..51e7b24 --- /dev/null +++ b/ch4/sha256/main.go @@ -0,0 +1,25 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 83. + +// The sha256 command computes the SHA256 hash (an array) of a string. +package main + +import "fmt" + +//!+ +import "crypto/sha256" + +func main() { + c1 := sha256.Sum256([]byte("x")) + c2 := sha256.Sum256([]byte("X")) + fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1) + // Output: + // 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881 + // 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015 + // false + // [32]uint8 +} + +//!- diff --git a/ch4/treesort/sort.go b/ch4/treesort/sort.go new file mode 100644 index 0000000..f9ee575 --- /dev/null +++ b/ch4/treesort/sort.go @@ -0,0 +1,50 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 101. + +// Package treesort provides insertion sort using an unbalanced binary tree. +package treesort + +//!+ +type tree struct { + value int + left, right *tree +} + +// Sort sorts values in place. +func Sort(values []int) { + var root *tree + for _, v := range values { + root = add(root, v) + } + appendValues(values[:0], root) +} + +// appendValues appends the elements of t to values in order +// and returns the resulting slice. +func appendValues(values []int, t *tree) []int { + if t != nil { + values = appendValues(values, t.left) + values = append(values, t.value) + values = appendValues(values, t.right) + } + return values +} + +func add(t *tree, value int) *tree { + if t == nil { + // Equivalent to return &tree{value: value}. + t = new(tree) + t.value = value + return t + } + if value < t.value { + t.left = add(t.left, value) + } else { + t.right = add(t.right, value) + } + return t +} + +//!- diff --git a/ch4/treesort/sort_test.go b/ch4/treesort/sort_test.go new file mode 100644 index 0000000..f033e8f --- /dev/null +++ b/ch4/treesort/sort_test.go @@ -0,0 +1,23 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package treesort_test + +import ( + "math/rand" + "sort" + "testing" + + "gopl.io/ch4/treesort" +) + +func TestSort(t *testing.T) { + data := make([]int, 50) + for i := range data { + data[i] = rand.Int() % 50 + } + treesort.Sort(data) + if !sort.IntsAreSorted(data) { + t.Errorf("not sorted: %s", data) + } +} diff --git a/ch5/defer1/defer.go b/ch5/defer1/defer.go new file mode 100644 index 0000000..3d2b80e --- /dev/null +++ b/ch5/defer1/defer.go @@ -0,0 +1,48 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 150. + +// Defer1 demonstrates a deferred call being invoked during a panic. +package main + +import "fmt" + +//!+f +func main() { + f(3) +} + +func f(x int) { + fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0 + defer fmt.Printf("defer %d\n", x) + f(x - 1) +} + +//!-f + +/* +//!+stdout +f(3) +f(2) +f(1) +defer 1 +defer 2 +defer 3 +//!-stdout + +//!+stderr +panic: runtime error: integer divide by zero +main.f(0) + src/gopl.io/ch5/defer1/defer.go:14 +main.f(1) + src/gopl.io/ch5/defer1/defer.go:16 +main.f(2) + src/gopl.io/ch5/defer1/defer.go:16 + +main.f(3) + src/gopl.io/ch5/defer1/defer.go:16 +main.main() + src/gopl.io/ch5/defer1/defer.go:10 +//!-stderr +*/ diff --git a/ch5/defer2/defer.go b/ch5/defer2/defer.go new file mode 100644 index 0000000..25fa881 --- /dev/null +++ b/ch5/defer2/defer.go @@ -0,0 +1,51 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 151. + +// Defer2 demonstrates a deferred call to runtime.Stack during a panic. +package main + +import ( + "fmt" + "os" + "runtime" +) + +//!+ +func main() { + defer printStack() + f(3) +} + +func printStack() { + var buf [4096]byte + n := runtime.Stack(buf[:], false) + os.Stdout.Write(buf[:n]) +} + +//!- + +func f(x int) { + fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0 + defer fmt.Printf("defer %d\n", x) + f(x - 1) +} + +/* +//!+printstack +goroutine 1 [running]: +main.printStack() + src/gopl.io/ch5/defer2/defer.go:20 +main.f(0) + src/gopl.io/ch5/defer2/defer.go:27 +main.f(1) + src/gopl.io/ch5/defer2/defer.go:29 +main.f(2) + src/gopl.io/ch5/defer2/defer.go:29 +main.f(3) + src/gopl.io/ch5/defer2/defer.go:29 +main.main() + src/gopl.io/ch5/defer2/defer.go:15 +//!-printstack +*/ diff --git a/ch5/fetch/main.go b/ch5/fetch/main.go new file mode 100644 index 0000000..c838195 --- /dev/null +++ b/ch5/fetch/main.go @@ -0,0 +1,54 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 148. + +// Fetch saves the contents of a URL into a local file. +package main + +import ( + "fmt" + "io" + "net/http" + "os" + "path" +) + +//!+ +// Fetch downloads the URL and returns the +// name and length of the local file. +func fetch(url string) (filename string, n int64, err error) { + resp, err := http.Get(url) + if err != nil { + return "", 0, err + } + defer resp.Body.Close() + + local := path.Base(resp.Request.URL.Path) + if local == "/" { + local = "index.html" + } + f, err := os.Create(local) + if err != nil { + return "", 0, err + } + n, err = io.Copy(f, resp.Body) + // Close file, but prefer error from Copy, if any. + if closeErr := f.Close(); err == nil { + err = closeErr + } + return local, n, err +} + +//!- + +func main() { + for _, url := range os.Args[1:] { + local, n, err := fetch(url) + if err != nil { + fmt.Fprintf(os.Stderr, "fetch %s: %v\n", url, err) + continue + } + fmt.Fprintf(os.Stderr, "%s => %s (%d bytes).\n", url, local, n) + } +} diff --git a/ch5/findlinks1/main.go b/ch5/findlinks1/main.go index bfae904..8996671 100644 --- a/ch5/findlinks1/main.go +++ b/ch5/findlinks1/main.go @@ -1,4 +1,7 @@ // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 122. //!+main // Findlinks1 prints the links in an HTML document read from standard input. diff --git a/ch5/findlinks2/main.go b/ch5/findlinks2/main.go new file mode 100644 index 0000000..35ba2a5 --- /dev/null +++ b/ch5/findlinks2/main.go @@ -0,0 +1,69 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 125. + +// Findlinks2 does an HTTP GET on each URL, parses the +// result as HTML, and prints the links within it. +// +// Usage: +// findlinks url ... +package main + +import ( + "fmt" + "net/http" + "os" + + "golang.org/x/net/html" +) + +// visit appends to links each link found in n, and returns the result. +func visit(links []string, n *html.Node) []string { + if n.Type == html.ElementNode && n.Data == "a" { + for _, a := range n.Attr { + if a.Key == "href" { + links = append(links, a.Val) + } + } + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + links = visit(links, c) + } + return links +} + +//!+ +func main() { + for _, url := range os.Args[1:] { + links, err := findLinks(url) + if err != nil { + fmt.Fprintf(os.Stderr, "findlinks2: %v\n", err) + continue + } + for _, link := range links { + fmt.Println(link) + } + } +} + +// findLinks performs an HTTP GET request for url, parses the +// response as HTML, and extracts and returns the links. +func findLinks(url string) ([]string, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("getting %s: %s", url, resp.Status) + } + doc, err := html.Parse(resp.Body) + resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("parsing %s as HTML: %v", url, err) + } + return visit(nil, doc), nil +} + +//!- diff --git a/ch5/findlinks3/findlinks.go b/ch5/findlinks3/findlinks.go new file mode 100644 index 0000000..f45b39e --- /dev/null +++ b/ch5/findlinks3/findlinks.go @@ -0,0 +1,56 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 139. + +// Findlinks3 crawls the web, starting with the URLs on the command line. +package main + +import ( + "fmt" + "log" + "os" + + "gopl.io/ch5/links" +) + +//!+breadthFirst +// breadthFirst calls f for each item in the worklist. +// Any items returned by f are added to the worklist. +// f is called at most once for each item. +func breadthFirst(f func(item string) []string, worklist []string) { + seen := make(map[string]bool) + for len(worklist) > 0 { + items := worklist + worklist = nil + for _, item := range items { + if !seen[item] { + seen[item] = true + worklist = append(worklist, f(item)...) + } + } + } +} + +//!-breadthFirst + +//!+crawl +func crawl(url string) []string { + fmt.Println(url) + list, err := links.Extract(url) + if err != nil { + log.Print(err) + } + return list +} + +//!-crawl + +//!+main +func main() { + // Crawl the web breadth-first, + // starting from the command-line arguments. + breadthFirst(crawl, os.Args[1:]) +} + +//!-main diff --git a/ch5/links/links.go b/ch5/links/links.go new file mode 100644 index 0000000..2df9e14 --- /dev/null +++ b/ch5/links/links.go @@ -0,0 +1,67 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 138. +//!+Extract + +// Package links provides a link-extraction function. +package links + +import ( + "fmt" + "net/http" + + "golang.org/x/net/html" +) + +// Extract makes an HTTP GET request to the specified URL, parses +// the response as HTML, and returns the links in the HTML document. +func Extract(url string) ([]string, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("getting %s: %s", url, resp.Status) + } + + doc, err := html.Parse(resp.Body) + resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("parsing %s as HTML: %v", url, err) + } + + var links []string + visitNode := func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "a" { + for _, a := range n.Attr { + if a.Key != "href" { + continue + } + link, err := resp.Request.URL.Parse(a.Val) + if err != nil { + continue // ignore bad URLs + } + links = append(links, link.String()) + } + } + } + forEachNode(doc, visitNode, nil) + return links, nil +} + +//!-Extract + +// Copied from gopl.io/ch5/outline2. +func forEachNode(n *html.Node, pre, post func(n *html.Node)) { + if pre != nil { + pre(n) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + forEachNode(c, pre, post) + } + if post != nil { + post(n) + } +} diff --git a/ch5/outline/main.go b/ch5/outline/main.go new file mode 100644 index 0000000..d3ccbf9 --- /dev/null +++ b/ch5/outline/main.go @@ -0,0 +1,36 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 123. + +// Outline prints the outline of an HTML document tree. +package main + +import ( + "fmt" + "os" + + "golang.org/x/net/html" +) + +//!+ +func main() { + doc, err := html.Parse(os.Stdin) + if err != nil { + fmt.Fprintf(os.Stderr, "outline: %v\n", err) + os.Exit(1) + } + outline(nil, doc) +} + +func outline(stack []string, n *html.Node) { + if n.Type == html.ElementNode { + stack = append(stack, n.Data) // push tag + fmt.Println(stack) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + outline(stack, c) + } +} + +//!- diff --git a/ch5/outline2/outline.go b/ch5/outline2/outline.go new file mode 100644 index 0000000..d3aee13 --- /dev/null +++ b/ch5/outline2/outline.go @@ -0,0 +1,80 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 133. + +// Outline prints the outline of an HTML document tree. +package main + +import ( + "fmt" + "net/http" + "os" + + "golang.org/x/net/html" +) + +func main() { + for _, url := range os.Args[1:] { + outline(url) + } +} + +func outline(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + doc, err := html.Parse(resp.Body) + if err != nil { + return err + } + + //!+call + forEachNode(doc, startElement, endElement) + //!-call + + return nil +} + +//!+forEachNode +// forEachNode calls the functions pre(x) and post(x) for each node +// x in the tree rooted at n. Both functions are optional. +// pre is called before the children are visited (preorder) and +// post is called after (postorder). +func forEachNode(n *html.Node, pre, post func(n *html.Node)) { + if pre != nil { + pre(n) + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + forEachNode(c, pre, post) + } + + if post != nil { + post(n) + } +} + +//!-forEachNode + +//!+startend +var depth int + +func startElement(n *html.Node) { + if n.Type == html.ElementNode { + fmt.Printf("%*s<%s>\n", depth*2, "", n.Data) + depth++ + } +} + +func endElement(n *html.Node) { + if n.Type == html.ElementNode { + depth-- + fmt.Printf("%*s\n", depth*2, "", n.Data) + } +} + +//!-startend diff --git a/ch5/squares/main.go b/ch5/squares/main.go new file mode 100644 index 0000000..1e10060 --- /dev/null +++ b/ch5/squares/main.go @@ -0,0 +1,30 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 135. + +// The squares program demonstrates a function value with state. +package main + +import "fmt" + +//!+ +// squares returns a function that returns +// the next square number each time it is called. +func squares() func() int { + var x int + return func() int { + x++ + return x * x + } +} + +func main() { + f := squares() + fmt.Println(f()) // "1" + fmt.Println(f()) // "4" + fmt.Println(f()) // "9" + fmt.Println(f()) // "16" +} + +//!- diff --git a/ch5/sum/main.go b/ch5/sum/main.go new file mode 100644 index 0000000..0a60d35 --- /dev/null +++ b/ch5/sum/main.go @@ -0,0 +1,33 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 142. + +// The sum program demonstrates a variadic function. +package main + +import "fmt" + +//!+ +func sum(vals ...int) int { + total := 0 + for _, val := range vals { + total += val + } + return total +} + +//!- + +func main() { + //!+main + fmt.Println(sum()) // "0" + fmt.Println(sum(3)) // "3" + fmt.Println(sum(1, 2, 3, 4)) // "10" + //!-main + + //!+slice + values := []int{1, 2, 3, 4} + fmt.Println(sum(values...)) // "10" + //!-slice +} diff --git a/ch5/title1/title.go b/ch5/title1/title.go new file mode 100644 index 0000000..65b5276 --- /dev/null +++ b/ch5/title1/title.go @@ -0,0 +1,82 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 144. + +// Title1 prints the title of an HTML document specified by a URL. +package main + +/* +//!+output +$ go build gopl.io/ch5/title1 +$ ./title1 http://gopl.io +The Go Programming Language +$ ./title1 https://golang.org/doc/effective_go.html +Effective Go - The Go Programming Language +$ ./title1 https://golang.org/doc/gopher/frontpage.png +title: https://golang.org/doc/gopher/frontpage.png + has type image/png, not text/html +//!-output +*/ + +import ( + "fmt" + "net/http" + "os" + "strings" + + "golang.org/x/net/html" +) + +// Copied from gopl.io/ch5/outline2. +func forEachNode(n *html.Node, pre, post func(n *html.Node)) { + if pre != nil { + pre(n) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + forEachNode(c, pre, post) + } + if post != nil { + post(n) + } +} + +//!+ +func title(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + + // Check Content-Type is HTML (e.g., "text/html; charset=utf-8"). + ct := resp.Header.Get("Content-Type") + if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") { + resp.Body.Close() + return fmt.Errorf("%s has type %s, not text/html", url, ct) + } + + doc, err := html.Parse(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("parsing %s as HTML: %v", url, err) + } + + visitNode := func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "title" && + n.FirstChild != nil { + fmt.Println(n.FirstChild.Data) + } + } + forEachNode(doc, visitNode, nil) + return nil +} + +//!- + +func main() { + for _, arg := range os.Args[1:] { + if err := title(arg); err != nil { + fmt.Fprintf(os.Stderr, "title: %v\n", err) + } + } +} diff --git a/ch5/title2/title.go b/ch5/title2/title.go new file mode 100644 index 0000000..1641739 --- /dev/null +++ b/ch5/title2/title.go @@ -0,0 +1,72 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 145. + +// Title2 prints the title of an HTML document specified by a URL. +// It uses defer to simplify closing the response body stream. +package main + +import ( + "fmt" + "net/http" + "os" + "strings" + + "golang.org/x/net/html" +) + +// Copied from gopl.io/ch5/outline2. +func forEachNode(n *html.Node, pre, post func(n *html.Node)) { + if pre != nil { + pre(n) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + forEachNode(c, pre, post) + } + if post != nil { + post(n) + } +} + +//!+ +func title(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + ct := resp.Header.Get("Content-Type") + if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") { + return fmt.Errorf("%s has type %s, not text/html", url, ct) + } + + doc, err := html.Parse(resp.Body) + if err != nil { + return fmt.Errorf("parsing %s as HTML: %v", url, err) + } + + // ...print doc's title element... + //!- + visitNode := func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "title" && + n.FirstChild != nil { + fmt.Println(n.FirstChild.Data) + } + } + forEachNode(doc, visitNode, nil) + //!+ + + return nil +} + +//!- + +func main() { + for _, arg := range os.Args[1:] { + if err := title(arg); err != nil { + fmt.Fprintf(os.Stderr, "title: %v\n", err) + } + } +} diff --git a/ch5/title3/title.go b/ch5/title3/title.go new file mode 100644 index 0000000..727b209 --- /dev/null +++ b/ch5/title3/title.go @@ -0,0 +1,99 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 153. + +// Title3 prints the title of an HTML document specified by a URL. +package main + +import ( + "fmt" + "net/http" + "os" + "strings" + + "golang.org/x/net/html" +) + +// Copied from gopl.io/ch5/outline2. +func forEachNode(n *html.Node, pre, post func(n *html.Node)) { + if pre != nil { + pre(n) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + forEachNode(c, pre, post) + } + if post != nil { + post(n) + } +} + +//!+ +// soleTitle returns the text of the first non-empty title element +// in doc, and an error if there was not exactly one. +func soleTitle(doc *html.Node) (title string, err error) { + type bailout struct{} + + defer func() { + switch p := recover(); p { + case nil: + // no panic + case bailout{}: + // "expected" panic + err = fmt.Errorf("multiple title elements") + default: + panic(p) // unexpected panic; carry on panicking + } + }() + + // Bail out of recursion if we find more than one non-empty title. + forEachNode(doc, func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "title" && + n.FirstChild != nil { + if title != "" { + panic(bailout{}) // multiple title elements + } + title = n.FirstChild.Data + } + }, nil) + if title == "" { + return "", fmt.Errorf("no title element") + } + return title, nil +} + +//!- + +func title(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + + // Check Content-Type is HTML (e.g., "text/html; charset=utf-8"). + ct := resp.Header.Get("Content-Type") + if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") { + resp.Body.Close() + return fmt.Errorf("%s has type %s, not text/html", url, ct) + } + + doc, err := html.Parse(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("parsing %s as HTML: %v", url, err) + } + title, err := soleTitle(doc) + if err != nil { + return err + } + fmt.Println(title) + return nil +} + +func main() { + for _, arg := range os.Args[1:] { + if err := title(arg); err != nil { + fmt.Fprintf(os.Stderr, "title: %v\n", err) + } + } +} diff --git a/ch5/toposort/main.go b/ch5/toposort/main.go new file mode 100644 index 0000000..1c5e899 --- /dev/null +++ b/ch5/toposort/main.go @@ -0,0 +1,69 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 136. + +// The toposort program prints the nodes of a DAG in topological order. +package main + +import ( + "fmt" + "sort" +) + +//!+table +// prereqs maps computer science courses to their prerequisites. +var prereqs = map[string][]string{ + "algorithms": {"data structures"}, + "calculus": {"linear algebra"}, + + "compilers": { + "data structures", + "formal languages", + "computer organization", + }, + + "data structures": {"discrete math"}, + "databases": {"data structures"}, + "discrete math": {"intro to programming"}, + "formal languages": {"discrete math"}, + "networks": {"operating systems"}, + "operating systems": {"data structures", "computer organization"}, + "programming languages": {"data structures", "computer organization"}, +} + +//!-table + +//!+main +func main() { + for i, course := range topoSort(prereqs) { + fmt.Printf("%d:\t%s\n", i+1, course) + } +} + +func topoSort(m map[string][]string) []string { + var order []string + seen := make(map[string]bool) + var visitAll func(items []string) + + visitAll = func(items []string) { + for _, item := range items { + if !seen[item] { + seen[item] = true + visitAll(m[item]) + order = append(order, item) + } + } + } + + var keys []string + for key := range m { + keys = append(keys, key) + } + + sort.Strings(keys) + visitAll(keys) + return order +} + +//!-main diff --git a/ch5/trace/main.go b/ch5/trace/main.go new file mode 100644 index 0000000..cc1962f --- /dev/null +++ b/ch5/trace/main.go @@ -0,0 +1,40 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 146. + +// The trace program uses defer to add entry/exit diagnostics to a function. +package main + +import ( + "log" + "time" +) + +//!+main +func bigSlowOperation() { + defer trace("bigSlowOperation")() // don't forget the extra parentheses + // ...lots of work... + time.Sleep(10 * time.Second) // simulate slow operation by sleeping +} + +func trace(msg string) func() { + start := time.Now() + log.Printf("enter %s", msg) + return func() { log.Printf("exit %s (%s)", msg, time.Since(start)) } +} + +//!-main + +func main() { + bigSlowOperation() +} + +/* +!+output +$ go build gopl.io/ch5/trace +$ ./trace +2015/11/18 09:53:26 enter bigSlowOperation +2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s) +!-output +*/ diff --git a/ch5/wait/wait.go b/ch5/wait/wait.go new file mode 100644 index 0000000..f5313e7 --- /dev/null +++ b/ch5/wait/wait.go @@ -0,0 +1,50 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 130. + +// The wait program waits for an HTTP server to start responding. +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "time" +) + +//!+ +// WaitForServer attempts to contact the server of a URL. +// It tries for one minute using exponential back-off. +// It reports an error if all attempts fail. +func WaitForServer(url string) error { + const timeout = 1 * time.Minute + deadline := time.Now().Add(timeout) + for tries := 0; time.Now().Before(deadline); tries++ { + _, err := http.Head(url) + if err == nil { + return nil // success + } + log.Printf("server not responding (%s); retrying...", err) + time.Sleep(time.Second << uint(tries)) // exponential back-off + } + return fmt.Errorf("server %s failed to respond after %s", url, timeout) +} + +//!- + +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "usage: wait url\n") + os.Exit(1) + } + url := os.Args[1] + //!+main + // (In function main.) + if err := WaitForServer(url); err != nil { + fmt.Fprintf(os.Stderr, "Site is down: %v\n", err) + os.Exit(1) + } + //!-main +} diff --git a/ch6/coloredpoint/main.go b/ch6/coloredpoint/main.go new file mode 100644 index 0000000..4b2335c --- /dev/null +++ b/ch6/coloredpoint/main.go @@ -0,0 +1,89 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 161. + +// Coloredpoint demonstrates struct embedding. +package main + +import ( + "fmt" + "math" +) + +//!+decl +import "image/color" + +type Point struct{ X, Y float64 } + +type ColoredPoint struct { + Point + Color color.RGBA +} + +//!-decl + +func (p Point) Distance(q Point) float64 { + dX := q.X - p.X + dY := q.Y - p.Y + return math.Sqrt(dX*dX + dY*dY) +} + +func (p *Point) ScaleBy(factor float64) { + p.X *= factor + p.Y *= factor +} + +func main() { + //!+main + red := color.RGBA{255, 0, 0, 255} + blue := color.RGBA{0, 0, 255, 255} + var p = ColoredPoint{Point{1, 1}, red} + var q = ColoredPoint{Point{5, 4}, blue} + fmt.Println(p.Distance(q.Point)) // "5" + p.ScaleBy(2) + q.ScaleBy(2) + fmt.Println(p.Distance(q.Point)) // "10" + //!-main +} + +/* +//!+error + p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point +//!-error +*/ + +func init() { + //!+methodexpr + p := Point{1, 2} + q := Point{4, 6} + + distance := Point.Distance // method expression + fmt.Println(distance(p, q)) // "5" + fmt.Printf("%T\n", distance) // "func(Point, Point) float64" + + scale := (*Point).ScaleBy + scale(&p, 2) + fmt.Println(p) // "{2 4}" + fmt.Printf("%T\n", scale) // "func(*Point, float64)" + //!-methodexpr +} + +func init() { + red := color.RGBA{255, 0, 0, 255} + blue := color.RGBA{0, 0, 255, 255} + + //!+indirect + type ColoredPoint struct { + *Point + Color color.RGBA + } + + p := ColoredPoint{&Point{1, 1}, red} + q := ColoredPoint{&Point{5, 4}, blue} + fmt.Println(p.Distance(*q.Point)) // "5" + q.Point = p.Point // p and q now share the same Point + p.ScaleBy(2) + fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}" + //!-indirect +} diff --git a/ch6/geometry/geometry.go b/ch6/geometry/geometry.go new file mode 100644 index 0000000..2400e3e --- /dev/null +++ b/ch6/geometry/geometry.go @@ -0,0 +1,42 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 156. + +// Package geometry defines simple types for plane geometry. +//!+point +package geometry + +import "math" + +type Point struct{ X, Y float64 } + +// traditional function +func Distance(p, q Point) float64 { + return math.Hypot(q.X-p.X, q.Y-p.Y) +} + +// same thing, but as a method of the Point type +func (p Point) Distance(q Point) float64 { + return math.Hypot(q.X-p.X, q.Y-p.Y) +} + +//!-point + +//!+path + +// A Path is a journey connecting the points with straight lines. +type Path []Point + +// Distance returns the distance traveled along the path. +func (path Path) Distance() float64 { + sum := 0.0 + for i := range path { + if i > 0 { + sum += path[i-1].Distance(path[i]) + } + } + return sum +} + +//!-path diff --git a/ch6/intset/intset.go b/ch6/intset/intset.go new file mode 100644 index 0000000..78227e5 --- /dev/null +++ b/ch6/intset/intset.go @@ -0,0 +1,73 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 165. + +// Package intset provides a set of integers based on a bit vector. +package intset + +import ( + "bytes" + "fmt" +) + +//!+intset + +// An IntSet is a set of small non-negative integers. +// Its zero value represents the empty set. +type IntSet struct { + words []uint64 +} + +// Has reports whether the set contains the non-negative value x. +func (s *IntSet) Has(x int) bool { + word, bit := x/64, uint(x%64) + return word < len(s.words) && s.words[word]&(1<= len(s.words) { + s.words = append(s.words, 0) + } + s.words[word] |= 1 << bit +} + +// UnionWith sets s to the union of s and t. +func (s *IntSet) UnionWith(t *IntSet) { + for i, tword := range t.words { + if i < len(s.words) { + s.words[i] |= tword + } else { + s.words = append(s.words, tword) + } + } +} + +//!-intset + +//!+string + +// String returns the set as a string of the form "{1 2 3}". +func (s *IntSet) String() string { + var buf bytes.Buffer + buf.WriteByte('{') + for i, word := range s.words { + if word == 0 { + continue + } + for j := 0; j < 64; j++ { + if word&(1< len("{") { + buf.WriteByte(' ') + } + fmt.Fprintf(&buf, "%d", 64*i+j) + } + } + } + buf.WriteByte('}') + return buf.String() +} + +//!-string diff --git a/ch6/intset/intset_test.go b/ch6/intset/intset_test.go new file mode 100644 index 0000000..2a9bd26 --- /dev/null +++ b/ch6/intset/intset_test.go @@ -0,0 +1,50 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package intset + +import "fmt" + +func Example1() { + //!+main + var x, y IntSet + x.Add(1) + x.Add(144) + x.Add(9) + fmt.Println(x.String()) // "{1 9 144}" + + y.Add(9) + y.Add(42) + fmt.Println(y.String()) // "{9 42}" + + x.UnionWith(&y) + fmt.Println(x.String()) // "{1 9 42 144}" + + fmt.Println(x.Has(9), x.Has(123)) // "true false" + //!-main + + // Output: + // {1 9 144} + // {9 42} + // {1 9 42 144} + // true false +} + +func Example2() { + var x IntSet + x.Add(1) + x.Add(144) + x.Add(9) + x.Add(42) + + //!+note + fmt.Println(&x) // "{1 9 42 144}" + fmt.Println(x.String()) // "{1 9 42 144}" + fmt.Println(x) // "{[4398046511618 0 65536]}" + //!-note + + // Output: + // {1 9 42 144} + // {1 9 42 144} + // {[4398046511618 0 65536]} +} diff --git a/ch6/urlvalues/main.go b/ch6/urlvalues/main.go new file mode 100644 index 0000000..7e226d5 --- /dev/null +++ b/ch6/urlvalues/main.go @@ -0,0 +1,53 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 160. + +// The urlvalues command demonstrates a map type with methods. +package main + +/* +//!+values +package url + +// Values maps a string key to a list of values. +type Values map[string][]string + +// Get returns the first value associated with the given key, +// or "" if there are none. +func (v Values) Get(key string) string { + if vs := v[key]; len(vs) > 0 { + return vs[0] + } + return "" +} + +// Add adds the value to key. +// It appends to any existing values associated with key. +func (v Values) Add(key, value string) { + v[key] = append(v[key], value) +} +//!-values +*/ + +import ( + "fmt" + "net/url" +) + +func main() { + //!+main + m := url.Values{"lang": {"en"}} // direct construction + m.Add("item", "1") + m.Add("item", "2") + + fmt.Println(m.Get("lang")) // "en" + fmt.Println(m.Get("q")) // "" + fmt.Println(m.Get("item")) // "1" (first value) + fmt.Println(m["item"]) // "[1 2]" (direct map access) + + m = nil + fmt.Println(m.Get("item")) // "" + m.Add("item", "3") // panic: assignment to entry in nil map + //!-main +} diff --git a/ch7/bytecounter/main.go b/ch7/bytecounter/main.go new file mode 100644 index 0000000..cb6e175 --- /dev/null +++ b/ch7/bytecounter/main.go @@ -0,0 +1,35 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 173. + +// Bytecounter demonstrates an implementation of io.Writer that counts bytes. +package main + +import ( + "fmt" +) + +//!+bytecounter + +type ByteCounter int + +func (c *ByteCounter) Write(p []byte) (int, error) { + *c += ByteCounter(len(p)) // convert int to ByteCounter + return len(p), nil +} + +//!-bytecounter + +func main() { + //!+main + var c ByteCounter + c.Write([]byte("hello")) + fmt.Println(c) // "5", = len("hello") + + c = 0 // reset the counter + var name = "Dolly" + fmt.Fprintf(&c, "hello, %s", name) + fmt.Println(c) // "12", = len("hello, Dolly") + //!-main +} diff --git a/ch7/eval/ast.go b/ch7/eval/ast.go new file mode 100644 index 0000000..4b6f381 --- /dev/null +++ b/ch7/eval/ast.go @@ -0,0 +1,40 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package eval + +// An Expr is an arithmetic expression. +type Expr interface { + // Eval returns the value of this Expr in the environment env. + Eval(env Env) float64 + // Check reports errors in this Expr and adds its Vars to the set. + Check(vars map[Var]bool) error +} + +//!+ast + +// A Var identifies a variable, e.g., x. +type Var string + +// A literal is a numeric constant, e.g., 3.141. +type literal float64 + +// A unary represents a unary operator expression, e.g., -x. +type unary struct { + op rune // one of '+', '-' + x Expr +} + +// A binary represents a binary operator expression, e.g., x+y. +type binary struct { + op rune // one of '+', '-', '*', '/' + x, y Expr +} + +// A call represents a function call expression, e.g., sin(x). +type call struct { + fn string // one of "pow", "sin", "sqrt" + args []Expr +} + +//!-ast diff --git a/ch7/eval/check.go b/ch7/eval/check.go new file mode 100644 index 0000000..7fddcbe --- /dev/null +++ b/ch7/eval/check.go @@ -0,0 +1,58 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package eval + +import ( + "fmt" + "strings" +) + +//!+Check + +func (v Var) Check(vars map[Var]bool) error { + vars[v] = true + return nil +} + +func (literal) Check(vars map[Var]bool) error { + return nil +} + +func (u unary) Check(vars map[Var]bool) error { + if !strings.ContainsRune("+-", u.op) { + return fmt.Errorf("unexpected unary op %q", u.op) + } + return u.x.Check(vars) +} + +func (b binary) Check(vars map[Var]bool) error { + if !strings.ContainsRune("+-*/", b.op) { + return fmt.Errorf("unexpected binary op %q", b.op) + } + if err := b.x.Check(vars); err != nil { + return err + } + return b.y.Check(vars) +} + +func (c call) Check(vars map[Var]bool) error { + arity, ok := numParams[c.fn] + if !ok { + return fmt.Errorf("unknown function %q", c.fn) + } + if len(c.args) != arity { + return fmt.Errorf("call to %s has %d args, want %d", + c.fn, len(c.args), arity) + } + for _, arg := range c.args { + if err := arg.Check(vars); err != nil { + return err + } + } + return nil +} + +var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1} + +//!-Check diff --git a/ch7/eval/coverage_test.go b/ch7/eval/coverage_test.go new file mode 100644 index 0000000..190dd37 --- /dev/null +++ b/ch7/eval/coverage_test.go @@ -0,0 +1,48 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package eval + +import ( + "fmt" + "math" + "testing" +) + +//!+TestCoverage +func TestCoverage(t *testing.T) { + var tests = []struct { + input string + env Env + want string // expected error from Parse/Check or result from Eval + }{ + {"x % 2", nil, "unexpected '%'"}, + {"!true", nil, "unexpected '!'"}, + {"log(10)", nil, `unknown function "log"`}, + {"sqrt(1, 2)", nil, "call to sqrt has 2 args, want 1"}, + {"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"}, + {"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"}, + {"5 / 9 * (F - 32)", Env{"F": -40}, "-40"}, + } + + for _, test := range tests { + expr, err := Parse(test.input) + if err == nil { + err = expr.Check(map[Var]bool{}) + } + if err != nil { + if err.Error() != test.want { + t.Errorf("%s: got %q, want %q", test.input, err, test.want) + } + continue + } + + got := fmt.Sprintf("%.6g", expr.Eval(test.env)) + if got != test.want { + t.Errorf("%s: %v => %s, want %s", + test.input, test.env, got, test.want) + } + } +} + +//!-TestCoverage diff --git a/ch7/eval/eval.go b/ch7/eval/eval.go new file mode 100644 index 0000000..4ab1dd7 --- /dev/null +++ b/ch7/eval/eval.go @@ -0,0 +1,70 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 198. + +// Package eval provides an expression evaluator. +package eval + +import ( + "fmt" + "math" +) + +//!+env + +type Env map[Var]float64 + +//!-env + +//!+Eval1 + +func (v Var) Eval(env Env) float64 { + return env[v] +} + +func (l literal) Eval(_ Env) float64 { + return float64(l) +} + +//!-Eval1 + +//!+Eval2 + +func (u unary) Eval(env Env) float64 { + switch u.op { + case '+': + return +u.x.Eval(env) + case '-': + return -u.x.Eval(env) + } + panic(fmt.Sprintf("unsupported unary operator: %q", u.op)) +} + +func (b binary) Eval(env Env) float64 { + switch b.op { + case '+': + return b.x.Eval(env) + b.y.Eval(env) + case '-': + return b.x.Eval(env) - b.y.Eval(env) + case '*': + return b.x.Eval(env) * b.y.Eval(env) + case '/': + return b.x.Eval(env) / b.y.Eval(env) + } + panic(fmt.Sprintf("unsupported binary operator: %q", b.op)) +} + +func (c call) Eval(env Env) float64 { + switch c.fn { + case "pow": + return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env)) + case "sin": + return math.Sin(c.args[0].Eval(env)) + case "sqrt": + return math.Sqrt(c.args[0].Eval(env)) + } + panic(fmt.Sprintf("unsupported function call: %s", c.fn)) +} + +//!-Eval2 diff --git a/ch7/eval/eval_test.go b/ch7/eval/eval_test.go new file mode 100644 index 0000000..e82ba36 --- /dev/null +++ b/ch7/eval/eval_test.go @@ -0,0 +1,113 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package eval + +import ( + "fmt" + "math" + "testing" +) + +//!+Eval +func TestEval(t *testing.T) { + tests := []struct { + expr string + env Env + want string + }{ + {"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"}, + {"pow(x, 3) + pow(y, 3)", Env{"x": 12, "y": 1}, "1729"}, + {"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"}, + {"5 / 9 * (F - 32)", Env{"F": -40}, "-40"}, + {"5 / 9 * (F - 32)", Env{"F": 32}, "0"}, + {"5 / 9 * (F - 32)", Env{"F": 212}, "100"}, + //!-Eval + // additional tests that don't appear in the book + {"-1 + -x", Env{"x": 1}, "-2"}, + {"-1 - x", Env{"x": 1}, "-2"}, + //!+Eval + } + var prevExpr string + for _, test := range tests { + // Print expr only when it changes. + if test.expr != prevExpr { + fmt.Printf("\n%s\n", test.expr) + prevExpr = test.expr + } + expr, err := Parse(test.expr) + if err != nil { + t.Error(err) // parse error + continue + } + got := fmt.Sprintf("%.6g", expr.Eval(test.env)) + fmt.Printf("\t%v => %s\n", test.env, got) + if got != test.want { + t.Errorf("%s.Eval() in %s = %q, want %q\n", + test.expr, test.env, got, test.want) + } + } +} + +//!-Eval + +/* +//!+output +sqrt(A / pi) + map[A:87616 pi:3.141592653589793] => 167 + +pow(x, 3) + pow(y, 3) + map[x:12 y:1] => 1729 + map[x:9 y:10] => 1729 + +5 / 9 * (F - 32) + map[F:-40] => -40 + map[F:32] => 0 + map[F:212] => 100 +//!-output + +// Additional outputs that don't appear in the book. + +-1 - x + map[x:1] => -2 + +-1 + -x + map[x:1] => -2 +*/ + +func TestErrors(t *testing.T) { + for _, test := range []struct{ expr, wantErr string }{ + {"x % 2", "unexpected '%'"}, + {"math.Pi", "unexpected '.'"}, + {"!true", "unexpected '!'"}, + {`"hello"`, "unexpected '\"'"}, + {"log(10)", `unknown function "log"`}, + {"sqrt(1, 2)", "call to sqrt has 2 args, want 1"}, + } { + expr, err := Parse(test.expr) + if err == nil { + vars := make(map[Var]bool) + err = expr.Check(vars) + if err == nil { + t.Errorf("unexpected success: %s", test.expr) + continue + } + } + fmt.Printf("%-20s%v\n", test.expr, err) // (for book) + if err.Error() != test.wantErr { + t.Errorf("got error %s, want %s", err, test.wantErr) + } + } +} + +/* +//!+errors +x % 2 unexpected '%' +math.Pi unexpected '.' +!true unexpected '!' +"hello" unexpected '"' + +log(10) unknown function "log" +sqrt(1, 2) call to sqrt has 2 args, want 1 +//!-errors +*/ diff --git a/ch7/eval/parse.go b/ch7/eval/parse.go new file mode 100644 index 0000000..e5f82fa --- /dev/null +++ b/ch7/eval/parse.go @@ -0,0 +1,160 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package eval + +import ( + "fmt" + "strconv" + "strings" + "text/scanner" +) + +// ---- lexer ---- + +// This lexer is similar to the one described in Chapter 13. +type lexer struct { + scan scanner.Scanner + token rune // current lookahead token +} + +func (lex *lexer) next() { lex.token = lex.scan.Scan() } +func (lex *lexer) text() string { return lex.scan.TokenText() } + +type lexPanic string + +// describe returns a string describing the current token, for use in errors. +func (lex *lexer) describe() string { + switch lex.token { + case scanner.EOF: + return "end of file" + case scanner.Ident: + return fmt.Sprintf("identifier %s", lex.text()) + case scanner.Int, scanner.Float: + return fmt.Sprintf("number %s", lex.text()) + } + return fmt.Sprintf("%q", rune(lex.token)) // any other rune +} + +func precedence(op rune) int { + switch op { + case '*', '/': + return 2 + case '+', '-': + return 1 + } + return 0 +} + +// ---- parser ---- + +// Parse parses the input string as an arithmetic expression. +// +// expr = num a literal number, e.g., 3.14159 +// | id a variable name, e.g., x +// | id '(' expr ',' ... ')' a function call +// | '-' expr a unary operator (+-) +// | expr '+' expr a binary operator (+-*/) +// +func Parse(input string) (_ Expr, err error) { + defer func() { + switch x := recover().(type) { + case nil: + // no panic + case lexPanic: + err = fmt.Errorf("%s", x) + default: + // unexpected panic: resume state of panic. + panic(x) + } + }() + lex := new(lexer) + lex.scan.Init(strings.NewReader(input)) + lex.scan.Mode = scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats + lex.next() // initial lookahead + e := parseExpr(lex) + if lex.token != scanner.EOF { + return nil, fmt.Errorf("unexpected %s", lex.describe()) + } + return e, nil +} + +func parseExpr(lex *lexer) Expr { return parseBinary(lex, 1) } + +// binary = unary ('+' binary)* +// parseBinary stops when it encounters an +// operator of lower precedence than prec1. +func parseBinary(lex *lexer, prec1 int) Expr { + lhs := parseUnary(lex) + for prec := precedence(lex.token); prec >= prec1; prec-- { + for precedence(lex.token) == prec { + op := lex.token + lex.next() // consume operator + rhs := parseBinary(lex, prec+1) + lhs = binary{op, lhs, rhs} + } + } + return lhs +} + +// unary = '+' expr | primary +func parseUnary(lex *lexer) Expr { + if lex.token == '+' || lex.token == '-' { + op := lex.token + lex.next() // consume '+' or '-' + return unary{op, parseUnary(lex)} + } + return parsePrimary(lex) +} + +// primary = id +// | id '(' expr ',' ... ',' expr ')' +// | num +// | '(' expr ')' +func parsePrimary(lex *lexer) Expr { + switch lex.token { + case scanner.Ident: + id := lex.text() + lex.next() // consume Ident + if lex.token != '(' { + return Var(id) + } + lex.next() // consume '(' + var args []Expr + if lex.token != ')' { + for { + args = append(args, parseExpr(lex)) + if lex.token != ',' { + break + } + lex.next() // consume ',' + } + if lex.token != ')' { + msg := fmt.Sprintf("got %s, want ')'", lex.token) + panic(lexPanic(msg)) + } + } + lex.next() // consume ')' + return call{id, args} + + case scanner.Int, scanner.Float: + f, err := strconv.ParseFloat(lex.text(), 64) + if err != nil { + panic(lexPanic(err.Error())) + } + lex.next() // consume number + return literal(f) + + case '(': + lex.next() // consume ')' + e := parseExpr(lex) + if lex.token != ')' { + msg := fmt.Sprintf("got %s, want ')'", lex.describe()) + panic(lexPanic(msg)) + } + lex.next() // consume ')' + return e + } + msg := fmt.Sprintf("unexpected %s", lex.describe()) + panic(lexPanic(msg)) +} diff --git a/ch7/eval/print.go b/ch7/eval/print.go new file mode 100644 index 0000000..1d3be26 --- /dev/null +++ b/ch7/eval/print.go @@ -0,0 +1,51 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +//+build ignore + +package eval + +import ( + "bytes" + "fmt" +) + +// Format formats an expression as a string. +// It does not attempt to remove unnecessary parens. +func Format(e Expr) string { + var buf bytes.Buffer + write(&buf, e) + return buf.String() +} + +func write(buf *bytes.Buffer, e Expr) { + switch e := e.(type) { + case literal: + fmt.Fprintf(buf, "%g", e) + + case Var: + fmt.Fprintf(buf, "%s", e) + + case *unary: + fmt.Fprintf(buf, "(%c", e.op) + write(buf, e.x) + buf.WriteByte(')') + + case *binary: + buf.WriteByte('(') + write(buf, e.x) + fmt.Fprintf(buf, " %c ", e.op) + write(buf, e.y) + buf.WriteByte(')') + + case *call: + fmt.Fprintf(buf, "%s(", e.fn) + for i, arg := range e.args { + if i > 0 { + buf.WriteString(", ") + } + write(buf, arg) + } + buf.WriteByte(')') + } +} diff --git a/ch7/http1/main.go b/ch7/http1/main.go new file mode 100644 index 0000000..9e7b664 --- /dev/null +++ b/ch7/http1/main.go @@ -0,0 +1,46 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 191. + +// Http1 is a rudimentary e-commerce server. +package main + +import ( + "fmt" + "log" + "net/http" +) + +//!+main + +func main() { + db := database{"shoes": 50, "socks": 5} + log.Fatal(http.ListenAndServe("localhost:8000", db)) +} + +type dollars float32 + +func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) } + +type database map[string]dollars + +func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { + for item, price := range db { + fmt.Fprintf(w, "%s: %s\n", item, price) + } +} + +//!-main + +/* +//!+handler +package http + +type Handler interface { + ServeHTTP(w ResponseWriter, r *Request) +} + +func ListenAndServe(address string, h Handler) error +//!-handler +*/ diff --git a/ch7/http2/main.go b/ch7/http2/main.go new file mode 100644 index 0000000..78ba8bc --- /dev/null +++ b/ch7/http2/main.go @@ -0,0 +1,48 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 192. + +// Http2 is an e-commerce server with /list and /price endpoints. +package main + +import ( + "fmt" + "log" + "net/http" +) + +func main() { + db := database{"shoes": 50, "socks": 5} + log.Fatal(http.ListenAndServe("localhost:8000", db)) +} + +type dollars float32 + +func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) } + +type database map[string]dollars + +//!+handler +func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/list": + for item, price := range db { + fmt.Fprintf(w, "%s: %s\n", item, price) + } + case "/price": + item := req.URL.Query().Get("item") + price, ok := db[item] + if !ok { + w.WriteHeader(http.StatusNotFound) // 404 + fmt.Fprintf(w, "no such item: %q\n", item) + return + } + fmt.Fprintf(w, "%s\n", price) + default: + w.WriteHeader(http.StatusNotFound) // 404 + fmt.Fprintf(w, "no such page: %s\n", req.URL) + } +} + +//!-handler diff --git a/ch7/http3/main.go b/ch7/http3/main.go new file mode 100644 index 0000000..816e2a2 --- /dev/null +++ b/ch7/http3/main.go @@ -0,0 +1,61 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 194. + +// Http3 is an e-commerce server that registers the /list and /price +// endpoints by calling (*http.ServeMux).Handle. +package main + +import ( + "fmt" + "log" + "net/http" +) + +type dollars float32 + +func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) } + +//!+main + +func main() { + db := database{"shoes": 50, "socks": 5} + mux := http.NewServeMux() + mux.Handle("/list", http.HandlerFunc(db.list)) + mux.Handle("/price", http.HandlerFunc(db.price)) + log.Fatal(http.ListenAndServe("localhost:8000", mux)) +} + +type database map[string]dollars + +func (db database) list(w http.ResponseWriter, req *http.Request) { + for item, price := range db { + fmt.Fprintf(w, "%s: %s\n", item, price) + } +} + +func (db database) price(w http.ResponseWriter, req *http.Request) { + item := req.URL.Query().Get("item") + price, ok := db[item] + if !ok { + w.WriteHeader(http.StatusNotFound) // 404 + fmt.Fprintf(w, "no such item: %q\n", item) + return + } + fmt.Fprintf(w, "%s\n", price) +} + +//!-main + +/* +//!+handlerfunc +package http + +type HandlerFunc func(w ResponseWriter, r *Request) + +func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) +} +//!-handlerfunc +*/ diff --git a/ch7/http3a/main.go b/ch7/http3a/main.go new file mode 100644 index 0000000..b682466 --- /dev/null +++ b/ch7/http3a/main.go @@ -0,0 +1,54 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 195. + +// Http3a is an e-commerce server that registers the /list and /price +// endpoints by calling (*http.ServeMux).HandleFunc. +package main + +import ( + "fmt" + "log" + "net/http" +) + +func main() { + db := database{"shoes": 50, "socks": 5} + mux := http.NewServeMux() + //!+main + mux.HandleFunc("/list", db.list) + mux.HandleFunc("/price", db.price) + //!-main + log.Fatal(http.ListenAndServe("localhost:8000", mux)) +} + +type database map[string]int + +func (db database) list(w http.ResponseWriter, req *http.Request) { + for item, price := range db { + fmt.Fprintf(w, "%s: $%d\n", item, price) + } +} + +func (db database) price(w http.ResponseWriter, req *http.Request) { + item := req.URL.Query().Get("item") + if price, ok := db[item]; ok { + fmt.Fprintf(w, "$%d\n", price) + } else { + w.WriteHeader(http.StatusNotFound) // 404 + fmt.Fprintf(w, "no such item: %q\n", item) + } +} + +/* +//!+handlerfunc +package http + +type HandlerFunc func(w ResponseWriter, r *Request) + +func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) +} +//!-handlerfunc +*/ diff --git a/ch7/http4/main.go b/ch7/http4/main.go new file mode 100644 index 0000000..b207d7a --- /dev/null +++ b/ch7/http4/main.go @@ -0,0 +1,47 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 195. + +// Http4 is an e-commerce server that registers the /list and /price +// endpoint by calling http.HandleFunc. +package main + +import ( + "fmt" + "log" + "net/http" +) + +//!+main + +func main() { + db := database{"shoes": 50, "socks": 5} + http.HandleFunc("/list", db.list) + http.HandleFunc("/price", db.price) + log.Fatal(http.ListenAndServe("localhost:8000", nil)) +} + +//!-main + +type dollars float32 + +func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) } + +type database map[string]dollars + +func (db database) list(w http.ResponseWriter, req *http.Request) { + for item, price := range db { + fmt.Fprintf(w, "%s: %s\n", item, price) + } +} + +func (db database) price(w http.ResponseWriter, req *http.Request) { + item := req.URL.Query().Get("item") + if price, ok := db[item]; ok { + fmt.Fprintf(w, "%s\n", price) + } else { + w.WriteHeader(http.StatusNotFound) // 404 + fmt.Fprintf(w, "no such item: %q\n", item) + } +} diff --git a/ch7/sleep/sleep.go b/ch7/sleep/sleep.go new file mode 100644 index 0000000..04f247e --- /dev/null +++ b/ch7/sleep/sleep.go @@ -0,0 +1,25 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 179. + +// The sleep program sleeps for a specified period of time. +package main + +import ( + "flag" + "fmt" + "time" +) + +//!+sleep +var period = flag.Duration("period", 1*time.Second, "sleep period") + +func main() { + flag.Parse() + fmt.Printf("Sleeping for %v...", *period) + time.Sleep(*period) + fmt.Println() +} + +//!-sleep diff --git a/ch7/sorting/main.go b/ch7/sorting/main.go new file mode 100644 index 0000000..be598d9 --- /dev/null +++ b/ch7/sorting/main.go @@ -0,0 +1,167 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 187. + +// Sorting sorts a music playlist into a variety of orders. +package main + +import ( + "fmt" + "os" + "sort" + "text/tabwriter" + "time" +) + +//!+main +type Track struct { + Title string + Artist string + Album string + Year int + Length time.Duration +} + +var tracks = []*Track{ + {"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")}, + {"Go", "Moby", "Moby", 1992, length("3m37s")}, + {"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")}, + {"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")}, +} + +func length(s string) time.Duration { + d, err := time.ParseDuration(s) + if err != nil { + panic(s) + } + return d +} + +//!-main + +//!+printTracks +func printTracks(tracks []*Track) { + const format = "%v\t%v\t%v\t%v\t%v\t\n" + tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0) + fmt.Fprintf(tw, format, "Title", "Artist", "Album", "Year", "Length") + fmt.Fprintf(tw, format, "-----", "------", "-----", "----", "------") + for _, t := range tracks { + fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length) + } + tw.Flush() // calculate column widths and print table +} + +//!-printTracks + +//!+artistcode +type byArtist []*Track + +func (x byArtist) Len() int { return len(x) } +func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist } +func (x byArtist) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +//!-artistcode + +//!+yearcode +type byYear []*Track + +func (x byYear) Len() int { return len(x) } +func (x byYear) Less(i, j int) bool { return x[i].Year < x[j].Year } +func (x byYear) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +//!-yearcode + +func main() { + fmt.Println("byArtist:") + sort.Sort(byArtist(tracks)) + printTracks(tracks) + + fmt.Println("\nReverse(byArtist):") + sort.Sort(sort.Reverse(byArtist(tracks))) + printTracks(tracks) + + fmt.Println("\nbyYear:") + sort.Sort(byYear(tracks)) + printTracks(tracks) + + fmt.Println("\nCustom:") + //!+customcall + sort.Sort(customSort{tracks, func(x, y *Track) bool { + if x.Title != y.Title { + return x.Title < y.Title + } + if x.Year != y.Year { + return x.Year < y.Year + } + if x.Length != y.Length { + return x.Length < y.Length + } + return false + }}) + //!-customcall + printTracks(tracks) +} + +/* +//!+artistoutput +Title Artist Album Year Length +----- ------ ----- ---- ------ +Go Ahead Alicia Keys As I Am 2007 4m36s +Go Delilah From the Roots Up 2012 3m38s +Ready 2 Go Martin Solveig Smash 2011 4m24s +Go Moby Moby 1992 3m37s +//!-artistoutput + +//!+artistrevoutput +Title Artist Album Year Length +----- ------ ----- ---- ------ +Go Moby Moby 1992 3m37s +Ready 2 Go Martin Solveig Smash 2011 4m24s +Go Delilah From the Roots Up 2012 3m38s +Go Ahead Alicia Keys As I Am 2007 4m36s +//!-artistrevoutput + +//!+yearoutput +Title Artist Album Year Length +----- ------ ----- ---- ------ +Go Moby Moby 1992 3m37s +Go Ahead Alicia Keys As I Am 2007 4m36s +Ready 2 Go Martin Solveig Smash 2011 4m24s +Go Delilah From the Roots Up 2012 3m38s +//!-yearoutput + +//!+customout +Title Artist Album Year Length +----- ------ ----- ---- ------ +Go Moby Moby 1992 3m37s +Go Delilah From the Roots Up 2012 3m38s +Go Ahead Alicia Keys As I Am 2007 4m36s +Ready 2 Go Martin Solveig Smash 2011 4m24s +//!-customout +*/ + +//!+customcode +type customSort struct { + t []*Track + less func(x, y *Track) bool +} + +func (x customSort) Len() int { return len(x.t) } +func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) } +func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] } + +//!-customcode + +func init() { + //!+ints + values := []int{3, 1, 4, 1} + fmt.Println(sort.IntsAreSorted(values)) // "false" + sort.Ints(values) + fmt.Println(values) // "[1 1 3 4]" + fmt.Println(sort.IntsAreSorted(values)) // "true" + sort.Sort(sort.Reverse(sort.IntSlice(values))) + fmt.Println(values) // "[4 3 1 1]" + fmt.Println(sort.IntsAreSorted(values)) // "false" + //!-ints +} diff --git a/ch7/surface/surface.go b/ch7/surface/surface.go new file mode 100644 index 0000000..ed768ba --- /dev/null +++ b/ch7/surface/surface.go @@ -0,0 +1,112 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 203. + +// The surface program plots the 3-D surface of a user-provided function. +package main + +import ( + "fmt" + "io" + "log" + "math" + "net/http" +) + +//!+parseAndCheck +import "gopl.io/ch7/eval" + +//!-parseAndCheck + +// -- copied from gopl.io/ch3/surface -- + +const ( + width, height = 600, 320 // canvas size in pixels + cells = 100 // number of grid cells + xyrange = 30.0 // x, y axis range (-xyrange..+xyrange) + xyscale = width / 2 / xyrange // pixels per x or y unit + zscale = height * 0.4 // pixels per z unit +) + +var sin30, cos30 = 0.5, math.Sqrt(3.0 / 4.0) // sin(30°), cos(30°) + +func corner(f func(x, y float64) float64, i, j int) (float64, float64) { + // find point (x,y) at corner of cell (i,j) + x := xyrange * (float64(i)/cells - 0.5) + y := xyrange * (float64(j)/cells - 0.5) + + z := f(x, y) // compute surface height z + + // project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy) + sx := width/2 + (x-y)*cos30*xyscale + sy := height/2 + (x+y)*sin30*xyscale - z*zscale + return sx, sy +} + +func surface(w io.Writer, f func(x, y float64) float64) { + fmt.Fprintf(w, "", width, height) + for i := 0; i < cells; i++ { + for j := 0; j < cells; j++ { + ax, ay := corner(f, i+1, j) + bx, by := corner(f, i, j) + cx, cy := corner(f, i, j+1) + dx, dy := corner(f, i+1, j+1) + fmt.Fprintf(w, "\n", + ax, ay, bx, by, cx, cy, dx, dy) + } + } + fmt.Fprintln(w, "") +} + +// -- main code for gopl.io/ch7/surface -- + +//!+parseAndCheck +func parseAndCheck(s string) (eval.Expr, error) { + if s == "" { + return nil, fmt.Errorf("empty expression") + } + expr, err := eval.Parse(s) + if err != nil { + return nil, err + } + vars := make(map[eval.Var]bool) + if err := expr.Check(vars); err != nil { + return nil, err + } + for v := range vars { + if v != "x" && v != "y" && v != "r" { + return nil, fmt.Errorf("undefined variable: %s", v) + } + } + return expr, nil +} + +//!-parseAndCheck + +//!+plot +func plot(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + expr, err := parseAndCheck(r.Form.Get("expr")) + if err != nil { + http.Error(w, "bad expr: "+err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "image/svg+xml") + surface(w, func(x, y float64) float64 { + r := math.Hypot(x, y) // distance from (0,0) + return expr.Eval(eval.Env{"x": x, "y": y, "r": r}) + }) +} + +//!-plot + +//!+main +func main() { + http.HandleFunc("/plot", plot) + log.Fatal(http.ListenAndServe("localhost:8000", nil)) +} + +//!-main diff --git a/ch7/tempconv/tempconv.go b/ch7/tempconv/tempconv.go new file mode 100644 index 0000000..6c3ef41 --- /dev/null +++ b/ch7/tempconv/tempconv.go @@ -0,0 +1,66 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 180. + +// Package tempconv performs Celsius and Fahrenheit temperature computations. +package tempconv + +import ( + "flag" + "fmt" +) + +type Celsius float64 +type Fahrenheit float64 + +func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9.0/5.0 + 32.0) } +func FToC(f Fahrenheit) Celsius { return Celsius((f - 32.0) * 5.0 / 9.0) } + +func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) } + +/* +//!+flagvalue +package flag + +// Value is the interface to the value stored in a flag. +type Value interface { + String() string + Set(string) error +} +//!-flagvalue +*/ + +//!+celsiusFlag +// *celsiusFlag satisfies the flag.Value interface. +type celsiusFlag struct{ Celsius } + +func (f *celsiusFlag) Set(s string) error { + var unit string + var value float64 + fmt.Sscanf(s, "%f%s", &value, &unit) // no error check needed + switch unit { + case "C", "°C": + f.Celsius = Celsius(value) + return nil + case "F", "°F": + f.Celsius = FToC(Fahrenheit(value)) + return nil + } + return fmt.Errorf("invalid temperature %q", s) +} + +//!-celsiusFlag + +//!+CelsiusFlag + +// CelsiusFlag defines a Celsius flag with the specified name, +// default value, and usage, and returns the address of the flag variable. +// The flag argument must have a quantity and a unit, e.g., "100C". +func CelsiusFlag(name string, value Celsius, usage string) *Celsius { + f := celsiusFlag{value} + flag.CommandLine.Var(&f, name, usage) + return &f.Celsius +} + +//!-CelsiusFlag diff --git a/ch7/tempflag/tempflag.go b/ch7/tempflag/tempflag.go new file mode 100644 index 0000000..6f6a4e7 --- /dev/null +++ b/ch7/tempflag/tempflag.go @@ -0,0 +1,24 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 181. + +// Tempflag prints the value of its -temp (temperature) flag. +package main + +import ( + "flag" + "fmt" + + "gopl.io/ch7/tempconv" +) + +//!+ +var temp = tempconv.CelsiusFlag("temp", 20.0, "the temperature") + +func main() { + flag.Parse() + fmt.Println(*temp) +} + +//!- diff --git a/ch7/xmlselect/main.go b/ch7/xmlselect/main.go new file mode 100644 index 0000000..66cea65 --- /dev/null +++ b/ch7/xmlselect/main.go @@ -0,0 +1,56 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 214. +//!+ + +// Xmlselect prints the text of selected elements of an XML document. +package main + +import ( + "encoding/xml" + "fmt" + "io" + "os" + "strings" +) + +func main() { + dec := xml.NewDecoder(os.Stdin) + var stack []string // stack of element names + for { + tok, err := dec.Token() + if err == io.EOF { + break + } else if err != nil { + fmt.Fprintf(os.Stderr, "xmlselect: %v\n", err) + os.Exit(1) + } + switch tok := tok.(type) { + case xml.StartElement: + stack = append(stack, tok.Name.Local) // push + case xml.EndElement: + stack = stack[:len(stack)-1] // pop + case xml.CharData: + if containsAll(stack, os.Args[1:]) { + fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok) + } + } + } +} + +// containsAll reports whether x contains the elements of y, in order. +func containsAll(x, y []string) bool { + for len(y) <= len(x) { + if len(y) == 0 { + return true + } + if x[0] == y[0] { + y = y[1:] + } + x = x[1:] + } + return false +} + +//!- diff --git a/ch8/cake/cake.go b/ch8/cake/cake.go new file mode 100644 index 0000000..8de0c3d --- /dev/null +++ b/ch8/cake/cake.go @@ -0,0 +1,89 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 234. + +// Package cake provides a simulation of +// a concurrent cake shop with numerous parameters. +// +// Use this command to run the benchmarks: +// $ go test -bench=. gopl.io/ch8/cake +package cake + +import ( + "fmt" + "math/rand" + "time" +) + +type Shop struct { + Verbose bool + Cakes int // number of cakes to bake + BakeTime time.Duration // time to bake one cake + BakeStdDev time.Duration // standard deviation of baking time + BakeBuf int // buffer slots between baking and icing + NumIcers int // number of cooks doing icing + IceTime time.Duration // time to ice one cake + IceStdDev time.Duration // standard deviation of icing time + IceBuf int // buffer slots between icing and inscribing + InscribeTime time.Duration // time to inscribe one cake + InscribeStdDev time.Duration // standard deviation of inscribing time +} + +type cake int + +func (s *Shop) baker(baked chan<- cake) { + for i := 0; i < s.Cakes; i++ { + c := cake(i) + if s.Verbose { + fmt.Println("baking", c) + } + work(s.BakeTime, s.BakeStdDev) + baked <- c + } + close(baked) +} + +func (s *Shop) icer(iced chan<- cake, baked <-chan cake) { + for c := range baked { + if s.Verbose { + fmt.Println("icing", c) + } + work(s.IceTime, s.IceStdDev) + iced <- c + } +} + +func (s *Shop) inscriber(iced <-chan cake) { + for i := 0; i < s.Cakes; i++ { + c := <-iced + if s.Verbose { + fmt.Println("inscribing", c) + } + work(s.InscribeTime, s.InscribeStdDev) + if s.Verbose { + fmt.Println("finished", c) + } + } +} + +// Work runs the simulation 'runs' times. +func (s *Shop) Work(runs int) { + for run := 0; run < runs; run++ { + baked := make(chan cake, s.BakeBuf) + iced := make(chan cake, s.IceBuf) + go s.baker(baked) + for i := 0; i < s.NumIcers; i++ { + go s.icer(iced, baked) + } + s.inscriber(iced) + } +} + +// work blocks the calling goroutine for a period of time +// that is normally distributed around d +// with a standard deviation of stddev. +func work(d, stddev time.Duration) { + delay := d + time.Duration(rand.NormFloat64()*float64(stddev)) + time.Sleep(delay) +} diff --git a/ch8/cake/cake_test.go b/ch8/cake/cake_test.go new file mode 100644 index 0000000..f8fea0e --- /dev/null +++ b/ch8/cake/cake_test.go @@ -0,0 +1,74 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package cake_test + +import ( + "testing" + "time" + + "gopl.io/ch8/cake" +) + +var defaults = cake.Shop{ + Verbose: testing.Verbose(), + Cakes: 20, + BakeTime: 10 * time.Millisecond, + NumIcers: 1, + IceTime: 10 * time.Millisecond, + InscribeTime: 10 * time.Millisecond, +} + +func Benchmark(b *testing.B) { + // Baseline: one baker, one icer, one inscriber. + // Each step takes exactly 10ms. No buffers. + cakeshop := defaults + cakeshop.Work(b.N) // 224 ms +} + +func BenchmarkBuffers(b *testing.B) { + // Adding buffers has no effect. + cakeshop := defaults + cakeshop.BakeBuf = 10 + cakeshop.IceBuf = 10 + cakeshop.Work(b.N) // 224 ms +} + +func BenchmarkVariable(b *testing.B) { + // Adding variability to rate of each step + // increases total time due to channel delays. + cakeshop := defaults + cakeshop.BakeStdDev = cakeshop.BakeTime / 4 + cakeshop.IceStdDev = cakeshop.IceTime / 4 + cakeshop.InscribeStdDev = cakeshop.InscribeTime / 4 + cakeshop.Work(b.N) // 259 ms +} + +func BenchmarkVariableBuffers(b *testing.B) { + // Adding channel buffers reduces + // delays resulting from variability. + cakeshop := defaults + cakeshop.BakeStdDev = cakeshop.BakeTime / 4 + cakeshop.IceStdDev = cakeshop.IceTime / 4 + cakeshop.InscribeStdDev = cakeshop.InscribeTime / 4 + cakeshop.BakeBuf = 10 + cakeshop.IceBuf = 10 + cakeshop.Work(b.N) // 244 ms +} + +func BenchmarkSlowIcing(b *testing.B) { + // Making the middle stage slower + // adds directly to the critical path. + cakeshop := defaults + cakeshop.IceTime = 50 * time.Millisecond + cakeshop.Work(b.N) // 1.032 s +} + +func BenchmarkSlowIcingManyIcers(b *testing.B) { + // Adding more icing cooks reduces the cost of icing + // to its sequential component, following Amdahl's Law. + cakeshop := defaults + cakeshop.IceTime = 50 * time.Millisecond + cakeshop.NumIcers = 5 + cakeshop.Work(b.N) // 288ms +} diff --git a/ch8/chat/chat.go b/ch8/chat/chat.go new file mode 100644 index 0000000..990bcf3 --- /dev/null +++ b/ch8/chat/chat.go @@ -0,0 +1,96 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 254. +//!+ + +// Chat is a server that lets clients chat with each other. +package main + +import ( + "bufio" + "fmt" + "log" + "net" +) + +//!+broadcaster +type client chan<- string // an outgoing message channel + +var ( + entering = make(chan client) + leaving = make(chan client) + messages = make(chan string) // all incoming client messages +) + +func broadcaster() { + clients := make(map[client]bool) // all connected clients + for { + select { + case msg := <-messages: + // Broadcast incoming message to all + // clients' outgoing message channels. + for cli := range clients { + cli <- msg + } + + case cli := <-entering: + clients[cli] = true + + case cli := <-leaving: + delete(clients, cli) + close(cli) + } + } +} + +//!-broadcaster + +//!+handleConn +func handleConn(conn net.Conn) { + ch := make(chan string) // outgoing client messages + go clientWriter(conn, ch) + + who := conn.RemoteAddr().String() + ch <- "You are " + who + messages <- who + " has arrived" + entering <- ch + + input := bufio.NewScanner(conn) + for input.Scan() { + messages <- who + ": " + input.Text() + } + // NOTE: ignoring potential errors from input.Err() + + leaving <- ch + messages <- who + " has left" + conn.Close() +} + +func clientWriter(conn net.Conn, ch <-chan string) { + for msg := range ch { + fmt.Fprintln(conn, msg) // NOTE: ignoring network errors + } +} + +//!-handleConn + +//!+main +func main() { + listener, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + + go broadcaster() + for { + conn, err := listener.Accept() + if err != nil { + log.Print(err) + continue + } + go handleConn(conn) + } +} + +//!-main diff --git a/ch8/clock1/clock.go b/ch8/clock1/clock.go new file mode 100644 index 0000000..03fada1 --- /dev/null +++ b/ch8/clock1/clock.go @@ -0,0 +1,43 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 219. +//!+ + +// Clock1 is a TCP server that periodically writes the time. +package main + +import ( + "io" + "log" + "net" + "time" +) + +func main() { + listener, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + for { + conn, err := listener.Accept() + if err != nil { + log.Print(err) // e.g., connection aborted + continue + } + handleConn(conn) // handle one connection at a time + } +} + +func handleConn(c net.Conn) { + defer c.Close() + for { + _, err := io.WriteString(c, time.Now().Format("15:04:05\n")) + if err != nil { + return // e.g., client disconnected + } + time.Sleep(1 * time.Second) + } +} + +//!- diff --git a/ch8/clock2/clock.go b/ch8/clock2/clock.go new file mode 100644 index 0000000..55c6235 --- /dev/null +++ b/ch8/clock2/clock.go @@ -0,0 +1,42 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 222. + +// Clock is a TCP server that periodically writes the time. +package main + +import ( + "io" + "log" + "net" + "time" +) + +func handleConn(c net.Conn) { + defer c.Close() + for { + _, err := io.WriteString(c, time.Now().Format("15:04:05\n")) + if err != nil { + return // e.g., client disconnected + } + time.Sleep(1 * time.Second) + } +} + +func main() { + listener, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + //!+ + for { + conn, err := listener.Accept() + if err != nil { + log.Print(err) // e.g., connection aborted + continue + } + go handleConn(conn) // handle connections concurrently + } + //!- +} diff --git a/ch8/countdown1/countdown.go b/ch8/countdown1/countdown.go new file mode 100644 index 0000000..036ba6a --- /dev/null +++ b/ch8/countdown1/countdown.go @@ -0,0 +1,29 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 244. + +// Countdown implements the countdown for a rocket launch. +package main + +import ( + "fmt" + "time" +) + +//!+ +func main() { + fmt.Println("Commencing countdown.") + tick := time.Tick(1 * time.Second) + for countdown := 10; countdown > 0; countdown-- { + fmt.Println(countdown) + <-tick + } + launch() +} + +//!- + +func launch() { + fmt.Println("Lift off!") +} diff --git a/ch8/countdown2/countdown.go b/ch8/countdown2/countdown.go new file mode 100644 index 0000000..6ac6746 --- /dev/null +++ b/ch8/countdown2/countdown.go @@ -0,0 +1,46 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 244. + +// Countdown implements the countdown for a rocket launch. +package main + +import ( + "fmt" + "os" + "time" +) + +//!+ + +func main() { + // ...create abort channel... + + //!- + + //!+abort + abort := make(chan struct{}) + go func() { + os.Stdin.Read(make([]byte, 1)) // read a single byte + abort <- struct{}{} + }() + //!-abort + + //!+ + fmt.Println("Commencing countdown. Press return to abort.") + select { + case <-time.After(10 * time.Second): + // Do nothing. + case <-abort: + fmt.Println("Launch aborted!") + return + } + launch() +} + +//!- + +func launch() { + fmt.Println("Lift off!") +} diff --git a/ch8/countdown3/countdown.go b/ch8/countdown3/countdown.go new file mode 100644 index 0000000..ade9a55 --- /dev/null +++ b/ch8/countdown3/countdown.go @@ -0,0 +1,51 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 246. + +// Countdown implements the countdown for a rocket launch. +package main + +// NOTE: the ticker goroutine never terminates if the launch is aborted. +// This is a "goroutine leak". + +import ( + "fmt" + "os" + "time" +) + +//!+ + +func main() { + // ...create abort channel... + + //!- + + abort := make(chan struct{}) + go func() { + os.Stdin.Read(make([]byte, 1)) // read a single byte + abort <- struct{}{} + }() + + //!+ + fmt.Println("Commencing countdown. Press return to abort.") + tick := time.Tick(1 * time.Second) + for countdown := 10; countdown > 0; countdown-- { + fmt.Println(countdown) + select { + case <-tick: + // Do nothing. + case <-abort: + fmt.Println("Launch aborted!") + return + } + } + launch() +} + +//!- + +func launch() { + fmt.Println("Lift off!") +} diff --git a/ch8/crawl1/findlinks.go b/ch8/crawl1/findlinks.go new file mode 100644 index 0000000..df568b4 --- /dev/null +++ b/ch8/crawl1/findlinks.go @@ -0,0 +1,72 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 240. + +// Crawl1 crawls web links starting with the command-line arguments. +// +// This version quickly exhausts available file descriptors +// due to excessive concurrent calls to links.Extract. +// +// Also, it never terminates because the worklist is never closed. +package main + +import ( + "fmt" + "log" + "os" + + "gopl.io/ch5/links" +) + +//!+crawl +func crawl(url string) []string { + fmt.Println(url) + list, err := links.Extract(url) + if err != nil { + log.Print(err) + } + return list +} + +//!-crawl + +//!+main +func main() { + worklist := make(chan []string) + + // Start with the command-line arguments. + go func() { worklist <- os.Args[1:] }() + + // Crawl the web concurrently. + seen := make(map[string]bool) + for list := range worklist { + for _, link := range list { + if !seen[link] { + seen[link] = true + go func(link string) { + worklist <- crawl(link) + }(link) + } + } + } +} + +//!-main + +/* +//!+output +$ go build gopl.io/ch8/crawl1 +$ ./crawl1 http://gopl.io/ +http://gopl.io/ +https://golang.org/help/ + +https://golang.org/doc/ +https://golang.org/blog/ +... +2015/07/15 18:22:12 Get ...: dial tcp: lookup blog.golang.org: no such host +2015/07/15 18:22:12 Get ...: dial tcp 23.21.222.120:443: socket: + too many open files +... +//!-output +*/ diff --git a/ch8/crawl2/findlinks.go b/ch8/crawl2/findlinks.go new file mode 100644 index 0000000..aad20f8 --- /dev/null +++ b/ch8/crawl2/findlinks.go @@ -0,0 +1,64 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 241. + +// Crawl2 crawls web links starting with the command-line arguments. +// +// This version uses a buffered channel as a counting semaphore +// to limit the number of concurrent calls to links.Extract. +package main + +import ( + "fmt" + "log" + "os" + + "gopl.io/ch5/links" +) + +//!+sema +// tokens is a counting semaphore used to +// enforce a limit of 20 concurrent requests. +var tokens = make(chan struct{}, 20) + +func crawl(url string) []string { + fmt.Println(url) + tokens <- struct{}{} // acquire a token + list, err := links.Extract(url) + <-tokens // release the token + + if err != nil { + log.Print(err) + } + return list +} + +//!-sema + +//!+ +func main() { + worklist := make(chan []string) + var n int // number of pending sends to worklist + + // Start with the command-line arguments. + n++ + go func() { worklist <- os.Args[1:] }() + + // Crawl the web concurrently. + seen := make(map[string]bool) + for ; n > 0; n-- { + list := <-worklist + for _, link := range list { + if !seen[link] { + seen[link] = true + n++ + go func(link string) { + worklist <- crawl(link) + }(link) + } + } + } +} + +//!- diff --git a/ch8/crawl3/findlinks.go b/ch8/crawl3/findlinks.go new file mode 100644 index 0000000..8283de1 --- /dev/null +++ b/ch8/crawl3/findlinks.go @@ -0,0 +1,61 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 243. + +// Crawl3 crawls web links starting with the command-line arguments. +// +// This version uses bounded parallelism. +// For simplicity, it does not address the termination problem. +// +package main + +import ( + "fmt" + "log" + "os" + + "gopl.io/ch5/links" +) + +func crawl(url string) []string { + fmt.Println(url) + list, err := links.Extract(url) + if err != nil { + log.Print(err) + } + return list +} + +//!+ +func main() { + worklist := make(chan []string) // lists of URLs, may have duplicates + unseenLinks := make(chan string) // de-duplicated URLs + + // Add command-line arguments to worklist. + go func() { worklist <- os.Args[1:] }() + + // Create 20 crawler goroutines to fetch each unseen link. + for i := 0; i < 20; i++ { + go func() { + for link := range unseenLinks { + foundLinks := crawl(link) + go func() { worklist <- foundLinks }() + } + }() + } + + // The main goroutine de-duplicates worklist items + // and sends the unseen ones to the crawlers. + seen := make(map[string]bool) + for list := range worklist { + for _, link := range list { + if !seen[link] { + seen[link] = true + unseenLinks <- link + } + } + } +} + +//!- diff --git a/ch8/du1/main.go b/ch8/du1/main.go new file mode 100644 index 0000000..8aee837 --- /dev/null +++ b/ch8/du1/main.go @@ -0,0 +1,79 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 247. + +//!+main + +// The du1 command computes the disk usage of the files in a directory. +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +func main() { + // Determine the initial directories. + flag.Parse() + roots := flag.Args() + if len(roots) == 0 { + roots = []string{"."} + } + + // Traverse the file tree. + fileSizes := make(chan int64) + go func() { + for _, root := range roots { + walkDir(root, fileSizes) + } + close(fileSizes) + }() + + // Print the results. + var nfiles, nbytes int64 + for size := range fileSizes { + nfiles++ + nbytes += size + } + printDiskUsage(nfiles, nbytes) +} + +func printDiskUsage(nfiles, nbytes int64) { + fmt.Printf("%d files %.1f GB\n", nfiles, float64(nbytes)/1e9) +} + +//!-main + +//!+walkDir + +// walkDir recursively walks the file tree rooted at dir +// and sends the size of each found file on fileSizes. +func walkDir(dir string, fileSizes chan<- int64) { + for _, entry := range dirents(dir) { + if entry.IsDir() { + subdir := filepath.Join(dir, entry.Name()) + walkDir(subdir, fileSizes) + } else { + fileSizes <- entry.Size() + } + } +} + +// dirents returns the entries of directory dir. +func dirents(dir string) []os.FileInfo { + entries, err := ioutil.ReadDir(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "du1: %v\n", err) + return nil + } + return entries +} + +//!-walkDir + +// The du1 variant uses two goroutines and +// prints the total after every file is found. diff --git a/ch8/du2/main.go b/ch8/du2/main.go new file mode 100644 index 0000000..842944f --- /dev/null +++ b/ch8/du2/main.go @@ -0,0 +1,94 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 249. + +// The du2 command computes the disk usage of the files in a directory. +package main + +// The du2 variant uses select and a time.Ticker +// to print the totals periodically if -v is set. + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" +) + +//!+ +var verbose = flag.Bool("v", false, "show verbose progress messages") + +func main() { + // ...start background goroutine... + + //!- + // Determine the initial directories. + flag.Parse() + roots := flag.Args() + if len(roots) == 0 { + roots = []string{"."} + } + + // Traverse the file tree. + fileSizes := make(chan int64) + go func() { + for _, root := range roots { + walkDir(root, fileSizes) + } + close(fileSizes) + }() + + //!+ + // Print the results periodically. + var tick <-chan time.Time + if *verbose { + tick = time.Tick(500 * time.Millisecond) + } + var nfiles, nbytes int64 +loop: + for { + select { + case size, ok := <-fileSizes: + if !ok { + break loop // fileSizes was closed + } + nfiles++ + nbytes += size + case <-tick: + printDiskUsage(nfiles, nbytes) + } + } + printDiskUsage(nfiles, nbytes) // final totals +} + +//!- + +func printDiskUsage(nfiles, nbytes int64) { + fmt.Printf("%d files %.1f GB\n", nfiles, float64(nbytes)/1e9) +} + +// walkDir recursively walks the file tree rooted at dir +// and sends the size of each found file on fileSizes. +func walkDir(dir string, fileSizes chan<- int64) { + for _, entry := range dirents(dir) { + if entry.IsDir() { + subdir := filepath.Join(dir, entry.Name()) + walkDir(subdir, fileSizes) + } else { + fileSizes <- entry.Size() + } + } +} + +// dirents returns the entries of directory dir. +func dirents(dir string) []os.FileInfo { + entries, err := ioutil.ReadDir(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "du: %v\n", err) + return nil + } + return entries +} diff --git a/ch8/du3/main.go b/ch8/du3/main.go new file mode 100644 index 0000000..42b3214 --- /dev/null +++ b/ch8/du3/main.go @@ -0,0 +1,118 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 250. + +// The du3 command computes the disk usage of the files in a directory. +package main + +// The du3 variant traverses all directories in parallel. +// It uses a concurrency-limiting counting semaphore +// to avoid opening too many files at once. + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + "time" +) + +var vFlag = flag.Bool("v", false, "show verbose progress messages") + +//!+ +func main() { + // ...determine roots... + + //!- + flag.Parse() + + // Determine the initial directories. + roots := flag.Args() + if len(roots) == 0 { + roots = []string{"."} + } + + //!+ + // Traverse each root of the file tree in parallel. + fileSizes := make(chan int64) + var n sync.WaitGroup + for _, root := range roots { + n.Add(1) + go walkDir(root, &n, fileSizes) + } + go func() { + n.Wait() + close(fileSizes) + }() + //!- + + // Print the results periodically. + var tick <-chan time.Time + if *vFlag { + tick = time.Tick(500 * time.Millisecond) + } + var nfiles, nbytes int64 +loop: + for { + select { + case size, ok := <-fileSizes: + if !ok { + break loop // fileSizes was closed + } + nfiles++ + nbytes += size + case <-tick: + printDiskUsage(nfiles, nbytes) + } + } + + printDiskUsage(nfiles, nbytes) // final totals + //!+ + // ...select loop... +} + +//!- + +func printDiskUsage(nfiles, nbytes int64) { + fmt.Printf("%d files %.1f GB\n", nfiles, float64(nbytes)/1e9) +} + +// walkDir recursively walks the file tree rooted at dir +// and sends the size of each found file on fileSizes. +//!+walkDir +func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { + defer n.Done() + for _, entry := range dirents(dir) { + if entry.IsDir() { + n.Add(1) + subdir := filepath.Join(dir, entry.Name()) + go walkDir(subdir, n, fileSizes) + } else { + fileSizes <- entry.Size() + } + } +} + +//!-walkDir + +//!+sema +// sema is a counting semaphore for limiting concurrency in dirents. +var sema = make(chan struct{}, 20) + +// dirents returns the entries of directory dir. +func dirents(dir string) []os.FileInfo { + sema <- struct{}{} // acquire token + defer func() { <-sema }() // release token + // ... + //!-sema + + entries, err := ioutil.ReadDir(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "du: %v\n", err) + return nil + } + return entries +} diff --git a/ch8/du4/main.go b/ch8/du4/main.go new file mode 100644 index 0000000..bc3f764 --- /dev/null +++ b/ch8/du4/main.go @@ -0,0 +1,145 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 251. + +// The du4 command computes the disk usage of the files in a directory. +package main + +// The du4 variant includes cancellation: +// it terminates quickly when the user hits return. + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "time" +) + +//!+1 +var done = make(chan struct{}) + +func cancelled() bool { + select { + case <-done: + return true + default: + return false + } +} + +//!-1 + +func main() { + // Determine the initial directories. + roots := os.Args[1:] + if len(roots) == 0 { + roots = []string{"."} + } + + //!+2 + // Cancel traversal when input is detected. + go func() { + os.Stdin.Read(make([]byte, 1)) // read a single byte + close(done) + }() + //!-2 + + // Traverse each root of the file tree in parallel. + fileSizes := make(chan int64) + var n sync.WaitGroup + for _, root := range roots { + n.Add(1) + go walkDir(root, &n, fileSizes) + } + go func() { + n.Wait() + close(fileSizes) + }() + + // Print the results periodically. + tick := time.Tick(500 * time.Millisecond) + var nfiles, nbytes int64 +loop: + //!+3 + for { + select { + case <-done: + // Drain fileSizes to allow existing goroutines to finish. + for range fileSizes { + // Do nothing. + } + return + case size, ok := <-fileSizes: + // ... + //!-3 + if !ok { + break loop // fileSizes was closed + } + nfiles++ + nbytes += size + case <-tick: + printDiskUsage(nfiles, nbytes) + } + } + printDiskUsage(nfiles, nbytes) // final totals +} + +func printDiskUsage(nfiles, nbytes int64) { + fmt.Printf("%d files %.1f GB\n", nfiles, float64(nbytes)/1e9) +} + +// walkDir recursively walks the file tree rooted at dir +// and sends the size of each found file on fileSizes. +//!+4 +func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { + defer n.Done() + if cancelled() { + return + } + for _, entry := range dirents(dir) { + // ... + //!-4 + if entry.IsDir() { + n.Add(1) + subdir := filepath.Join(dir, entry.Name()) + go walkDir(subdir, n, fileSizes) + } else { + fileSizes <- entry.Size() + } + //!+4 + } +} + +//!-4 + +var sema = make(chan struct{}, 20) // concurrency-limiting counting semaphore + +// dirents returns the entries of directory dir. +//!+5 +func dirents(dir string) []os.FileInfo { + select { + case sema <- struct{}{}: // acquire token + case <-done: + return nil // cancelled + } + defer func() { <-sema }() // release token + + // ...read directory... + //!-5 + + f, err := os.Open(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "du: %v\n", err) + return nil + } + defer f.Close() + + entries, err := f.Readdir(0) // 0 => no limit; read all entries + if err != nil { + fmt.Fprintf(os.Stderr, "du: %v\n", err) + // Don't return: Readdir may return partial results. + } + return entries +} diff --git a/ch8/netcat1/netcat.go b/ch8/netcat1/netcat.go new file mode 100644 index 0000000..e3c7823 --- /dev/null +++ b/ch8/netcat1/netcat.go @@ -0,0 +1,32 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 221. +//!+ + +// Netcat1 is a read-only TCP client. +package main + +import ( + "io" + "log" + "net" + "os" +) + +func main() { + conn, err := net.Dial("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + defer conn.Close() + mustCopy(os.Stdout, conn) +} + +func mustCopy(dst io.Writer, src io.Reader) { + if _, err := io.Copy(dst, src); err != nil { + log.Fatal(err) + } +} + +//!- diff --git a/ch8/netcat2/netcat.go b/ch8/netcat2/netcat.go new file mode 100644 index 0000000..7a86a5c --- /dev/null +++ b/ch8/netcat2/netcat.go @@ -0,0 +1,33 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 223. + +// Netcat is a simple read/write client for TCP servers. +package main + +import ( + "io" + "log" + "net" + "os" +) + +//!+ +func main() { + conn, err := net.Dial("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + defer conn.Close() + go mustCopy(os.Stdout, conn) + mustCopy(conn, os.Stdin) +} + +//!- + +func mustCopy(dst io.Writer, src io.Reader) { + if _, err := io.Copy(dst, src); err != nil { + log.Fatal(err) + } +} diff --git a/ch8/netcat3/netcat.go b/ch8/netcat3/netcat.go new file mode 100644 index 0000000..3950998 --- /dev/null +++ b/ch8/netcat3/netcat.go @@ -0,0 +1,39 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 227. + +// Netcat is a simple read/write client for TCP servers. +package main + +import ( + "io" + "log" + "net" + "os" +) + +//!+ +func main() { + conn, err := net.Dial("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + done := make(chan struct{}) + go func() { + io.Copy(os.Stdout, conn) // NOTE: ignoring errors + log.Println("done") + done <- struct{}{} // signal the main goroutine + }() + mustCopy(conn, os.Stdin) + conn.Close() + <-done // wait for background goroutine to finish +} + +//!- + +func mustCopy(dst io.Writer, src io.Reader) { + if _, err := io.Copy(dst, src); err != nil { + log.Fatal(err) + } +} diff --git a/ch8/pipeline1/main.go b/ch8/pipeline1/main.go new file mode 100644 index 0000000..06ecd5c --- /dev/null +++ b/ch8/pipeline1/main.go @@ -0,0 +1,37 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 228. + +// Pipeline1 demonstrates an infinite 3-stage pipeline. +package main + +import "fmt" + +//!+ +func main() { + naturals := make(chan int) + squares := make(chan int) + + // Counter + go func() { + for x := 0; ; x++ { + naturals <- x + } + }() + + // Squarer + go func() { + for { + x := <-naturals + squares <- x * x + } + }() + + // Printer (in main goroutine) + for { + fmt.Println(<-squares) + } +} + +//!- diff --git a/ch8/pipeline2/main.go b/ch8/pipeline2/main.go new file mode 100644 index 0000000..6015c01 --- /dev/null +++ b/ch8/pipeline2/main.go @@ -0,0 +1,38 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 229. + +// Pipeline2 demonstrates a finite 3-stage pipeline. +package main + +import "fmt" + +//!+ +func main() { + naturals := make(chan int) + squares := make(chan int) + + // Counter + go func() { + for x := 0; x < 100; x++ { + naturals <- x + } + close(naturals) + }() + + // Squarer + go func() { + for x := range naturals { + squares <- x * x + } + close(squares) + }() + + // Printer (in main goroutine) + for x := range squares { + fmt.Println(x) + } +} + +//!- diff --git a/ch8/pipeline3/main.go b/ch8/pipeline3/main.go new file mode 100644 index 0000000..cc679ea --- /dev/null +++ b/ch8/pipeline3/main.go @@ -0,0 +1,42 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 231. + +// Pipeline3 demonstrates a finite 3-stage pipeline +// with range, close, and unidirectional channel types. +package main + +import "fmt" + +//!+ +func counter(out chan<- int) { + for x := 0; x < 100; x++ { + out <- x + } + close(out) +} + +func squarer(out chan<- int, in <-chan int) { + for v := range in { + out <- v * v + } + close(out) +} + +func printer(in <-chan int) { + for v := range in { + fmt.Println(v) + } +} + +func main() { + naturals := make(chan int) + squares := make(chan int) + + go counter(naturals) + go squarer(squares, naturals) + printer(squares) +} + +//!- diff --git a/ch8/reverb1/reverb.go b/ch8/reverb1/reverb.go new file mode 100644 index 0000000..c72823f --- /dev/null +++ b/ch8/reverb1/reverb.go @@ -0,0 +1,51 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 223. + +// Reverb1 is a TCP server that simulates an echo. +package main + +import ( + "bufio" + "fmt" + "log" + "net" + "strings" + "time" +) + +//!+ +func echo(c net.Conn, shout string, delay time.Duration) { + fmt.Fprintln(c, "\t", strings.ToUpper(shout)) + time.Sleep(delay) + fmt.Fprintln(c, "\t", shout) + time.Sleep(delay) + fmt.Fprintln(c, "\t", strings.ToLower(shout)) +} + +func handleConn(c net.Conn) { + input := bufio.NewScanner(c) + for input.Scan() { + echo(c, input.Text(), 1*time.Second) + } + // NOTE: ignoring potential errors from input.Err() + c.Close() +} + +//!- + +func main() { + l, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + for { + conn, err := l.Accept() + if err != nil { + log.Print(err) // e.g., connection aborted + continue + } + go handleConn(conn) + } +} diff --git a/ch8/reverb2/reverb.go b/ch8/reverb2/reverb.go new file mode 100644 index 0000000..b74487d --- /dev/null +++ b/ch8/reverb2/reverb.go @@ -0,0 +1,51 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 224. + +// Reverb2 is a TCP server that simulates an echo. +package main + +import ( + "bufio" + "fmt" + "log" + "net" + "strings" + "time" +) + +func echo(c net.Conn, shout string, delay time.Duration) { + fmt.Fprintln(c, "\t", strings.ToUpper(shout)) + time.Sleep(delay) + fmt.Fprintln(c, "\t", shout) + time.Sleep(delay) + fmt.Fprintln(c, "\t", strings.ToLower(shout)) +} + +//!+ +func handleConn(c net.Conn) { + input := bufio.NewScanner(c) + for input.Scan() { + go echo(c, input.Text(), 1*time.Second) + } + // NOTE: ignoring potential errors from input.Err() + c.Close() +} + +//!- + +func main() { + l, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + for { + conn, err := l.Accept() + if err != nil { + log.Print(err) // e.g., connection aborted + continue + } + go handleConn(conn) + } +} diff --git a/ch8/spinner/main.go b/ch8/spinner/main.go new file mode 100644 index 0000000..c6a8cb1 --- /dev/null +++ b/ch8/spinner/main.go @@ -0,0 +1,38 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 218. + +// Spinner displays an animation while computing the 45th Fibonacci number. +package main + +import ( + "fmt" + "time" +) + +//!+ +func main() { + go spinner(100 * time.Millisecond) + const n = 45 + fibN := fib(n) // slow + fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN) +} + +func spinner(delay time.Duration) { + for { + for _, r := range `-\|/` { + fmt.Printf("\r%c", r) + time.Sleep(delay) + } + } +} + +func fib(x int) int { + if x < 2 { + return x + } + return fib(x-1) + fib(x-2) +} + +//!- diff --git a/ch8/thumbnail/main.go b/ch8/thumbnail/main.go new file mode 100644 index 0000000..a66174c --- /dev/null +++ b/ch8/thumbnail/main.go @@ -0,0 +1,42 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// +build ignore + +// The thumbnail command produces thumbnails of JPEG files +// whose names are provided on each line of the standard input. +// +// The "+build ignore" tag (see p.295) excludes this file from the +// thumbnail package, but it can be compiled as a command and run like +// this: +// +// Run with: +// $ go run $GOPATH/src/gopl.io/ch8/thumbnail/main.go +// foo.jpeg +// ^D +// +package main + +import ( + "bufio" + "fmt" + "log" + "os" + + "gopl.io/ch8/thumbnail" +) + +func main() { + input := bufio.NewScanner(os.Stdin) + for input.Scan() { + thumb, err := thumbnail.ImageFile(input.Text()) + if err != nil { + log.Print(err) + continue + } + fmt.Println(thumb) + } + if err := input.Err(); err != nil { + log.Fatal(err) + } +} diff --git a/ch8/thumbnail/thumbnail.go b/ch8/thumbnail/thumbnail.go new file mode 100644 index 0000000..a05f702 --- /dev/null +++ b/ch8/thumbnail/thumbnail.go @@ -0,0 +1,86 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 234. + +// The thumbnail package produces thumbnail-size images from +// larger images. Only JPEG images are currently supported. +package thumbnail + +import ( + "fmt" + "image" + "image/jpeg" + "io" + "os" + "path/filepath" + "strings" +) + +// Image returns a thumbnail-size version of src. +func Image(src image.Image) image.Image { + // Compute thumbnail size, preserving aspect ratio. + xs := src.Bounds().Size().X + ys := src.Bounds().Size().Y + width, height := 128, 128 + if aspect := float64(xs) / float64(ys); aspect < 1.0 { + width = int(128 * aspect) // portrait + } else { + height = int(128 / aspect) // landscape + } + xscale := float64(xs) / float64(width) + yscale := float64(ys) / float64(height) + + dst := image.NewRGBA(image.Rect(0, 0, width, height)) + + // a very crude scaling algorithm + for x := 0; x < width; x++ { + for y := 0; y < height; y++ { + srcx := int(float64(x) * xscale) + srcy := int(float64(y) * yscale) + dst.Set(x, y, src.At(srcx, srcy)) + } + } + return dst +} + +// ImageStream reads an image from r and +// writes a thumbnail-size version of it to w. +func ImageStream(w io.Writer, r io.Reader) error { + src, _, err := image.Decode(r) + if err != nil { + return err + } + dst := Image(src) + return jpeg.Encode(w, dst, nil) +} + +// ImageFile2 reads an image from infile and writes +// a thumbnail-size version of it to outfile. +func ImageFile2(outfile, infile string) (err error) { + in, err := os.Open(infile) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(outfile) + if err != nil { + return err + } + + if err := ImageStream(out, in); err != nil { + out.Close() + return fmt.Errorf("scaling %s to %s: %s", infile, outfile, err) + } + return out.Close() +} + +// ImageFile reads an image from infile and writes +// a thumbnail-size version of it in the same directory. +// It returns the generated file name, e.g. "foo.thumb.jpeg". +func ImageFile(infile string) (string, error) { + ext := filepath.Ext(infile) // e.g., ".jpg", ".JPEG" + outfile := strings.TrimSuffix(infile, ext) + ".thumb" + ext + return outfile, ImageFile2(outfile, infile) +} diff --git a/ch8/thumbnail/thumbnail_test.go b/ch8/thumbnail/thumbnail_test.go new file mode 100644 index 0000000..be05b6c --- /dev/null +++ b/ch8/thumbnail/thumbnail_test.go @@ -0,0 +1,148 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// This file is just a place to put example code from the book. +// It does not actually run any code in gopl.io/ch8/thumbnail. + +package thumbnail_test + +import ( + "log" + "os" + "sync" + + "gopl.io/ch8/thumbnail" +) + +//!+1 +// makeThumbnails makes thumbnails of the specified files. +func makeThumbnails(filenames []string) { + for _, f := range filenames { + if _, err := thumbnail.ImageFile(f); err != nil { + log.Println(err) + } + } +} + +//!-1 + +//!+2 +// NOTE: incorrect! +func makeThumbnails2(filenames []string) { + for _, f := range filenames { + go thumbnail.ImageFile(f) // NOTE: ignoring errors + } +} + +//!-2 + +//!+3 +// makeThumbnails3 makes thumbnails of the specified files in parallel. +func makeThumbnails3(filenames []string) { + ch := make(chan struct{}) + for _, f := range filenames { + go func(f string) { + thumbnail.ImageFile(f) // NOTE: ignoring errors + ch <- struct{}{} + }(f) + } + + // Wait for goroutines to complete. + for range filenames { + <-ch + } +} + +//!-3 + +//!+4 +// makeThumbnails4 makes thumbnails for the specified files in parallel. +// It returns an error if any step failed. +func makeThumbnails4(filenames []string) error { + errors := make(chan error) + + for _, f := range filenames { + go func(f string) { + _, err := thumbnail.ImageFile(f) + errors <- err + }(f) + } + + for range filenames { + if err := <-errors; err != nil { + return err // NOTE: incorrect: goroutine leak! + } + } + + return nil +} + +//!-4 + +//!+5 +// makeThumbnails5 makes thumbnails for the specified files in parallel. +// It returns the generated file names in an arbitrary order, +// or an error if any step failed. +func makeThumbnails5(filenames []string) (thumbfiles []string, err error) { + type item struct { + thumbfile string + err error + } + + ch := make(chan item, len(filenames)) + for _, f := range filenames { + go func(f string) { + var it item + it.thumbfile, it.err = thumbnail.ImageFile(f) + ch <- it + }(f) + } + + for range filenames { + it := <-ch + if it.err != nil { + return nil, it.err + } + thumbfiles = append(thumbfiles, it.thumbfile) + } + + return thumbfiles, nil +} + +//!-5 + +//!+6 +// makeThumbnails6 makes thumbnails for each file received from the channel. +// It returns the number of bytes occupied by the files it creates. +func makeThumbnails6(filenames <-chan string) int64 { + sizes := make(chan int64) + var wg sync.WaitGroup // number of working goroutines + for f := range filenames { + wg.Add(1) + // worker + go func(f string) { + defer wg.Done() + thumb, err := thumbnail.ImageFile(f) + if err != nil { + log.Println(err) + return + } + info, _ := os.Stat(thumb) // OK to ignore error + sizes <- info.Size() + }(f) + } + + // closer + go func() { + wg.Wait() + close(sizes) + }() + + var total int64 + for size := range sizes { + total += size + } + return total +} + +//!-6 diff --git a/ch9/bank1/bank.go b/ch9/bank1/bank.go new file mode 100644 index 0000000..67fb41a --- /dev/null +++ b/ch9/bank1/bank.go @@ -0,0 +1,31 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 261. +//!+ + +// Package bank provides a concurrency-safe bank with one account. +package bank + +var deposits = make(chan int) // send amount to deposit +var balances = make(chan int) // receive balance + +func Deposit(amount int) { deposits <- amount } +func Balance() int { return <-balances } + +func teller() { + var balance int // balance is confined to teller goroutine + for { + select { + case amount := <-deposits: + balance += amount + case balances <- balance: + } + } +} + +func init() { + go teller() // start the monitor goroutine +} + +//!- diff --git a/ch9/bank1/bank_test.go b/ch9/bank1/bank_test.go new file mode 100644 index 0000000..1c36b1c --- /dev/null +++ b/ch9/bank1/bank_test.go @@ -0,0 +1,36 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package bank_test + +import ( + "fmt" + "testing" + + "gopl.io/ch9/bank1" +) + +func TestBank(t *testing.T) { + done := make(chan struct{}) + + // Alice + go func() { + bank.Deposit(200) + fmt.Println("=", bank.Balance()) + done <- struct{}{} + }() + + // Bob + go func() { + bank.Deposit(100) + done <- struct{}{} + }() + + // Wait for both transactions. + <-done + <-done + + if got, want := bank.Balance(), 300; got != want { + t.Errorf("Balance = %d, want %d", got, want) + } +} diff --git a/ch9/bank2/bank.go b/ch9/bank2/bank.go new file mode 100644 index 0000000..8cc73d7 --- /dev/null +++ b/ch9/bank2/bank.go @@ -0,0 +1,28 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 262. + +// Package bank provides a concurrency-safe bank with one account. +package bank + +//!+ +var ( + sema = make(chan struct{}, 1) // a binary semaphore guarding balance + balance int +) + +func Deposit(amount int) { + sema <- struct{}{} // acquire token + balance = balance + amount + <-sema // release token +} + +func Balance() int { + sema <- struct{}{} // acquire token + b := balance + <-sema // release token + return b +} + +//!- diff --git a/ch9/bank2/bank_test.go b/ch9/bank2/bank_test.go new file mode 100644 index 0000000..98fed00 --- /dev/null +++ b/ch9/bank2/bank_test.go @@ -0,0 +1,28 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package bank_test + +import ( + "sync" + "testing" + + "gopl.io/ch9/bank2" +) + +func TestBank(t *testing.T) { + // Deposit [1..1000] concurrently. + var n sync.WaitGroup + for i := 1; i <= 1000; i++ { + n.Add(1) + go func(amount int) { + bank.Deposit(amount) + n.Done() + }(i) + } + n.Wait() + + if got, want := bank.Balance(), (1000+1)*1000/2; got != want { + t.Errorf("Balance = %d, want %d", got, want) + } +} diff --git a/ch9/bank3/bank.go b/ch9/bank3/bank.go new file mode 100644 index 0000000..5c42d45 --- /dev/null +++ b/ch9/bank3/bank.go @@ -0,0 +1,30 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 263. + +// Package bank provides a concurrency-safe single-account bank. +package bank + +//!+ +import "sync" + +var ( + mu sync.Mutex // guards balance + balance int +) + +func Deposit(amount int) { + mu.Lock() + balance = balance + amount + mu.Unlock() +} + +func Balance() int { + mu.Lock() + b := balance + mu.Unlock() + return b +} + +//!- diff --git a/ch9/bank3/bank_test.go b/ch9/bank3/bank_test.go new file mode 100644 index 0000000..98fed00 --- /dev/null +++ b/ch9/bank3/bank_test.go @@ -0,0 +1,28 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package bank_test + +import ( + "sync" + "testing" + + "gopl.io/ch9/bank2" +) + +func TestBank(t *testing.T) { + // Deposit [1..1000] concurrently. + var n sync.WaitGroup + for i := 1; i <= 1000; i++ { + n.Add(1) + go func(amount int) { + bank.Deposit(amount) + n.Done() + }(i) + } + n.Wait() + + if got, want := bank.Balance(), (1000+1)*1000/2; got != want { + t.Errorf("Balance = %d, want %d", got, want) + } +} diff --git a/ch9/memo1/memo.go b/ch9/memo1/memo.go new file mode 100644 index 0000000..7b9c1b6 --- /dev/null +++ b/ch9/memo1/memo.go @@ -0,0 +1,40 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 272. + +//!+ + +// Package memo provides a concurrency-unsafe +// memoization of a function of type Func. +package memo + +// A Memo caches the results of calling a Func. +type Memo struct { + f Func + cache map[string]result +} + +// Func is the type of the function to memoize. +type Func func(key string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +func New(f Func) *Memo { + return &Memo{f: f, cache: make(map[string]result)} +} + +// NOTE: not concurrency-safe! +func (memo *Memo) Get(key string) (interface{}, error) { + res, ok := memo.cache[key] + if !ok { + res.value, res.err = memo.f(key) + memo.cache[key] = res + } + return res.value, res.err +} + +//!- diff --git a/ch9/memo1/memo_test.go b/ch9/memo1/memo_test.go new file mode 100644 index 0000000..1b1c9af --- /dev/null +++ b/ch9/memo1/memo_test.go @@ -0,0 +1,67 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo1" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +// NOTE: not concurrency-safe! Test fails. +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} + +/* +//!+output +$ go test -v gopl.io/ch9/memo1 +=== RUN Test +https://golang.org, 175.026418ms, 7537 bytes +https://godoc.org, 172.686825ms, 6878 bytes +https://play.golang.org, 115.762377ms, 5767 bytes +http://gopl.io, 749.887242ms, 2856 bytes + +https://golang.org, 721ns, 7537 bytes +https://godoc.org, 152ns, 6878 bytes +https://play.golang.org, 205ns, 5767 bytes +http://gopl.io, 326ns, 2856 bytes +--- PASS: Test (1.21s) +PASS +ok gopl.io/ch9/memo1 1.257s +//!-output +*/ + +/* +//!+race +$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1 +=== RUN TestConcurrent +... +WARNING: DATA RACE +Write by goroutine 36: + runtime.mapassign1() + ~/go/src/runtime/hashmap.go:411 +0x0 + gopl.io/ch9/memo1.(*Memo).Get() + ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205 + ... + +Previous write by goroutine 35: + runtime.mapassign1() + ~/go/src/runtime/hashmap.go:411 +0x0 + gopl.io/ch9/memo1.(*Memo).Get() + ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205 +... +Found 1 data race(s) +FAIL gopl.io/ch9/memo1 2.393s +//!-race +*/ diff --git a/ch9/memo2/memo.go b/ch9/memo2/memo.go new file mode 100644 index 0000000..6397c1a --- /dev/null +++ b/ch9/memo2/memo.go @@ -0,0 +1,44 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 275. + +// Package memo provides a concurrency-safe memoization a function of +// type Func. Concurrent requests are serialized by a Mutex. +package memo + +import "sync" + +// Func is the type of the function to memoize. +type Func func(string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +func New(f Func) *Memo { + return &Memo{f: f, cache: make(map[string]result)} +} + +//!+ + +type Memo struct { + f Func + mu sync.Mutex // guards cache + cache map[string]result +} + +// Get is concurrency-safe. +func (memo *Memo) Get(key string) (value interface{}, err error) { + memo.mu.Lock() + res, ok := memo.cache[key] + if !ok { + res.value, res.err = memo.f(key) + memo.cache[key] = res + } + memo.mu.Unlock() + return res.value, res.err +} + +//!- diff --git a/ch9/memo2/memo_test.go b/ch9/memo2/memo_test.go new file mode 100644 index 0000000..bd4dc7b --- /dev/null +++ b/ch9/memo2/memo_test.go @@ -0,0 +1,23 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo2" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} diff --git a/ch9/memo3/memo.go b/ch9/memo3/memo.go new file mode 100644 index 0000000..0c21afe --- /dev/null +++ b/ch9/memo3/memo.go @@ -0,0 +1,48 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 276. + +// Package memo provides a concurrency-safe memoization a function of +// type Func. Requests for different keys run concurrently. +// Concurrent requests for the same key result in duplicate work. +package memo + +import "sync" + +type Memo struct { + f Func + mu sync.Mutex // guards cache + cache map[string]result +} + +type Func func(string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +func New(f Func) *Memo { + return &Memo{f: f, cache: make(map[string]result)} +} + +//!+ + +func (memo *Memo) Get(key string) (value interface{}, err error) { + memo.mu.Lock() + res, ok := memo.cache[key] + memo.mu.Unlock() + if !ok { + res.value, res.err = memo.f(key) + + // Between the two critical sections, several goroutines + // may race to compute f(key) and update the map. + memo.mu.Lock() + memo.cache[key] = res + memo.mu.Unlock() + } + return res.value, res.err +} + +//!- diff --git a/ch9/memo3/memo_test.go b/ch9/memo3/memo_test.go new file mode 100644 index 0000000..19fc5f0 --- /dev/null +++ b/ch9/memo3/memo_test.go @@ -0,0 +1,23 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo3" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} diff --git a/ch9/memo4/memo.go b/ch9/memo4/memo.go new file mode 100644 index 0000000..fc70e2f --- /dev/null +++ b/ch9/memo4/memo.go @@ -0,0 +1,61 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 276. + +// Package memo provides a concurrency-safe memoization a function of +// a function. Requests for different keys proceed in parallel. +// Concurrent requests for the same key block until the first completes. +// This implementation uses a Mutex. +package memo + +import "sync" + +// Func is the type of the function to memoize. +type Func func(string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +//!+ +type entry struct { + res result + ready chan struct{} // closed when res is ready +} + +func New(f Func) *Memo { + return &Memo{f: f, cache: make(map[string]*entry)} +} + +type Memo struct { + f Func + mu sync.Mutex // guards cache + cache map[string]*entry +} + +func (memo *Memo) Get(key string) (value interface{}, err error) { + memo.mu.Lock() + e := memo.cache[key] + if e == nil { + // This is the first request for this key. + // This goroutine becomes responsible for computing + // the value and broadcasting the ready condition. + e = &entry{ready: make(chan struct{})} + memo.cache[key] = e + memo.mu.Unlock() + + e.res.value, e.res.err = memo.f(key) + + close(e.ready) // broadcast ready condition + } else { + // This is a repeat request for this key. + memo.mu.Unlock() + + <-e.ready // wait for ready condition + } + return e.res.value, e.res.err +} + +//!- diff --git a/ch9/memo4/memo_test.go b/ch9/memo4/memo_test.go new file mode 100644 index 0000000..55cdc12 --- /dev/null +++ b/ch9/memo4/memo_test.go @@ -0,0 +1,23 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo4" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} diff --git a/ch9/memo5/memo.go b/ch9/memo5/memo.go new file mode 100644 index 0000000..4a8fd9b --- /dev/null +++ b/ch9/memo5/memo.go @@ -0,0 +1,88 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 278. + +// Package memo provides a concurrency-safe non-blocking memoization +// of a function. Requests for different keys proceed in parallel. +// Concurrent requests for the same key block until the first completes. +// This implementation uses a monitor goroutine. +package memo + +//!+Func + +// Func is the type of the function to memoize. +type Func func(key string) (interface{}, error) + +// A result is the result of calling a Func. +type result struct { + value interface{} + err error +} + +type entry struct { + res result + ready chan struct{} // closed when res is ready +} + +//!-Func + +//!+get + +// A request is a message requesting that the Func be applied to key. +type request struct { + key string + response chan<- result // the client wants a single result +} + +type Memo struct{ requests chan request } + +// New returns a memoization of f. Clients must subsequently call Close. +func New(f Func) *Memo { + memo := &Memo{requests: make(chan request)} + go memo.server(f) + return memo +} + +func (memo *Memo) Get(key string) (interface{}, error) { + response := make(chan result) + memo.requests <- request{key, response} + res := <-response + return res.value, res.err +} + +func (memo *Memo) Close() { close(memo.requests) } + +//!-get + +//!+monitor + +func (memo *Memo) server(f Func) { + cache := make(map[string]*entry) + for req := range memo.requests { + e := cache[req.key] + if e == nil { + // This is the first request for this key. + e = &entry{ready: make(chan struct{})} + cache[req.key] = e + go e.call(f, req.key) // call f(key) + } + go e.deliver(req.response) + } +} + +func (e *entry) call(f Func, key string) { + // Evaluate the function. + e.res.value, e.res.err = f(key) + // Broadcast the ready condition. + close(e.ready) +} + +func (e *entry) deliver(response chan<- result) { + // Wait for the ready condition. + <-e.ready + // Send the result to the client. + response <- e.res +} + +//!-monitor diff --git a/ch9/memo5/memo_test.go b/ch9/memo5/memo_test.go new file mode 100644 index 0000000..e74052e --- /dev/null +++ b/ch9/memo5/memo_test.go @@ -0,0 +1,25 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo5" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + defer m.Close() + memotest.Sequential(t, m) +} + +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + defer m.Close() + memotest.Concurrent(t, m) +} diff --git a/ch9/memotest/memotest.go b/ch9/memotest/memotest.go new file mode 100644 index 0000000..aab1acf --- /dev/null +++ b/ch9/memotest/memotest.go @@ -0,0 +1,102 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 272. + +// Package memotest provides common functions for +// testing various designs of the memo package. +package memotest + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "sync" + "testing" + "time" +) + +//!+httpRequestBody +func httpGetBody(url string) (interface{}, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return ioutil.ReadAll(resp.Body) +} + +//!-httpRequestBody + +var HTTPGetBody = httpGetBody + +func incomingURLs() <-chan string { + ch := make(chan string) + go func() { + for _, url := range []string{ + "https://golang.org", + "https://godoc.org", + "https://play.golang.org", + "http://gopl.io", + "https://golang.org", + "https://godoc.org", + "https://play.golang.org", + "http://gopl.io", + } { + ch <- url + } + close(ch) + }() + return ch +} + +type M interface { + Get(key string) (interface{}, error) +} + +/* +//!+seq + m := memo.New(httpGetBody) +//!-seq +*/ + +func Sequential(t *testing.T, m M) { + //!+seq + for url := range incomingURLs() { + start := time.Now() + value, err := m.Get(url) + if err != nil { + log.Print(err) + } + fmt.Printf("%s, %s, %d bytes\n", + url, time.Since(start), len(value.([]byte))) + } + //!-seq +} + +/* +//!+conc + m := memo.New(httpGetBody) +//!-conc +*/ + +func Concurrent(t *testing.T, m M) { + //!+conc + var n sync.WaitGroup + for url := range incomingURLs() { + n.Add(1) + go func(url string) { + start := time.Now() + value, err := m.Get(url) + if err != nil { + log.Print(err) + } + fmt.Printf("%s, %s, %d bytes\n", + url, time.Since(start), len(value.([]byte))) + n.Done() + }(url) + } + n.Wait() + //!-conc +}