├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── go.yml ├── .gitignore ├── .mdlrc ├── LICENSE.md ├── README.md ├── SUMMARY.md ├── TDD-outside-in.jpg ├── amazing-art.png ├── anti-patterns.md ├── app-intro.md ├── arrays-and-slices.md ├── arrays ├── v1 │ ├── sum.go │ └── sum_test.go ├── v2 │ ├── sum.go │ └── sum_test.go ├── v3 │ ├── sum.go │ └── sum_test.go ├── v4 │ ├── sum.go │ └── sum_test.go ├── v5 │ ├── sum.go │ └── sum_test.go ├── v6 │ ├── sum.go │ └── sum_test.go ├── v7 │ ├── sum.go │ └── sum_test.go └── v8 │ ├── assert.go │ ├── bad_bank.go │ ├── bad_bank_test.go │ ├── collection_fun.go │ ├── sum.go │ └── sum_test.go ├── blogrenderer ├── post.go ├── renderer.go ├── renderer_test.TestRender.it_converts_a_single_post_into_HTML.approved.txt ├── renderer_test.TestRender.it_renders_an_index_of_posts.approved.txt ├── renderer_test.go └── templates │ ├── blog.gohtml │ ├── bottom.gohtml │ ├── index.gohtml │ └── top.gohtml ├── book.json ├── build.books.sh ├── build.sh ├── command-line.md ├── command-line ├── v1 │ ├── cmd │ │ ├── cli │ │ │ └── main.go │ │ └── webserver │ │ │ ├── game.db.json │ │ │ └── main.go │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── league.go │ ├── server.go │ ├── server_integration_test.go │ ├── server_test.go │ ├── tape.go │ └── tape_test.go ├── v2 │ ├── CLI.go │ ├── CLI_test.go │ ├── cmd │ │ ├── cli │ │ │ └── main.go │ │ └── webserver │ │ │ ├── game.db.json │ │ │ └── main.go │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── league.go │ ├── server.go │ ├── server_integration_test.go │ ├── server_test.go │ ├── tape.go │ └── tape_test.go └── v3 │ ├── CLI.go │ ├── CLI_test.go │ ├── cmd │ ├── cli │ │ └── main.go │ └── webserver │ │ └── main.go │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── league.go │ ├── server.go │ ├── server_integration_test.go │ ├── server_test.go │ ├── tape.go │ ├── tape_test.go │ └── testing.go ├── concurrency.md ├── concurrency ├── v1 │ ├── check_website.go │ ├── check_websites.go │ ├── check_websites_benchmark_test.go │ └── check_websites_test.go ├── v2 │ ├── check_website.go │ ├── check_websites.go │ ├── check_websites_benchmark_test.go │ └── check_websites_test.go └── v3 │ ├── check_website.go │ ├── check_websites.go │ ├── check_websites_benchmark_test.go │ └── check_websites_test.go ├── context-aware-reader.md ├── context.md ├── context ├── v1 │ ├── context.go │ └── context_test.go ├── v2 │ ├── context.go │ ├── context_test.go │ └── testdoubles.go └── v3 │ ├── context.go │ ├── context_test.go │ └── testdoubles.go ├── contributing.md ├── dependency-injection.md ├── di ├── v1 │ ├── di.go │ └── di_test.go └── v2 │ ├── di.go │ └── di_test.go ├── epub-cover-small.png ├── epub-cover.png ├── epub-cover.pxm ├── error-types.md ├── for ├── v1 │ ├── repeat.go │ └── repeat_test.go ├── v2 │ ├── repeat.go │ └── repeat_test.go ├── v3 │ ├── repeat.go │ └── repeat_test.go └── vx │ ├── repeat.go │ └── repeat_test.go ├── gb-readme.md ├── generics.md ├── generics ├── assert.go ├── generics_test.go └── stack.go ├── go.mod ├── go.sum ├── hello-world.md ├── hello-world ├── .DS_Store ├── v1 │ └── hello.go ├── v2 │ ├── hello.go │ └── hello_test.go ├── v3 │ ├── hello.go │ └── hello_test.go ├── v4 │ ├── hello.go │ └── hello_test.go ├── v5 │ ├── hello.go │ └── hello_test.go ├── v6 │ ├── hello.go │ └── hello_test.go ├── v7 │ ├── hello.go │ └── hello_test.go └── v8 │ ├── hello.go │ └── hello_test.go ├── html-templates.md ├── http-handlers-revisited.md ├── http-server.md ├── http-server ├── v1 │ ├── main.go │ ├── server.go │ └── server_test.go ├── v2 │ ├── main.go │ ├── server.go │ └── server_test.go ├── v3 │ ├── main.go │ ├── server.go │ └── server_test.go ├── v4 │ ├── main.go │ ├── server.go │ └── server_test.go └── v5 │ ├── in_memory_player_store.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── install-go.md ├── integers.md ├── integers ├── v1 │ ├── adder.go │ └── adder_test.go └── v2 │ ├── adder.go │ └── adder_test.go ├── intro-to-acceptance-tests.md ├── io.md ├── io ├── v1 │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── in_memory_player_store.go │ ├── league.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── v2 │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── in_memory_player_store.go │ ├── league.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── v3 │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── in_memory_player_store.go │ ├── league.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── v4 │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── in_memory_player_store.go │ ├── league.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── v5 │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── league.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── v6 │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── league.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── v7 │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── league.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ ├── server_test.go │ ├── tape.go │ └── tape_test.go ├── v8 │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── league.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ ├── server_test.go │ ├── tape.go │ └── tape_test.go └── v9 │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── league.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ ├── server_test.go │ ├── tape.go │ └── tape_test.go ├── iteration.md ├── iterators └── iterators_test.go ├── json.md ├── json ├── v1 │ ├── in_memory_player_store.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── v2 │ ├── in_memory_player_store.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── v3 │ ├── in_memory_player_store.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── v4 │ ├── in_memory_player_store.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── v5 │ ├── in_memory_player_store.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go └── v6 │ ├── in_memory_player_store.go │ ├── main.go │ ├── server.go │ ├── server_integration_test.go │ └── server_test.go ├── maps.md ├── maps ├── v1 │ ├── dictionary.go │ └── dictionary_test.go ├── v2 │ ├── dictionary.go │ └── dictionary_test.go ├── v3 │ ├── dictionary.go │ └── dictionary_test.go ├── v4 │ ├── dictionary.go │ └── dictionary_test.go ├── v5 │ ├── dictionary.go │ └── dictionary_test.go ├── v6 │ ├── dictionary.go │ └── dictionary_test.go └── v7 │ ├── dictionary.go │ └── dictionary_test.go ├── math.md ├── math ├── clock.template.svg ├── example_clock.svg ├── images │ ├── unit_circle.png │ ├── unit_circle_12_oclock.png │ ├── unit_circle_coords.png │ └── unit_circle_params.png ├── v1 │ └── clockface │ │ ├── clockface.go │ │ └── clockface_test.go ├── v10 │ └── clockface │ │ ├── clockface.go │ │ ├── clockface │ │ ├── clock.svg │ │ └── main.go │ │ ├── clockface_acceptance_test.go │ │ ├── clockface_test.go │ │ └── svgWriter.go ├── v11 │ └── clockface │ │ ├── clockface.go │ │ ├── clockface │ │ ├── clock.svg │ │ └── main.go │ │ ├── clockface_acceptance_test.go │ │ ├── clockface_test.go │ │ └── svgWriter.go ├── v12 │ └── clockface │ │ ├── clockface.go │ │ ├── clockface │ │ ├── clock.svg │ │ └── main.go │ │ ├── clockface_acceptance_test.go │ │ ├── clockface_test.go │ │ └── svgWriter.go ├── v2 │ └── clockface │ │ ├── clockface.go │ │ ├── clockface_acceptance_test.go │ │ └── clockface_test.go ├── v3 │ └── clockface │ │ ├── clockface.go │ │ ├── clockface_acceptance_test.go │ │ └── clockface_test.go ├── v4 │ └── clockface │ │ ├── clockface.go │ │ ├── clockface_acceptance_test.go │ │ └── clockface_test.go ├── v5 │ └── clockface │ │ ├── clockface.go │ │ ├── clockface_acceptance_test.go │ │ └── clockface_test.go ├── v6 │ └── clockface │ │ ├── clockface.go │ │ ├── clockface │ │ ├── clock.svg │ │ └── main.go │ │ ├── clockface_acceptance_test.go │ │ └── clockface_test.go ├── v7 │ └── clockface │ │ ├── clockface.go │ │ ├── clockface │ │ ├── clock.svg │ │ └── main.go │ │ ├── clockface_acceptance_test.go │ │ ├── clockface_test.go │ │ └── svgWriter.go ├── v7b │ └── clockface │ │ ├── clockface.go │ │ ├── clockface │ │ ├── clock.svg │ │ └── main.go │ │ ├── clockface_acceptance_test.go │ │ ├── clockface_test.go │ │ └── svgWriter.go ├── v7c │ └── clockface │ │ ├── clockface.go │ │ ├── clockface │ │ ├── clock.svg │ │ └── main.go │ │ ├── clockface_acceptance_test.go │ │ ├── clockface_test.go │ │ └── svgWriter.go ├── v8 │ └── clockface │ │ ├── clockface.go │ │ ├── clockface │ │ ├── clock.svg │ │ └── main.go │ │ ├── clockface_acceptance_test.go │ │ ├── clockface_test.go │ │ └── svgWriter.go ├── v9 │ └── clockface │ │ ├── clockface.go │ │ ├── clockface │ │ ├── clock.svg │ │ └── main.go │ │ ├── clockface_acceptance_test.go │ │ ├── clockface_test.go │ │ └── svgWriter.go └── vFinal │ └── clockface │ ├── clockface.go │ ├── clockface │ ├── clock.svg │ └── main.go │ ├── clockface_test.go │ └── svg │ ├── svg.go │ └── svg_test.go ├── meta.tmpl.tex ├── mocking.md ├── mocking ├── v1 │ ├── countdown_test.go │ └── main.go ├── v2 │ ├── countdown_test.go │ └── main.go ├── v3 │ ├── countdown_test.go │ └── main.go ├── v4 │ ├── countdown_test.go │ └── main.go ├── v5 │ ├── countdown_test.go │ └── main.go └── v6 │ ├── countdown_test.go │ └── main.go ├── os-exec.md ├── pdf-cover.md ├── pdf-cover.tex ├── pointers-and-errors.md ├── pointers ├── v1 │ ├── wallet.go │ └── wallet_test.go ├── v2 │ ├── wallet.go │ └── wallet_test.go ├── v3 │ ├── wallet.go │ └── wallet_test.go └── v4 │ ├── wallet.go │ └── wallet_test.go ├── q-and-a ├── context-aware-reader │ ├── context_aware_reader.go │ └── context_aware_reader_test.go ├── error-types │ ├── error-types_test.go │ └── v2 │ │ └── error-types_test.go ├── http-handlers-revisited │ ├── basic_test.go │ ├── still_basic.go │ └── still_basic_test.go └── os-exec │ ├── msg.xml │ └── os-exec_test.go ├── reading-files.md ├── reading-files ├── blogposts.go ├── blogposts_test.go └── post.go ├── red-green-blue-gophers-smaller.png ├── red-green-blue-gophers.png ├── refactoring-checklist.md ├── reflection.md ├── reflection ├── v1 │ ├── reflection.go │ └── reflection_test.go ├── v10 │ ├── reflection.go │ └── reflection_test.go ├── v2 │ ├── reflection.go │ └── reflection_test.go ├── v3 │ ├── reflection.go │ └── reflection_test.go ├── v4 │ ├── reflection.go │ └── reflection_test.go ├── v5 │ ├── reflection.go │ └── reflection_test.go ├── v6 │ ├── reflection.go │ └── reflection_test.go ├── v7 │ ├── reflection.go │ └── reflection_test.go ├── v8 │ ├── reflection.go │ └── reflection_test.go └── v9 │ ├── reflection.go │ └── reflection_test.go ├── revisiting-arrays-and-slices-with-generics.md ├── roman-numerals.md ├── roman-numerals ├── v1 │ └── numeral_test.go ├── v10 │ ├── numeral_test.go │ └── roman_numerals.go ├── v11 │ ├── numeral_test.go │ └── roman_numerals.go ├── v2 │ └── numeral_test.go ├── v3 │ └── numeral_test.go ├── v4 │ └── numeral_test.go ├── v5 │ └── numeral_test.go ├── v6 │ └── numeral_test.go ├── v7 │ └── numeral_test.go ├── v8 │ └── numeral_test.go └── v9 │ └── numeral_test.go ├── scaling-acceptance-tests.md ├── select.md ├── select ├── v1 │ ├── racer.go │ └── racer_test.go ├── v2 │ ├── racer.go │ └── racer_test.go └── v3 │ ├── racer.go │ └── racer_test.go ├── structs-methods-and-interfaces.md ├── structs ├── v1 │ ├── shapes.go │ └── shapes_test.go ├── v2 │ ├── shapes.go │ └── shapes_test.go ├── v3 │ ├── shapes.go │ └── shapes_test.go ├── v4 │ ├── shapes.go │ └── shapes_test.go ├── v5 │ ├── shapes.go │ └── shapes_test.go ├── v6 │ ├── shapes.go │ └── shapes_test.go ├── v7 │ ├── shapes.go │ └── shapes_test.go └── v8 │ ├── shapes.go │ └── shapes_test.go ├── sync.md ├── sync ├── v1 │ ├── sync.go │ └── sync_test.go └── v2 │ ├── sync.go │ └── sync_test.go ├── template.md ├── time.md ├── time ├── v1 │ ├── CLI.go │ ├── CLI_test.go │ ├── blind_alerter.go │ ├── cmd │ │ ├── cli │ │ │ └── main.go │ │ └── webserver │ │ │ └── main.go │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── league.go │ ├── server.go │ ├── server_integration_test.go │ ├── server_test.go │ ├── tape.go │ ├── tape_test.go │ └── testing.go ├── v2 │ ├── CLI.go │ ├── CLI_test.go │ ├── blind_alerter.go │ ├── cmd │ │ ├── cli │ │ │ └── main.go │ │ └── webserver │ │ │ └── main.go │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── league.go │ ├── server.go │ ├── server_integration_test.go │ ├── server_test.go │ ├── tape.go │ ├── tape_test.go │ ├── testing.go │ ├── texas_holdem.go │ └── texas_holdem_test.go └── v3 │ ├── BlindAlerter.go │ ├── CLI.go │ ├── CLI_test.go │ ├── cmd │ ├── cli │ │ └── main.go │ └── webserver │ │ └── main.go │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── game.go │ ├── league.go │ ├── server.go │ ├── server_integration_test.go │ ├── server_test.go │ ├── tape.go │ ├── tape_test.go │ ├── testing.go │ ├── texas_holdem.go │ └── texas_holdem_test.go ├── title.txt ├── todo └── todo1_test.go ├── websockets.md ├── websockets ├── v1 │ ├── CLI.go │ ├── CLI_test.go │ ├── Gopkg.lock │ ├── Gopkg.toml │ ├── blind_alerter.go │ ├── cmd │ │ ├── cli │ │ │ └── main.go │ │ └── webserver │ │ │ ├── game.html │ │ │ └── main.go │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── game.go │ ├── game.html │ ├── league.go │ ├── server.go │ ├── server_integration_test.go │ ├── server_test.go │ ├── tape.go │ ├── tape_test.go │ ├── testing.go │ ├── texas_holdem.go │ ├── texas_holdem_test.go │ └── vendor │ │ └── github.com │ │ └── gorilla │ │ └── websocket │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── AUTHORS │ │ ├── LICENSE │ │ ├── README.md │ │ ├── client.go │ │ ├── client_clone.go │ │ ├── client_clone_legacy.go │ │ ├── compression.go │ │ ├── conn.go │ │ ├── conn_write.go │ │ ├── conn_write_legacy.go │ │ ├── doc.go │ │ ├── json.go │ │ ├── mask.go │ │ ├── mask_safe.go │ │ ├── prepared.go │ │ ├── proxy.go │ │ ├── server.go │ │ ├── trace.go │ │ ├── trace_17.go │ │ ├── util.go │ │ └── x_net_proxy.go └── v2 │ ├── CLI.go │ ├── CLI_test.go │ ├── Gopkg.lock │ ├── Gopkg.toml │ ├── blind_alerter.go │ ├── cmd │ ├── cli │ │ └── main.go │ └── webserver │ │ ├── game.html │ │ └── main.go │ ├── file_system_store.go │ ├── file_system_store_test.go │ ├── game.go │ ├── game.html │ ├── league.go │ ├── player_server_ws.go │ ├── server.go │ ├── server_integration_test.go │ ├── server_test.go │ ├── tape.go │ ├── tape_test.go │ ├── testing.go │ ├── texas_holdem.go │ ├── texas_holdem_test.go │ └── vendor │ └── github.com │ └── gorilla │ └── websocket │ ├── .gitignore │ ├── .travis.yml │ ├── AUTHORS │ ├── LICENSE │ ├── README.md │ ├── client.go │ ├── client_clone.go │ ├── client_clone_legacy.go │ ├── compression.go │ ├── conn.go │ ├── conn_write.go │ ├── conn_write_legacy.go │ ├── doc.go │ ├── json.go │ ├── mask.go │ ├── mask_safe.go │ ├── prepared.go │ ├── proxy.go │ ├── server.go │ ├── trace.go │ ├── trace_17.go │ ├── util.go │ └── x_net_proxy.go ├── why.md └── working-without-mocks.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # Top-most EditorConfig file 2 | root = true 3 | 4 | # Every file should according to these default configurations if not specified 5 | [*] 6 | # Use UNIX-style line endings 7 | end_of_line = LF 8 | # Use utf-8 file encoding 9 | charset = utf-8 10 | # 4 space indent 11 | indent_style = space 12 | indent_size = 4 13 | # Ensure file ends with a newline when saving(prevent `no newline at EOF`) 14 | insert_final_newline = true 15 | # Remove any whitespace characters preceding newline characters 16 | trim_trailing_whitespace = true 17 | 18 | # For YAML 19 | [*.{yml,yaml}] 20 | indent_size = 2 21 | 22 | # For Go files 23 | [*.go] 24 | # `gofmt` uses tabs for indentation 25 | indent_style = tab 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [quii] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | 4 | # Book build output 5 | _book/ 6 | 7 | # eBook build output 8 | *.epub 9 | *.mobi 10 | *.pdf 11 | 12 | # templated files 13 | meta.tex 14 | 15 | game.db.json 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | git_recurse true 2 | 3 | rules "MD001", "MD002", "MD003", "MD004", "MD005", "MD006", "MD007", "MD008", "MD009", "MD010", "MD011", "MD012", "MD014", "MD015", "MD016", "MD017", "MD018", "MD019", "MD020", "MD021", "MD023", "MD025", "MD028", "MD030", "MD035", "MD037", "MD038", "MD040", "MD041", "MD042" 4 | 5 | # exclude rules 6 | exclude "MD013" # Line length 7 | exclude "MD022" # Headers should be surrounded by blank lines 8 | exclude "MD024" # Multiple headers with the same content 9 | exclude "MD026" # Trailing punctuation in header 10 | exclude "MD027" # Multiple spaces after blockquote symbol 11 | exclude "MD029" # Ordered list item prefix 12 | exclude "MD031" # Fenced code blocks should be surrounded by blank lines 13 | exclude "MD032" # Lists should be surrounded by blank lines 14 | exclude "MD033" # Inline HTML 15 | exclude "MD034" # Bare URL used 16 | exclude "MD036" # Emphasis used instead of a header 17 | exclude "MD039" # Spaces inside link text 18 | -------------------------------------------------------------------------------- /TDD-outside-in.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/TDD-outside-in.jpg -------------------------------------------------------------------------------- /amazing-art.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/amazing-art.png -------------------------------------------------------------------------------- /arrays/v1/sum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Sum calculates the total from an array of numbers. 4 | func Sum(numbers [5]int) int { 5 | sum := 0 6 | for i := 0; i < 5; i++ { 7 | sum += numbers[i] 8 | } 9 | return sum 10 | } 11 | -------------------------------------------------------------------------------- /arrays/v1/sum_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestSum(t *testing.T) { 6 | 7 | numbers := [5]int{1, 2, 3, 4, 5} 8 | 9 | got := Sum(numbers) 10 | want := 15 11 | 12 | if want != got { 13 | t.Errorf("got %d want %d given, %v", got, want, numbers) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /arrays/v2/sum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Sum calculates the total from an array of numbers. 4 | func Sum(numbers [5]int) int { 5 | sum := 0 6 | for _, number := range numbers { 7 | sum += number 8 | } 9 | return sum 10 | } 11 | -------------------------------------------------------------------------------- /arrays/v2/sum_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestSum(t *testing.T) { 6 | 7 | numbers := [5]int{1, 2, 3, 4, 5} 8 | 9 | got := Sum(numbers) 10 | want := 15 11 | 12 | if got != want { 13 | t.Errorf("got %d want %d given, %v", got, want, numbers) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /arrays/v3/sum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Sum calculates the total from a slice of numbers. 4 | func Sum(numbers []int) int { 5 | sum := 0 6 | for _, number := range numbers { 7 | sum += number 8 | } 9 | return sum 10 | } 11 | -------------------------------------------------------------------------------- /arrays/v3/sum_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestSum(t *testing.T) { 6 | 7 | t.Run("collections of any size", func(t *testing.T) { 8 | 9 | numbers := []int{1, 2, 3} 10 | 11 | got := Sum(numbers) 12 | want := 6 13 | 14 | if got != want { 15 | t.Errorf("got %d want %d given, %v", got, want, numbers) 16 | } 17 | }) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /arrays/v4/sum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Sum calculates the total from a slice of numbers. 4 | func Sum(numbers []int) int { 5 | sum := 0 6 | for _, number := range numbers { 7 | sum += number 8 | } 9 | return sum 10 | } 11 | 12 | // SumAll calculates the respective sums of every slice passed in. 13 | func SumAll(numbersToSum ...[]int) []int { 14 | lengthOfNumbers := len(numbersToSum) 15 | sums := make([]int, lengthOfNumbers) 16 | 17 | for i, numbers := range numbersToSum { 18 | sums[i] = Sum(numbers) 19 | } 20 | 21 | return sums 22 | } 23 | -------------------------------------------------------------------------------- /arrays/v4/sum_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSum(t *testing.T) { 9 | 10 | t.Run("collections of any size", func(t *testing.T) { 11 | 12 | numbers := []int{1, 2, 3} 13 | 14 | got := Sum(numbers) 15 | want := 6 16 | 17 | if got != want { 18 | t.Errorf("got %d want %d given, %v", got, want, numbers) 19 | } 20 | }) 21 | 22 | } 23 | 24 | func TestSumAll(t *testing.T) { 25 | 26 | got := SumAll([]int{1, 2}, []int{0, 9}) 27 | want := []int{3, 9} 28 | 29 | if !reflect.DeepEqual(got, want) { 30 | t.Errorf("got %v want %v", got, want) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /arrays/v5/sum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Sum calculates the total from a slice of numbers. 4 | func Sum(numbers []int) int { 5 | sum := 0 6 | for _, number := range numbers { 7 | sum += number 8 | } 9 | return sum 10 | } 11 | 12 | // SumAll calculates the respective sums of every slice passed in. 13 | func SumAll(numbersToSum ...[]int) []int { 14 | var sums []int 15 | for _, numbers := range numbersToSum { 16 | sums = append(sums, Sum(numbers)) 17 | } 18 | 19 | return sums 20 | } 21 | -------------------------------------------------------------------------------- /arrays/v5/sum_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSum(t *testing.T) { 9 | 10 | t.Run("collections of any size", func(t *testing.T) { 11 | 12 | numbers := []int{1, 2, 3} 13 | 14 | got := Sum(numbers) 15 | want := 6 16 | 17 | if got != want { 18 | t.Errorf("got %d want %d given, %v", got, want, numbers) 19 | } 20 | }) 21 | 22 | } 23 | 24 | func TestSumAll(t *testing.T) { 25 | 26 | got := SumAll([]int{1, 2}, []int{0, 9}) 27 | want := []int{3, 9} 28 | 29 | if !reflect.DeepEqual(got, want) { 30 | t.Errorf("got %v want %v", got, want) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /arrays/v6/sum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Sum calculates the total from a slice of numbers. 4 | func Sum(numbers []int) int { 5 | sum := 0 6 | for _, number := range numbers { 7 | sum += number 8 | } 9 | return sum 10 | } 11 | 12 | // SumAllTails calculates the respective sums of every slice passed in. 13 | func SumAllTails(numbersToSum ...[]int) []int { 14 | var sums []int 15 | for _, numbers := range numbersToSum { 16 | tail := numbers[1:] 17 | sums = append(sums, Sum(tail)) 18 | } 19 | 20 | return sums 21 | } 22 | -------------------------------------------------------------------------------- /arrays/v6/sum_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSum(t *testing.T) { 9 | 10 | t.Run("collections of any size", func(t *testing.T) { 11 | 12 | numbers := []int{1, 2, 3} 13 | 14 | got := Sum(numbers) 15 | want := 6 16 | 17 | if got != want { 18 | t.Errorf("got %d want %d given, %v", got, want, numbers) 19 | } 20 | }) 21 | 22 | } 23 | 24 | func TestSumAllTails(t *testing.T) { 25 | 26 | got := SumAllTails([]int{1, 2}, []int{0, 9}) 27 | want := []int{2, 9} 28 | 29 | if !reflect.DeepEqual(got, want) { 30 | t.Errorf("got %v want %v", got, want) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /arrays/v7/sum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Sum calculates the total from a slice of numbers. 4 | func Sum(numbers []int) int { 5 | sum := 0 6 | for _, number := range numbers { 7 | sum += number 8 | } 9 | return sum 10 | } 11 | 12 | // SumAllTails calculates the sums of all but the first number given a collection of slices. 13 | func SumAllTails(numbersToSum ...[]int) []int { 14 | var sums []int 15 | for _, numbers := range numbersToSum { 16 | if len(numbers) == 0 { 17 | sums = append(sums, 0) 18 | } else { 19 | tail := numbers[1:] 20 | sums = append(sums, Sum(tail)) 21 | } 22 | } 23 | 24 | return sums 25 | } 26 | -------------------------------------------------------------------------------- /arrays/v7/sum_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSum(t *testing.T) { 9 | 10 | t.Run("collections of any size", func(t *testing.T) { 11 | 12 | numbers := []int{1, 2, 3} 13 | 14 | got := Sum(numbers) 15 | want := 6 16 | 17 | if got != want { 18 | t.Errorf("got %d want %d given, %v", got, want, numbers) 19 | } 20 | }) 21 | 22 | } 23 | 24 | func TestSumAllTails(t *testing.T) { 25 | 26 | checkSums := func(t *testing.T, got, want []int) { 27 | if !reflect.DeepEqual(got, want) { 28 | t.Errorf("got %v want %v", got, want) 29 | } 30 | } 31 | 32 | t.Run("make the sums of tails of", func(t *testing.T) { 33 | got := SumAllTails([]int{1, 2}, []int{0, 9}) 34 | want := []int{2, 9} 35 | checkSums(t, got, want) 36 | }) 37 | 38 | t.Run("safely sum empty slices", func(t *testing.T) { 39 | got := SumAllTails([]int{}, []int{3, 4, 5}) 40 | want := []int{0, 9} 41 | checkSums(t, got, want) 42 | }) 43 | 44 | } 45 | -------------------------------------------------------------------------------- /arrays/v8/assert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func AssertEqual[T comparable](t *testing.T, got, want T) { 6 | t.Helper() 7 | if got != want { 8 | t.Errorf("got %v, want %v", got, want) 9 | } 10 | } 11 | 12 | func AssertNotEqual[T comparable](t *testing.T, got, want T) { 13 | t.Helper() 14 | if got == want { 15 | t.Errorf("didn't want %v", got) 16 | } 17 | } 18 | 19 | func AssertTrue(t *testing.T, got bool) { 20 | t.Helper() 21 | if !got { 22 | t.Errorf("got %v, want true", got) 23 | } 24 | } 25 | 26 | func AssertFalse(t *testing.T, got bool) { 27 | t.Helper() 28 | if got { 29 | t.Errorf("got %v, want false", got) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /arrays/v8/bad_bank.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Transaction struct { 4 | From string 5 | To string 6 | Sum float64 7 | } 8 | 9 | func NewTransaction(from, to Account, sum float64) Transaction { 10 | return Transaction{From: from.Name, To: to.Name, Sum: sum} 11 | } 12 | 13 | type Account struct { 14 | Name string 15 | Balance float64 16 | } 17 | 18 | func NewBalanceFor(account Account, transactions []Transaction) Account { 19 | return Reduce( 20 | transactions, 21 | applyTransaction, 22 | account, 23 | ) 24 | } 25 | 26 | func applyTransaction(a Account, transaction Transaction) Account { 27 | if transaction.From == a.Name { 28 | a.Balance -= transaction.Sum 29 | } 30 | if transaction.To == a.Name { 31 | a.Balance += transaction.Sum 32 | } 33 | return a 34 | } 35 | -------------------------------------------------------------------------------- /arrays/v8/bad_bank_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestBadBank(t *testing.T) { 6 | var ( 7 | riya = Account{Name: "Riya", Balance: 100} 8 | chris = Account{Name: "Chris", Balance: 75} 9 | adil = Account{Name: "Adil", Balance: 200} 10 | 11 | transactions = []Transaction{ 12 | NewTransaction(chris, riya, 100), 13 | NewTransaction(adil, chris, 25), 14 | } 15 | ) 16 | 17 | newBalanceFor := func(account Account) float64 { 18 | return NewBalanceFor(account, transactions).Balance 19 | } 20 | 21 | AssertEqual(t, newBalanceFor(riya), 200) 22 | AssertEqual(t, newBalanceFor(chris), 0) 23 | AssertEqual(t, newBalanceFor(adil), 175) 24 | } 25 | -------------------------------------------------------------------------------- /arrays/v8/collection_fun.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func Find[A any](items []A, predicate func(A) bool) (value A, found bool) { 4 | for _, v := range items { 5 | if predicate(v) { 6 | return v, true 7 | } 8 | } 9 | return 10 | } 11 | 12 | func Reduce[A, B any](collection []A, f func(B, A) B, initialValue B) B { 13 | var result = initialValue 14 | for _, x := range collection { 15 | result = f(result, x) 16 | } 17 | return result 18 | } 19 | -------------------------------------------------------------------------------- /arrays/v8/sum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Sum calculates the total from a slice of numbers. 4 | func Sum(numbers []int) int { 5 | add := func(acc, x int) int { return acc + x } 6 | return Reduce(numbers, add, 0) 7 | } 8 | 9 | // SumAllTails calculates the sums of all but the first number given a collection of slices. 10 | func SumAllTails(numbers ...[]int) []int { 11 | sumTail := func(acc, x []int) []int { 12 | if len(x) == 0 { 13 | return append(acc, 0) 14 | } else { 15 | tail := x[1:] 16 | return append(acc, Sum(tail)) 17 | } 18 | } 19 | 20 | return Reduce(numbers, sumTail, []int{}) 21 | } 22 | -------------------------------------------------------------------------------- /blogrenderer/post.go: -------------------------------------------------------------------------------- 1 | package blogrenderer 2 | 3 | import "strings" 4 | 5 | // Post is a representation of a post 6 | type Post struct { 7 | Title, Description, Body string 8 | Tags []string 9 | } 10 | 11 | // SanitisedTitle returns the title of the post with spaces replaced by dashes for pleasant URLs 12 | func (p Post) SanitisedTitle() string { 13 | return strings.ToLower(strings.Replace(p.Title, " ", "-", -1)) 14 | } 15 | -------------------------------------------------------------------------------- /blogrenderer/renderer_test.TestRender.it_renders_an_index_of_posts.approved.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My amazing blog! 5 | 6 | 7 | 8 | 9 | 19 |
20 | 21 |
  1. Hello World
  2. Hello World 2
22 | 23 |
24 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /blogrenderer/templates/blog.gohtml: -------------------------------------------------------------------------------- 1 | {{template "top" .}} 2 |

{{.Title}}

3 | 4 |

{{.Description}}

5 | 6 | Tags: 7 | {{.HTMLBody}} 8 | {{template "bottom" .}} 9 | -------------------------------------------------------------------------------- /blogrenderer/templates/bottom.gohtml: -------------------------------------------------------------------------------- 1 | {{define "bottom"}} 2 | 3 | 9 | 10 | 11 | {{end}} 12 | -------------------------------------------------------------------------------- /blogrenderer/templates/index.gohtml: -------------------------------------------------------------------------------- 1 | {{template "top" .}} 2 |
    {{range .}}
  1. {{.Title}}
  2. {{end}}
3 | {{template "bottom" .}} 4 | -------------------------------------------------------------------------------- /blogrenderer/templates/top.gohtml: -------------------------------------------------------------------------------- 1 | {{define "top"}} 2 | 3 | 4 | My amazing blog! 5 | 6 | 7 | 8 | 9 | 19 |
20 | {{end}} 21 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "structure": { 3 | "readme": "gb-readme.md" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | go install github.com/client9/misspell/cmd/misspell@latest 6 | go install github.com/po3rin/gofmtmd/cmd/gofmtmd@latest 7 | 8 | ls *.md | xargs misspell -error 9 | 10 | for md_file in ./*.md; do 11 | echo "formatting file: $md_file" 12 | gofmtmd "$md_file" -r 13 | done 14 | 15 | go test ./... 16 | go vet ./... 17 | go fmt ./... 18 | -------------------------------------------------------------------------------- /command-line/v1/cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("Let's play poker") 7 | } 8 | -------------------------------------------------------------------------------- /command-line/v1/cmd/webserver/game.db.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /command-line/v1/cmd/webserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quii/learn-go-with-tests/command-line/v1" 5 | "log" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | const dbFileName = "game.db.json" 11 | 12 | func main() { 13 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 14 | 15 | if err != nil { 16 | log.Fatalf("problem opening %s %v", dbFileName, err) 17 | } 18 | 19 | store, err := poker.NewFileSystemPlayerStore(db) 20 | 21 | if err != nil { 22 | log.Fatalf("problem creating file system player store, %v ", err) 23 | } 24 | 25 | server := poker.NewPlayerServer(store) 26 | 27 | log.Fatal(http.ListenAndServe(":5000", server)) 28 | } 29 | -------------------------------------------------------------------------------- /command-line/v1/league.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a league. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a league from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing league, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /command-line/v1/tape.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type tape struct { 9 | file *os.File 10 | } 11 | 12 | func (t *tape) Write(p []byte) (n int, err error) { 13 | t.file.Truncate(0) 14 | t.file.Seek(0, io.SeekStart) 15 | return t.file.Write(p) 16 | } 17 | -------------------------------------------------------------------------------- /command-line/v1/tape_test.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func TestTape_Write(t *testing.T) { 9 | file, clean := createTempFile(t, "12345") 10 | defer clean() 11 | 12 | tape := &tape{file} 13 | 14 | tape.Write([]byte("abc")) 15 | 16 | file.Seek(0, io.SeekStart) 17 | newFileContents, _ := io.ReadAll(file) 18 | 19 | got := string(newFileContents) 20 | want := "abc" 21 | 22 | if got != want { 23 | t.Errorf("got %q want %q", got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /command-line/v2/CLI.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | // CLI helps players through a game of poker. 4 | type CLI struct { 5 | playerStore PlayerStore 6 | } 7 | 8 | // PlayPoker starts the game. 9 | func (cli *CLI) PlayPoker() { 10 | cli.playerStore.RecordWin("Cleo") 11 | } 12 | -------------------------------------------------------------------------------- /command-line/v2/CLI_test.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCLI(t *testing.T) { 8 | playerStore := &StubPlayerStore{} 9 | 10 | cli := &CLI{playerStore} 11 | cli.PlayPoker() 12 | 13 | if len(playerStore.winCalls) != 1 { 14 | t.Fatal("expected a win call but didn't get any") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /command-line/v2/cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("Let's play poker") 7 | } 8 | -------------------------------------------------------------------------------- /command-line/v2/cmd/webserver/game.db.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /command-line/v2/cmd/webserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quii/learn-go-with-tests/command-line/v1" 5 | "log" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | const dbFileName = "game.db.json" 11 | 12 | func main() { 13 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 14 | 15 | if err != nil { 16 | log.Fatalf("problem opening %s %v", dbFileName, err) 17 | } 18 | 19 | store, err := poker.NewFileSystemPlayerStore(db) 20 | 21 | if err != nil { 22 | log.Fatalf("problem creating file system player store, %v ", err) 23 | } 24 | 25 | server := poker.NewPlayerServer(store) 26 | 27 | log.Fatal(http.ListenAndServe(":5000", server)) 28 | } 29 | -------------------------------------------------------------------------------- /command-line/v2/league.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a league. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a league from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing league, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /command-line/v2/tape.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type tape struct { 9 | file *os.File 10 | } 11 | 12 | func (t *tape) Write(p []byte) (n int, err error) { 13 | t.file.Truncate(0) 14 | t.file.Seek(0, io.SeekStart) 15 | return t.file.Write(p) 16 | } 17 | -------------------------------------------------------------------------------- /command-line/v2/tape_test.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func TestTape_Write(t *testing.T) { 9 | file, clean := createTempFile(t, "12345") 10 | defer clean() 11 | 12 | tape := &tape{file} 13 | 14 | tape.Write([]byte("abc")) 15 | 16 | file.Seek(0, io.SeekStart) 17 | newFileContents, _ := io.ReadAll(file) 18 | 19 | got := string(newFileContents) 20 | want := "abc" 21 | 22 | if got != want { 23 | t.Errorf("got %q want %q", got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /command-line/v3/CLI.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | // CLI helps players through a game of poker. 10 | type CLI struct { 11 | playerStore PlayerStore 12 | in *bufio.Scanner 13 | } 14 | 15 | // NewCLI creates a CLI for playing poker. 16 | func NewCLI(store PlayerStore, in io.Reader) *CLI { 17 | return &CLI{ 18 | playerStore: store, 19 | in: bufio.NewScanner(in), 20 | } 21 | } 22 | 23 | // PlayPoker starts the game. 24 | func (cli *CLI) PlayPoker() { 25 | userInput := cli.readLine() 26 | cli.playerStore.RecordWin(extractWinner(userInput)) 27 | } 28 | 29 | func extractWinner(userInput string) string { 30 | return strings.Replace(userInput, " wins", "", 1) 31 | } 32 | 33 | func (cli *CLI) readLine() string { 34 | cli.in.Scan() 35 | return cli.in.Text() 36 | } 37 | -------------------------------------------------------------------------------- /command-line/v3/cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | poker "github.com/quii/learn-go-with-tests/command-line/v3" 9 | ) 10 | 11 | const dbFileName = "game.db.json" 12 | 13 | func main() { 14 | store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName) 15 | 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | defer close() 20 | 21 | fmt.Println("Let's play poker") 22 | fmt.Println("Type {Name} wins to record a win") 23 | poker.NewCLI(store, os.Stdin).PlayPoker() 24 | } 25 | -------------------------------------------------------------------------------- /command-line/v3/cmd/webserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | poker "github.com/quii/learn-go-with-tests/command-line/v3" 8 | ) 9 | 10 | const dbFileName = "game.db.json" 11 | 12 | func main() { 13 | store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName) 14 | 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | defer close() 19 | 20 | server := poker.NewPlayerServer(store) 21 | 22 | log.Fatal(http.ListenAndServe(":5000", server)) 23 | } 24 | -------------------------------------------------------------------------------- /command-line/v3/league.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a league. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a league from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing league, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /command-line/v3/tape.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type tape struct { 9 | file *os.File 10 | } 11 | 12 | func (t *tape) Write(p []byte) (n int, err error) { 13 | t.file.Truncate(0) 14 | t.file.Seek(0, io.SeekStart) 15 | return t.file.Write(p) 16 | } 17 | -------------------------------------------------------------------------------- /command-line/v3/tape_test.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func TestTape_Write(t *testing.T) { 9 | file, clean := createTempFile(t, "12345") 10 | defer clean() 11 | 12 | tape := &tape{file} 13 | 14 | tape.Write([]byte("abc")) 15 | 16 | file.Seek(0, io.SeekStart) 17 | newFileContents, _ := io.ReadAll(file) 18 | 19 | got := string(newFileContents) 20 | want := "abc" 21 | 22 | if got != want { 23 | t.Errorf("got %q want %q", got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /concurrency/v1/check_website.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | import "net/http" 4 | 5 | // CheckWebsite returns true if the URL returns a 200 status code, false otherwise. 6 | func CheckWebsite(url string) bool { 7 | response, err := http.Head(url) 8 | if err != nil { 9 | return false 10 | } 11 | 12 | return response.StatusCode == http.StatusOK 13 | } 14 | -------------------------------------------------------------------------------- /concurrency/v1/check_websites.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | // WebsiteChecker checks a url, returning a bool. 4 | type WebsiteChecker func(string) bool 5 | 6 | // CheckWebsites takes a WebsiteChecker and a slice of urls and returns a map. 7 | // of urls to the result of checking each url with the WebsiteChecker function. 8 | func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { 9 | results := make(map[string]bool) 10 | 11 | for _, url := range urls { 12 | results[url] = wc(url) 13 | } 14 | 15 | return results 16 | } 17 | -------------------------------------------------------------------------------- /concurrency/v1/check_websites_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func slowStubWebsiteChecker(_ string) bool { 9 | time.Sleep(20 * time.Millisecond) 10 | return true 11 | } 12 | 13 | func BenchmarkCheckWebsites(b *testing.B) { 14 | urls := make([]string, 100) 15 | for i := 0; i < len(urls); i++ { 16 | urls[i] = "a url" 17 | } 18 | 19 | for i := 0; i < b.N; i++ { 20 | CheckWebsites(slowStubWebsiteChecker, urls) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /concurrency/v1/check_websites_test.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func mockWebsiteChecker(url string) bool { 9 | if url == "waat://furhurterwe.geds" { 10 | return false 11 | } 12 | return true 13 | } 14 | 15 | func TestCheckWebsites(t *testing.T) { 16 | websites := []string{ 17 | "http://google.com", 18 | "http://blog.gypsydave5.com", 19 | "waat://furhurterwe.geds", 20 | } 21 | 22 | want := map[string]bool{ 23 | "http://google.com": true, 24 | "http://blog.gypsydave5.com": true, 25 | "waat://furhurterwe.geds": false, 26 | } 27 | 28 | got := CheckWebsites(mockWebsiteChecker, websites) 29 | 30 | if !reflect.DeepEqual(want, got) { 31 | t.Fatalf("wanted %v, got %v", want, got) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /concurrency/v2/check_website.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | import "net/http" 4 | 5 | // CheckWebsite returns true if the URL returns a 200 status code, false otherwise. 6 | func CheckWebsite(url string) bool { 7 | response, err := http.Head(url) 8 | if err != nil { 9 | return false 10 | } 11 | 12 | return response.StatusCode == http.StatusOK 13 | } 14 | -------------------------------------------------------------------------------- /concurrency/v2/check_websites.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // WebsiteChecker checks a url, returning a bool. 8 | type WebsiteChecker func(string) bool 9 | 10 | // CheckWebsites takes a WebsiteChecker and a slice of urls and returns a map. 11 | // of urls to the result of checking each url with the WebsiteChecker function. 12 | func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { 13 | results := make(map[string]bool) 14 | 15 | for _, url := range urls { 16 | go func() { 17 | results[url] = wc(url) 18 | }() 19 | } 20 | 21 | time.Sleep(2 * time.Second) 22 | 23 | return results 24 | } 25 | -------------------------------------------------------------------------------- /concurrency/v2/check_websites_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func slowStubWebsiteChecker(_ string) bool { 9 | time.Sleep(20 * time.Millisecond) 10 | return true 11 | } 12 | 13 | func BenchmarkCheckWebsites(b *testing.B) { 14 | urls := make([]string, 100) 15 | for i := 0; i < len(urls); i++ { 16 | urls[i] = "a url" 17 | } 18 | 19 | for i := 0; i < b.N; i++ { 20 | CheckWebsites(slowStubWebsiteChecker, urls) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /concurrency/v2/check_websites_test.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func mockWebsiteChecker(url string) bool { 9 | if url == "waat://furhurterwe.geds" { 10 | return false 11 | } 12 | return true 13 | } 14 | 15 | func TestCheckWebsites(t *testing.T) { 16 | websites := []string{ 17 | "http://google.com", 18 | "http://blog.gypsydave5.com", 19 | "waat://furhurterwe.geds", 20 | } 21 | 22 | want := map[string]bool{ 23 | "http://google.com": true, 24 | "http://blog.gypsydave5.com": true, 25 | "waat://furhurterwe.geds": false, 26 | } 27 | 28 | got := CheckWebsites(mockWebsiteChecker, websites) 29 | 30 | if !reflect.DeepEqual(want, got) { 31 | t.Fatalf("wanted %v, got %v", want, got) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /concurrency/v3/check_website.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | import "net/http" 4 | 5 | // CheckWebsite returns true if the URL returns a 200 status code, false otherwise. 6 | func CheckWebsite(url string) bool { 7 | response, err := http.Head(url) 8 | if err != nil { 9 | return false 10 | } 11 | 12 | return response.StatusCode == http.StatusOK 13 | } 14 | -------------------------------------------------------------------------------- /concurrency/v3/check_websites.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | // WebsiteChecker checks a url, returning a bool. 4 | type WebsiteChecker func(string) bool 5 | type result struct { 6 | string 7 | bool 8 | } 9 | 10 | // CheckWebsites takes a WebsiteChecker and a slice of urls and returns a map. 11 | // of urls to the result of checking each url with the WebsiteChecker function. 12 | func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { 13 | results := make(map[string]bool) 14 | resultChannel := make(chan result) 15 | 16 | for _, url := range urls { 17 | go func() { 18 | resultChannel <- result{url, wc(url)} 19 | }() 20 | } 21 | 22 | for i := 0; i < len(urls); i++ { 23 | r := <-resultChannel 24 | results[r.string] = r.bool 25 | } 26 | 27 | return results 28 | } 29 | -------------------------------------------------------------------------------- /concurrency/v3/check_websites_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func slowStubWebsiteChecker(_ string) bool { 9 | time.Sleep(20 * time.Millisecond) 10 | return true 11 | } 12 | 13 | func BenchmarkCheckWebsites(b *testing.B) { 14 | urls := make([]string, 100) 15 | for i := 0; i < len(urls); i++ { 16 | urls[i] = "a url" 17 | } 18 | 19 | for i := 0; i < b.N; i++ { 20 | CheckWebsites(slowStubWebsiteChecker, urls) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /concurrency/v3/check_websites_test.go: -------------------------------------------------------------------------------- 1 | package concurrency 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func mockWebsiteChecker(url string) bool { 9 | if url == "waat://furhurterwe.geds" { 10 | return false 11 | } 12 | return true 13 | } 14 | 15 | func TestCheckWebsites(t *testing.T) { 16 | websites := []string{ 17 | "http://google.com", 18 | "http://blog.gypsydave5.com", 19 | "waat://furhurterwe.geds", 20 | } 21 | 22 | want := map[string]bool{ 23 | "http://google.com": true, 24 | "http://blog.gypsydave5.com": true, 25 | "waat://furhurterwe.geds": false, 26 | } 27 | 28 | got := CheckWebsites(mockWebsiteChecker, websites) 29 | 30 | if !reflect.DeepEqual(want, got) { 31 | t.Fatalf("wanted %v, got %v", want, got) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /context/v1/context.go: -------------------------------------------------------------------------------- 1 | package context1 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // Store fetches data. 9 | type Store interface { 10 | Fetch() string 11 | } 12 | 13 | // Server returns a handler for calling Store. 14 | func Server(store Store) http.HandlerFunc { 15 | return func(w http.ResponseWriter, r *http.Request) { 16 | fmt.Fprint(w, store.Fetch()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /context/v1/context_test.go: -------------------------------------------------------------------------------- 1 | package context1 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | type StubStore struct { 10 | response string 11 | } 12 | 13 | func (s *StubStore) Fetch() string { 14 | return s.response 15 | } 16 | 17 | func TestServer(t *testing.T) { 18 | data := "hello, world" 19 | svr := Server(&StubStore{data}) 20 | 21 | request := httptest.NewRequest(http.MethodGet, "/", nil) 22 | response := httptest.NewRecorder() 23 | 24 | svr.ServeHTTP(response, request) 25 | 26 | if response.Body.String() != data { 27 | t.Errorf(`got "%s", want "%s"`, response.Body.String(), data) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /context/v2/context.go: -------------------------------------------------------------------------------- 1 | package context2 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // Store fetches data. 9 | type Store interface { 10 | Fetch() string 11 | Cancel() 12 | } 13 | 14 | // Server returns a handler for calling Store. 15 | func Server(store Store) http.HandlerFunc { 16 | return func(w http.ResponseWriter, r *http.Request) { 17 | ctx := r.Context() 18 | 19 | data := make(chan string, 1) 20 | 21 | go func() { 22 | data <- store.Fetch() 23 | }() 24 | 25 | select { 26 | case d := <-data: 27 | fmt.Fprint(w, d) 28 | case <-ctx.Done(): 29 | store.Cancel() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /context/v2/testdoubles.go: -------------------------------------------------------------------------------- 1 | package context2 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | // SpyStore allows you to simulate a store and see how its used. 9 | type SpyStore struct { 10 | response string 11 | cancelled bool 12 | t *testing.T 13 | } 14 | 15 | // Fetch returns response after a short delay. 16 | func (s *SpyStore) Fetch() string { 17 | time.Sleep(100 * time.Millisecond) 18 | return s.response 19 | } 20 | 21 | // Cancel will record the call. 22 | func (s *SpyStore) Cancel() { 23 | s.cancelled = true 24 | } 25 | 26 | func (s *SpyStore) assertWasCancelled() { 27 | s.t.Helper() 28 | if !s.cancelled { 29 | s.t.Error("store was not told to cancel") 30 | } 31 | } 32 | 33 | func (s *SpyStore) assertWasNotCancelled() { 34 | s.t.Helper() 35 | if s.cancelled { 36 | s.t.Error("store was told to cancel") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /context/v3/context.go: -------------------------------------------------------------------------------- 1 | package context3 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // Store fetches data. 10 | type Store interface { 11 | Fetch(ctx context.Context) (string, error) 12 | } 13 | 14 | // Server returns a handler for calling Store. 15 | func Server(store Store) http.HandlerFunc { 16 | return func(w http.ResponseWriter, r *http.Request) { 17 | data, err := store.Fetch(r.Context()) 18 | 19 | if err != nil { 20 | return // todo: log error however you like 21 | } 22 | 23 | fmt.Fprint(w, data) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /di/v1/di.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // Greet sends a personalised greeting to writer. 10 | func Greet(writer io.Writer, name string) { 11 | fmt.Fprintf(writer, "Hello, %s", name) 12 | } 13 | 14 | func main() { 15 | Greet(os.Stdout, "Elodie") 16 | } 17 | -------------------------------------------------------------------------------- /di/v1/di_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestGreet(t *testing.T) { 9 | buffer := bytes.Buffer{} 10 | Greet(&buffer, "Chris") 11 | 12 | got := buffer.String() 13 | want := "Hello, Chris" 14 | 15 | if got != want { 16 | t.Errorf("got %q want %q", got, want) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /di/v2/di.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | // Greet sends a personalised greeting to writer. 11 | func Greet(writer io.Writer, name string) { 12 | fmt.Fprintf(writer, "Hello, %s", name) 13 | } 14 | 15 | // MyGreeterHandler says Hello, world over HTTP. 16 | func MyGreeterHandler(w http.ResponseWriter, r *http.Request) { 17 | Greet(w, "world") 18 | } 19 | 20 | func main() { 21 | log.Fatal(http.ListenAndServe(":5000", http.HandlerFunc(MyGreeterHandler))) 22 | } 23 | -------------------------------------------------------------------------------- /di/v2/di_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestGreet(t *testing.T) { 9 | buffer := bytes.Buffer{} 10 | Greet(&buffer, "Chris") 11 | 12 | got := buffer.String() 13 | want := "Hello, Chris" 14 | 15 | if got != want { 16 | t.Errorf("got %q want %q", got, want) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /epub-cover-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/epub-cover-small.png -------------------------------------------------------------------------------- /epub-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/epub-cover.png -------------------------------------------------------------------------------- /epub-cover.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/epub-cover.pxm -------------------------------------------------------------------------------- /for/v1/repeat.go: -------------------------------------------------------------------------------- 1 | package iteration 2 | 3 | // Repeat returns character repeated 5 times. 4 | func Repeat(character string) string { 5 | var repeated string 6 | for i := 0; i < 5; i++ { 7 | repeated = repeated + character 8 | } 9 | return repeated 10 | } 11 | -------------------------------------------------------------------------------- /for/v1/repeat_test.go: -------------------------------------------------------------------------------- 1 | package iteration 2 | 3 | import "testing" 4 | 5 | func TestRepeat(t *testing.T) { 6 | repeated := Repeat("a") 7 | expected := "aaaaa" 8 | 9 | if repeated != expected { 10 | t.Errorf("expected %q but got %q", expected, repeated) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /for/v2/repeat.go: -------------------------------------------------------------------------------- 1 | package iteration 2 | 3 | const repeatCount = 5 4 | 5 | // Repeat returns character repeated 5 times. 6 | func Repeat(character string) string { 7 | var repeated string 8 | for i := 0; i < repeatCount; i++ { 9 | repeated += character 10 | } 11 | return repeated 12 | } 13 | -------------------------------------------------------------------------------- /for/v2/repeat_test.go: -------------------------------------------------------------------------------- 1 | package iteration 2 | 3 | import "testing" 4 | 5 | func TestRepeat(t *testing.T) { 6 | repeated := Repeat("a") 7 | expected := "aaaaa" 8 | 9 | if repeated != expected { 10 | t.Errorf("expected %q but got %q", expected, repeated) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /for/v3/repeat.go: -------------------------------------------------------------------------------- 1 | package iteration 2 | 3 | import "strings" 4 | 5 | const repeatCount = 5 6 | 7 | // Repeat returns character repeated 5 times. 8 | func Repeat(character string) string { 9 | var repeated strings.Builder 10 | for i := 0; i < repeatCount; i++ { 11 | repeated.WriteString(character) 12 | } 13 | return repeated.String() 14 | } 15 | -------------------------------------------------------------------------------- /for/v3/repeat_test.go: -------------------------------------------------------------------------------- 1 | package iteration 2 | 3 | import "testing" 4 | 5 | func TestRepeat(t *testing.T) { 6 | repeated := Repeat("a") 7 | expected := "aaaaa" 8 | 9 | if repeated != expected { 10 | t.Errorf("expected %q but got %q", expected, repeated) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /for/vx/repeat.go: -------------------------------------------------------------------------------- 1 | package iteration 2 | 3 | const repeatCount = 5 4 | 5 | // Repeat returns character repeated 5 times. 6 | func Repeat(character string) string { 7 | var repeated string 8 | for i := 0; i < repeatCount; i++ { 9 | repeated += character 10 | } 11 | return repeated 12 | } 13 | -------------------------------------------------------------------------------- /for/vx/repeat_test.go: -------------------------------------------------------------------------------- 1 | package iteration 2 | 3 | import "testing" 4 | 5 | func TestRepeat(t *testing.T) { 6 | repeated := Repeat("a") 7 | expected := "aaaaa" 8 | 9 | if repeated != expected { 10 | t.Errorf("expected %q but got %q", expected, repeated) 11 | } 12 | } 13 | 14 | func BenchmarkRepeat(b *testing.B) { 15 | for i := 0; i < b.N; i++ { 16 | Repeat("a") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /generics/assert.go: -------------------------------------------------------------------------------- 1 | package generics 2 | 3 | import "testing" 4 | 5 | func AssertEqual[T comparable](t *testing.T, got, want T) { 6 | t.Helper() 7 | if got != want { 8 | t.Errorf("got %v, want %v", got, want) 9 | } 10 | } 11 | 12 | func AssertNotEqual[T comparable](t *testing.T, got, want T) { 13 | t.Helper() 14 | if got == want { 15 | t.Errorf("didn't want %v", got) 16 | } 17 | } 18 | 19 | func AssertTrue(t *testing.T, got bool) { 20 | t.Helper() 21 | if !got { 22 | t.Errorf("got %v, want true", got) 23 | } 24 | } 25 | 26 | func AssertFalse(t *testing.T, got bool) { 27 | t.Helper() 28 | if got { 29 | t.Errorf("got %v, want false", got) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /generics/stack.go: -------------------------------------------------------------------------------- 1 | package generics 2 | 3 | type Stack[T any] struct { 4 | values []T 5 | } 6 | 7 | func NewStack[T any]() *Stack[T] { 8 | return new(Stack[T]) 9 | } 10 | 11 | func (s *Stack[T]) Push(value T) { 12 | s.values = append(s.values, value) 13 | } 14 | 15 | func (s *Stack[T]) IsEmpty() bool { 16 | return len(s.values) == 0 17 | } 18 | 19 | func (s *Stack[T]) Pop() (T, bool) { 20 | if s.IsEmpty() { 21 | var zero T 22 | return zero, false 23 | } 24 | 25 | index := len(s.values) - 1 26 | el := s.values[index] 27 | s.values = s.values[:index] 28 | return el, true 29 | } 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/quii/learn-go-with-tests 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/approvals/go-approval-tests v0.0.0-20211008131110-0c40b30e0000 7 | github.com/gomarkdown/markdown v0.0.0-20240626202925-2eda941fd024 8 | github.com/gorilla/websocket v1.5.3 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/approvals/go-approval-tests v0.0.0-20211008131110-0c40b30e0000 h1:H152l3O+2XIXQu8IrqEXeqJOFCvSShUXs7+x0lw8V1k= 2 | github.com/approvals/go-approval-tests v0.0.0-20211008131110-0c40b30e0000/go.mod h1:PJOqSY8IofNv3heAD6k8E7EfFS6okiSS9bSAasaAUME= 3 | github.com/gomarkdown/markdown v0.0.0-20240626202925-2eda941fd024 h1:saBP362Qm7zDdDXqv61kI4rzhmLFq3Z1gx34xpl6cWE= 4 | github.com/gomarkdown/markdown v0.0.0-20240626202925-2eda941fd024/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= 5 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 6 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 7 | -------------------------------------------------------------------------------- /hello-world/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/hello-world/.DS_Store -------------------------------------------------------------------------------- /hello-world/v1/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("Hello, world") 7 | } 8 | -------------------------------------------------------------------------------- /hello-world/v2/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | // Hello returns a greeting. 6 | func Hello() string { 7 | return "Hello, world" 8 | } 9 | 10 | func main() { 11 | fmt.Println(Hello()) 12 | } 13 | -------------------------------------------------------------------------------- /hello-world/v2/hello_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestHello(t *testing.T) { 6 | got := Hello() 7 | want := "Hello, world" 8 | 9 | if got != want { 10 | t.Errorf("got %q want %q", got, want) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /hello-world/v3/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | // Hello returns a personalised greeting. 6 | func Hello(name string) string { 7 | return "Hello, " + name 8 | } 9 | 10 | func main() { 11 | fmt.Println(Hello("world")) 12 | } 13 | -------------------------------------------------------------------------------- /hello-world/v3/hello_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestHello(t *testing.T) { 6 | got := Hello("Chris") 7 | want := "Hello, Chris" 8 | 9 | if got != want { 10 | t.Errorf("got %q want %q", got, want) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /hello-world/v4/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | const englishHelloPrefix = "Hello, " 6 | 7 | // Hello returns a personalised greeting. 8 | func Hello(name string) string { 9 | return englishHelloPrefix + name 10 | } 11 | 12 | func main() { 13 | fmt.Println(Hello("world")) 14 | } 15 | -------------------------------------------------------------------------------- /hello-world/v4/hello_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestHello(t *testing.T) { 6 | got := Hello("Chris") 7 | want := "Hello, Chris" 8 | 9 | if got != want { 10 | t.Errorf("got %q want %q", got, want) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /hello-world/v5/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | const englishHelloPrefix = "Hello, " 6 | 7 | // Hello returns a personalised greeting, defaulting to Hello, world if an empty name is passed. 8 | func Hello(name string) string { 9 | if name == "" { 10 | name = "World" 11 | } 12 | return englishHelloPrefix + name 13 | } 14 | 15 | func main() { 16 | fmt.Println(Hello("world")) 17 | } 18 | -------------------------------------------------------------------------------- /hello-world/v5/hello_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestHello(t *testing.T) { 6 | t.Run("saying hello to people", func(t *testing.T) { 7 | got := Hello("Chris") 8 | want := "Hello, Chris" 9 | assertCorrectMessage(t, got, want) 10 | }) 11 | 12 | t.Run("empty string defaults to 'world'", func(t *testing.T) { 13 | got := Hello("") 14 | want := "Hello, World" 15 | assertCorrectMessage(t, got, want) 16 | }) 17 | 18 | } 19 | 20 | func assertCorrectMessage(t testing.TB, got, want string) { 21 | t.Helper() 22 | if got != want { 23 | t.Errorf("got %q want %q", got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /hello-world/v6/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | const spanish = "Spanish" 6 | const french = "French" 7 | const englishHelloPrefix = "Hello, " 8 | const spanishHelloPrefix = "Hola, " 9 | const frenchHelloPrefix = "Bonjour, " 10 | 11 | // Hello returns a personalised greeting in a given language. 12 | func Hello(name string, language string) string { 13 | if name == "" { 14 | name = "World" 15 | } 16 | 17 | if language == spanish { 18 | return spanishHelloPrefix + name 19 | } 20 | 21 | if language == french { 22 | return frenchHelloPrefix + name 23 | } 24 | 25 | return englishHelloPrefix + name 26 | } 27 | 28 | func main() { 29 | fmt.Println(Hello("world", "")) 30 | } 31 | -------------------------------------------------------------------------------- /hello-world/v6/hello_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestHello(t *testing.T) { 6 | t.Run("to a person", func(t *testing.T) { 7 | got := Hello("Chris", "") 8 | want := "Hello, Chris" 9 | assertCorrectMessage(t, got, want) 10 | }) 11 | 12 | t.Run("empty string", func(t *testing.T) { 13 | got := Hello("", "") 14 | want := "Hello, World" 15 | assertCorrectMessage(t, got, want) 16 | }) 17 | 18 | t.Run("in Spanish", func(t *testing.T) { 19 | got := Hello("Elodie", spanish) 20 | want := "Hola, Elodie" 21 | assertCorrectMessage(t, got, want) 22 | }) 23 | 24 | t.Run("in French", func(t *testing.T) { 25 | got := Hello("Lauren", french) 26 | want := "Bonjour, Lauren" 27 | assertCorrectMessage(t, got, want) 28 | }) 29 | 30 | } 31 | 32 | func assertCorrectMessage(t testing.TB, got, want string) { 33 | t.Helper() 34 | if got != want { 35 | t.Errorf("got %q want %q", got, want) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /hello-world/v7/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | const spanish = "Spanish" 6 | const french = "French" 7 | const englishHelloPrefix = "Hello, " 8 | const spanishHelloPrefix = "Hola, " 9 | const frenchHelloPrefix = "Bonjour, " 10 | 11 | // Hello returns a personalised greeting in a given language. 12 | func Hello(name string, language string) string { 13 | if name == "" { 14 | name = "World" 15 | } 16 | 17 | prefix := englishHelloPrefix 18 | 19 | switch language { 20 | case spanish: 21 | prefix = spanishHelloPrefix 22 | case french: 23 | prefix = frenchHelloPrefix 24 | } 25 | 26 | return prefix + name 27 | } 28 | 29 | func main() { 30 | fmt.Println(Hello("world", "")) 31 | } 32 | -------------------------------------------------------------------------------- /hello-world/v7/hello_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestHello(t *testing.T) { 6 | t.Run("saying hello to people", func(t *testing.T) { 7 | got := Hello("Chris", "") 8 | want := "Hello, Chris" 9 | assertCorrectMessage(t, got, want) 10 | }) 11 | 12 | t.Run("say hello world when an empty string is supplied", func(t *testing.T) { 13 | got := Hello("", "") 14 | want := "Hello, World" 15 | assertCorrectMessage(t, got, want) 16 | }) 17 | 18 | t.Run("say hello in Spanish", func(t *testing.T) { 19 | got := Hello("Elodie", spanish) 20 | want := "Hola, Elodie" 21 | assertCorrectMessage(t, got, want) 22 | }) 23 | 24 | t.Run("say hello in French", func(t *testing.T) { 25 | got := Hello("Lauren", french) 26 | want := "Bonjour, Lauren" 27 | assertCorrectMessage(t, got, want) 28 | }) 29 | } 30 | 31 | func assertCorrectMessage(t testing.TB, got, want string) { 32 | t.Helper() 33 | if got != want { 34 | t.Errorf("got %q want %q", got, want) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /hello-world/v8/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | const spanish = "Spanish" 6 | const french = "French" 7 | const englishHelloPrefix = "Hello, " 8 | const spanishHelloPrefix = "Hola, " 9 | const frenchHelloPrefix = "Bonjour, " 10 | 11 | // Hello returns a personalised greeting in a given language. 12 | func Hello(name string, language string) string { 13 | if name == "" { 14 | name = "World" 15 | } 16 | 17 | return greetingPrefix(language) + name 18 | } 19 | 20 | func greetingPrefix(language string) (prefix string) { 21 | switch language { 22 | case french: 23 | prefix = frenchHelloPrefix 24 | case spanish: 25 | prefix = spanishHelloPrefix 26 | default: 27 | prefix = englishHelloPrefix 28 | } 29 | return 30 | } 31 | 32 | func main() { 33 | fmt.Println(Hello("world", "")) 34 | } 35 | -------------------------------------------------------------------------------- /hello-world/v8/hello_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestHello(t *testing.T) { 6 | t.Run("to a person", func(t *testing.T) { 7 | got := Hello("Chris", "") 8 | want := "Hello, Chris" 9 | assertCorrectMessage(t, got, want) 10 | }) 11 | 12 | t.Run("empty string", func(t *testing.T) { 13 | got := Hello("", "") 14 | want := "Hello, World" 15 | assertCorrectMessage(t, got, want) 16 | }) 17 | 18 | t.Run("in Spanish", func(t *testing.T) { 19 | got := Hello("Elodie", spanish) 20 | want := "Hola, Elodie" 21 | assertCorrectMessage(t, got, want) 22 | }) 23 | 24 | t.Run("in French", func(t *testing.T) { 25 | got := Hello("Lauren", french) 26 | want := "Bonjour, Lauren" 27 | assertCorrectMessage(t, got, want) 28 | }) 29 | } 30 | 31 | func assertCorrectMessage(t testing.TB, got, want string) { 32 | t.Helper() 33 | if got != want { 34 | t.Errorf("got %q want %q", got, want) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /http-server/v1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | handler := http.HandlerFunc(PlayerServer) 10 | log.Fatal(http.ListenAndServe(":5000", handler)) 11 | } 12 | -------------------------------------------------------------------------------- /http-server/v1/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // PlayerServer currently returns "20" given _any_ request. 9 | func PlayerServer(w http.ResponseWriter, r *http.Request) { 10 | fmt.Fprint(w, "20") 11 | } 12 | -------------------------------------------------------------------------------- /http-server/v1/server_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestGETPlayers(t *testing.T) { 10 | request, _ := http.NewRequest(http.MethodGet, "/", nil) 11 | response := httptest.NewRecorder() 12 | 13 | PlayerServer(response, request) 14 | 15 | t.Run("returns Pepper's score", func(t *testing.T) { 16 | got := response.Body.String() 17 | want := "20" 18 | 19 | if got != want { 20 | t.Errorf("got %q, want %q", got, want) 21 | } 22 | }) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /http-server/v2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct{} 10 | 11 | // GetPlayerScore retrieves scores for a given player. 12 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 13 | return 123 14 | } 15 | 16 | func main() { 17 | server := &PlayerServer{&InMemoryPlayerStore{}} 18 | log.Fatal(http.ListenAndServe(":5000", server)) 19 | } 20 | -------------------------------------------------------------------------------- /http-server/v2/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // PlayerStore stores score information about players. 10 | type PlayerStore interface { 11 | GetPlayerScore(name string) int 12 | } 13 | 14 | // PlayerServer is a HTTP interface for player information. 15 | type PlayerServer struct { 16 | store PlayerStore 17 | } 18 | 19 | func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 20 | player := strings.TrimPrefix(r.URL.Path, "/players/") 21 | 22 | score := p.store.GetPlayerScore(player) 23 | 24 | if score == 0 { 25 | w.WriteHeader(http.StatusNotFound) 26 | } 27 | 28 | fmt.Fprint(w, score) 29 | } 30 | -------------------------------------------------------------------------------- /http-server/v3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct{} 10 | 11 | // GetPlayerScore retrieves scores for a given player. 12 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 13 | return 123 14 | } 15 | 16 | func main() { 17 | server := &PlayerServer{&InMemoryPlayerStore{}} 18 | log.Fatal(http.ListenAndServe(":5000", server)) 19 | } 20 | -------------------------------------------------------------------------------- /http-server/v3/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // PlayerStore stores score information about players. 10 | type PlayerStore interface { 11 | GetPlayerScore(name string) int 12 | } 13 | 14 | // PlayerServer is a HTTP interface for player information. 15 | type PlayerServer struct { 16 | store PlayerStore 17 | } 18 | 19 | func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 20 | 21 | switch r.Method { 22 | case http.MethodPost: 23 | p.processWin(w) 24 | case http.MethodGet: 25 | p.showScore(w, r) 26 | } 27 | 28 | } 29 | 30 | func (p *PlayerServer) showScore(w http.ResponseWriter, r *http.Request) { 31 | player := strings.TrimPrefix(r.URL.Path, "/players/") 32 | 33 | score := p.store.GetPlayerScore(player) 34 | 35 | if score == 0 { 36 | w.WriteHeader(http.StatusNotFound) 37 | } 38 | 39 | fmt.Fprint(w, score) 40 | } 41 | 42 | func (p *PlayerServer) processWin(w http.ResponseWriter) { 43 | w.WriteHeader(http.StatusAccepted) 44 | } 45 | -------------------------------------------------------------------------------- /http-server/v4/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct{} 10 | 11 | // RecordWin will record a player's win. 12 | func (i *InMemoryPlayerStore) RecordWin(name string) { 13 | } 14 | 15 | // GetPlayerScore retrieves scores for a given player. 16 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 17 | return 123 18 | } 19 | 20 | func main() { 21 | server := &PlayerServer{&InMemoryPlayerStore{}} 22 | log.Fatal(http.ListenAndServe(":5000", server)) 23 | } 24 | -------------------------------------------------------------------------------- /http-server/v5/in_memory_player_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "sync" 4 | 5 | // NewInMemoryPlayerStore initialises an empty player store. 6 | func NewInMemoryPlayerStore() *InMemoryPlayerStore { 7 | return &InMemoryPlayerStore{ 8 | map[string]int{}, 9 | sync.RWMutex{}, 10 | } 11 | } 12 | 13 | // InMemoryPlayerStore collects data about players in memory. 14 | type InMemoryPlayerStore struct { 15 | store map[string]int 16 | // A mutex is used to synchronize read/write access to the map 17 | lock sync.RWMutex 18 | } 19 | 20 | // RecordWin will record a player's win. 21 | func (i *InMemoryPlayerStore) RecordWin(name string) { 22 | i.lock.Lock() 23 | defer i.lock.Unlock() 24 | i.store[name]++ 25 | } 26 | 27 | // GetPlayerScore retrieves scores for a given player. 28 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 29 | i.lock.RLock() 30 | defer i.lock.RUnlock() 31 | return i.store[name] 32 | } 33 | -------------------------------------------------------------------------------- /http-server/v5/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | server := &PlayerServer{NewInMemoryPlayerStore()} 10 | log.Fatal(http.ListenAndServe(":5000", server)) 11 | } 12 | -------------------------------------------------------------------------------- /http-server/v5/server_integration_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestRecordingWinsAndRetrievingThem(t *testing.T) { 10 | store := NewInMemoryPlayerStore() 11 | server := PlayerServer{store} 12 | player := "Pepper" 13 | 14 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 15 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 16 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 17 | 18 | response := httptest.NewRecorder() 19 | server.ServeHTTP(response, newGetScoreRequest(player)) 20 | assertStatus(t, response.Code, http.StatusOK) 21 | 22 | assertResponseBody(t, response.Body.String(), "3") 23 | } 24 | -------------------------------------------------------------------------------- /integers/v1/adder.go: -------------------------------------------------------------------------------- 1 | package integers 2 | 3 | // Add takes two integers and returns the sum of them. 4 | func Add(x, y int) int { 5 | return x + y 6 | } 7 | -------------------------------------------------------------------------------- /integers/v1/adder_test.go: -------------------------------------------------------------------------------- 1 | package integers 2 | 3 | import "testing" 4 | 5 | func TestAdder(t *testing.T) { 6 | sum := Add(2, 2) 7 | expected := 4 8 | 9 | if sum != expected { 10 | t.Errorf("expected '%d' but got '%d'", expected, sum) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /integers/v2/adder.go: -------------------------------------------------------------------------------- 1 | package integers 2 | 3 | // Add takes two integers and returns the sum of them. 4 | func Add(x, y int) int { 5 | return x + y 6 | } 7 | -------------------------------------------------------------------------------- /integers/v2/adder_test.go: -------------------------------------------------------------------------------- 1 | package integers 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestAdder(t *testing.T) { 9 | sum := Add(2, 2) 10 | expected := 4 11 | 12 | if sum != expected { 13 | t.Errorf("expected '%d' but got '%d'", expected, sum) 14 | } 15 | } 16 | 17 | func ExampleAdd() { 18 | sum := Add(1, 5) 19 | fmt.Println(sum) 20 | // Output: 6 21 | } 22 | -------------------------------------------------------------------------------- /io/v1/file_system_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // FileSystemPlayerStore stores players in the filesystem. 8 | type FileSystemPlayerStore struct { 9 | database io.ReadSeeker 10 | } 11 | 12 | // GetLeague returns the scores of all the players. 13 | func (f *FileSystemPlayerStore) GetLeague() []Player { 14 | f.database.Seek(0, io.SeekStart) 15 | league, _ := NewLeague(f.database) 16 | return league 17 | } 18 | -------------------------------------------------------------------------------- /io/v1/file_system_store_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestFileSystemStore(t *testing.T) { 9 | 10 | t.Run("league from a reader", func(t *testing.T) { 11 | database := strings.NewReader(`[ 12 | {"Name": "Cleo", "Wins": 10}, 13 | {"Name": "Chris", "Wins": 33}]`) 14 | 15 | store := FileSystemPlayerStore{database} 16 | 17 | got := store.GetLeague() 18 | 19 | want := []Player{ 20 | {"Cleo", 10}, 21 | {"Chris", 33}, 22 | } 23 | 24 | assertLeague(t, got, want) 25 | 26 | // read again 27 | got = store.GetLeague() 28 | assertLeague(t, got, want) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /io/v1/in_memory_player_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NewInMemoryPlayerStore initialises an empty player store. 4 | func NewInMemoryPlayerStore() *InMemoryPlayerStore { 5 | return &InMemoryPlayerStore{map[string]int{}} 6 | } 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct { 10 | store map[string]int 11 | } 12 | 13 | // GetLeague returns a collection of Players. 14 | func (i *InMemoryPlayerStore) GetLeague() []Player { 15 | var league []Player 16 | for name, wins := range i.store { 17 | league = append(league, Player{name, wins}) 18 | } 19 | return league 20 | } 21 | 22 | // RecordWin will record a player's win. 23 | func (i *InMemoryPlayerStore) RecordWin(name string) { 24 | i.store[name]++ 25 | } 26 | 27 | // GetPlayerScore retrieves scores for a given player. 28 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 29 | return i.store[name] 30 | } 31 | -------------------------------------------------------------------------------- /io/v1/league.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | // NewLeague creates a league from JSON. 9 | func NewLeague(rdr io.Reader) ([]Player, error) { 10 | var league []Player 11 | err := json.NewDecoder(rdr).Decode(&league) 12 | return league, err 13 | } 14 | -------------------------------------------------------------------------------- /io/v1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | server := NewPlayerServer(NewInMemoryPlayerStore()) 10 | log.Fatal(http.ListenAndServe(":5000", server)) 11 | } 12 | -------------------------------------------------------------------------------- /io/v2/file_system_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // FileSystemPlayerStore stores players in the filesystem. 8 | type FileSystemPlayerStore struct { 9 | database io.ReadSeeker 10 | } 11 | 12 | // GetLeague returns the scores of all the players. 13 | func (f *FileSystemPlayerStore) GetLeague() []Player { 14 | f.database.Seek(0, io.SeekStart) 15 | league, _ := NewLeague(f.database) 16 | return league 17 | } 18 | 19 | // GetPlayerScore retrieves a player's score. 20 | func (f *FileSystemPlayerStore) GetPlayerScore(name string) int { 21 | 22 | var wins int 23 | 24 | for _, player := range f.GetLeague() { 25 | if player.Name == name { 26 | wins = player.Wins 27 | break 28 | } 29 | } 30 | 31 | return wins 32 | } 33 | -------------------------------------------------------------------------------- /io/v2/in_memory_player_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NewInMemoryPlayerStore initialises an empty player store. 4 | func NewInMemoryPlayerStore() *InMemoryPlayerStore { 5 | return &InMemoryPlayerStore{map[string]int{}} 6 | } 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct { 10 | store map[string]int 11 | } 12 | 13 | // GetLeague returns a collection of Players. 14 | func (i *InMemoryPlayerStore) GetLeague() []Player { 15 | var league []Player 16 | for name, wins := range i.store { 17 | league = append(league, Player{name, wins}) 18 | } 19 | return league 20 | } 21 | 22 | // RecordWin will record a player's win. 23 | func (i *InMemoryPlayerStore) RecordWin(name string) { 24 | i.store[name]++ 25 | } 26 | 27 | // GetPlayerScore retrieves scores for a given player. 28 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 29 | return i.store[name] 30 | } 31 | -------------------------------------------------------------------------------- /io/v2/league.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // NewLeague creates a league from JSON. 10 | func NewLeague(rdr io.Reader) ([]Player, error) { 11 | var league []Player 12 | err := json.NewDecoder(rdr).Decode(&league) 13 | 14 | if err != nil { 15 | err = fmt.Errorf("problem parsing league, %v", err) 16 | } 17 | 18 | return league, err 19 | } 20 | -------------------------------------------------------------------------------- /io/v2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | server := NewPlayerServer(NewInMemoryPlayerStore()) 10 | log.Fatal(http.ListenAndServe(":5000", server)) 11 | } 12 | -------------------------------------------------------------------------------- /io/v3/in_memory_player_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NewInMemoryPlayerStore initialises an empty player store. 4 | func NewInMemoryPlayerStore() *InMemoryPlayerStore { 5 | return &InMemoryPlayerStore{map[string]int{}} 6 | } 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct { 10 | store map[string]int 11 | } 12 | 13 | // GetLeague returns a collection of Players. 14 | func (i *InMemoryPlayerStore) GetLeague() League { 15 | var league []Player 16 | for name, wins := range i.store { 17 | league = append(league, Player{name, wins}) 18 | } 19 | return league 20 | } 21 | 22 | // RecordWin will record a player's win. 23 | func (i *InMemoryPlayerStore) RecordWin(name string) { 24 | i.store[name]++ 25 | } 26 | 27 | // GetPlayerScore retrieves scores for a given player. 28 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 29 | return i.store[name] 30 | } 31 | -------------------------------------------------------------------------------- /io/v3/league.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a league. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a league from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing league, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /io/v3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | server := NewPlayerServer(NewInMemoryPlayerStore()) 10 | log.Fatal(http.ListenAndServe(":5000", server)) 11 | } 12 | -------------------------------------------------------------------------------- /io/v4/in_memory_player_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NewInMemoryPlayerStore initialises an empty player store. 4 | func NewInMemoryPlayerStore() *InMemoryPlayerStore { 5 | return &InMemoryPlayerStore{map[string]int{}} 6 | } 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct { 10 | store map[string]int 11 | } 12 | 13 | // GetLeague returns a collection of Players. 14 | func (i *InMemoryPlayerStore) GetLeague() League { 15 | var league []Player 16 | for name, wins := range i.store { 17 | league = append(league, Player{name, wins}) 18 | } 19 | return league 20 | } 21 | 22 | // RecordWin will record a player's win. 23 | func (i *InMemoryPlayerStore) RecordWin(name string) { 24 | i.store[name]++ 25 | } 26 | 27 | // GetPlayerScore retrieves scores for a given player. 28 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 29 | return i.store[name] 30 | } 31 | -------------------------------------------------------------------------------- /io/v4/league.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a league. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a league from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing league, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /io/v4/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | server := NewPlayerServer(NewInMemoryPlayerStore()) 10 | log.Fatal(http.ListenAndServe(":5000", server)) 11 | } 12 | -------------------------------------------------------------------------------- /io/v5/league.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a league. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a league from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing league, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /io/v5/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | const dbFileName = "game.db.json" 10 | 11 | func main() { 12 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 13 | 14 | if err != nil { 15 | log.Fatalf("problem opening %s %v", dbFileName, err) 16 | } 17 | 18 | store := &FileSystemPlayerStore{db} 19 | server := NewPlayerServer(store) 20 | 21 | log.Fatal(http.ListenAndServe(":5000", server)) 22 | } 23 | -------------------------------------------------------------------------------- /io/v6/league.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a league. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a league from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing league, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /io/v6/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | const dbFileName = "game.db.json" 10 | 11 | func main() { 12 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 13 | 14 | if err != nil { 15 | log.Fatalf("problem opening %s %v", dbFileName, err) 16 | } 17 | 18 | store := NewFileSystemPlayerStore(db) 19 | 20 | server := NewPlayerServer(store) 21 | 22 | log.Fatal(http.ListenAndServe(":5000", server)) 23 | } 24 | -------------------------------------------------------------------------------- /io/v7/league.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a league. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a league from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing league, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /io/v7/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | const dbFileName = "game.db.json" 10 | 11 | func main() { 12 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 13 | 14 | if err != nil { 15 | log.Fatalf("problem opening %s %v", dbFileName, err) 16 | } 17 | 18 | store := NewFileSystemPlayerStore(db) 19 | 20 | server := NewPlayerServer(store) 21 | 22 | log.Fatal(http.ListenAndServe(":5000", server)) 23 | } 24 | -------------------------------------------------------------------------------- /io/v7/tape.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type tape struct { 9 | file *os.File 10 | } 11 | 12 | func (t *tape) Write(p []byte) (n int, err error) { 13 | t.file.Truncate(0) 14 | t.file.Seek(0, io.SeekStart) 15 | return t.file.Write(p) 16 | } 17 | -------------------------------------------------------------------------------- /io/v7/tape_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func TestTape_Write(t *testing.T) { 9 | file, clean := createTempFile(t, "12345") 10 | defer clean() 11 | 12 | tape := &tape{file} 13 | 14 | tape.Write([]byte("abc")) 15 | 16 | file.Seek(0, io.SeekStart) 17 | newFileContents, _ := io.ReadAll(file) 18 | 19 | got := string(newFileContents) 20 | want := "abc" 21 | 22 | if got != want { 23 | t.Errorf("got %q want %q", got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /io/v8/league.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a league. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a league from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing league, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /io/v8/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | const dbFileName = "game.db.json" 10 | 11 | func main() { 12 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 13 | 14 | if err != nil { 15 | log.Fatalf("problem opening %s %v", dbFileName, err) 16 | } 17 | 18 | store, err := NewFileSystemPlayerStore(db) 19 | 20 | if err != nil { 21 | log.Fatalf("problem creating file system player store, %v ", err) 22 | } 23 | 24 | server := NewPlayerServer(store) 25 | 26 | log.Fatal(http.ListenAndServe(":5000", server)) 27 | } 28 | -------------------------------------------------------------------------------- /io/v8/tape.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type tape struct { 9 | file *os.File 10 | } 11 | 12 | func (t *tape) Write(p []byte) (n int, err error) { 13 | t.file.Truncate(0) 14 | t.file.Seek(0, io.SeekStart) 15 | return t.file.Write(p) 16 | } 17 | -------------------------------------------------------------------------------- /io/v8/tape_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func TestTape_Write(t *testing.T) { 9 | file, clean := createTempFile(t, "12345") 10 | defer clean() 11 | 12 | tape := &tape{file} 13 | 14 | tape.Write([]byte("abc")) 15 | 16 | file.Seek(0, io.SeekStart) 17 | newFileContents, _ := io.ReadAll(file) 18 | 19 | got := string(newFileContents) 20 | want := "abc" 21 | 22 | if got != want { 23 | t.Errorf("got %q want %q", got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /io/v9/league.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a league. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a league from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing league, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /io/v9/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | const dbFileName = "game.db.json" 10 | 11 | func main() { 12 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 13 | 14 | if err != nil { 15 | log.Fatalf("problem opening %s %v", dbFileName, err) 16 | } 17 | 18 | store, err := NewFileSystemPlayerStore(db) 19 | 20 | if err != nil { 21 | log.Fatalf("problem creating file system player store, %v ", err) 22 | } 23 | 24 | server := NewPlayerServer(store) 25 | 26 | log.Fatal(http.ListenAndServe(":5000", server)) 27 | } 28 | -------------------------------------------------------------------------------- /io/v9/tape.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type tape struct { 9 | file *os.File 10 | } 11 | 12 | func (t *tape) Write(p []byte) (n int, err error) { 13 | t.file.Truncate(0) 14 | t.file.Seek(0, io.SeekStart) 15 | return t.file.Write(p) 16 | } 17 | -------------------------------------------------------------------------------- /io/v9/tape_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func TestTape_Write(t *testing.T) { 9 | file, clean := createTempFile(t, "12345") 10 | defer clean() 11 | 12 | tape := &tape{file} 13 | 14 | tape.Write([]byte("abc")) 15 | 16 | file.Seek(0, io.SeekStart) 17 | newFileContents, _ := io.ReadAll(file) 18 | 19 | got := string(newFileContents) 20 | want := "abc" 21 | 22 | if got != want { 23 | t.Errorf("got %q want %q", got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /json/v1/in_memory_player_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NewInMemoryPlayerStore initialises an empty player store. 4 | func NewInMemoryPlayerStore() *InMemoryPlayerStore { 5 | return &InMemoryPlayerStore{map[string]int{}} 6 | } 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct { 10 | store map[string]int 11 | } 12 | 13 | // RecordWin will record a player's win. 14 | func (i *InMemoryPlayerStore) RecordWin(name string) { 15 | i.store[name]++ 16 | } 17 | 18 | // GetPlayerScore retrieves scores for a given player. 19 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 20 | return i.store[name] 21 | } 22 | -------------------------------------------------------------------------------- /json/v1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | server := &PlayerServer{NewInMemoryPlayerStore()} 10 | log.Fatal(http.ListenAndServe(":5000", server)) 11 | } 12 | -------------------------------------------------------------------------------- /json/v1/server_integration_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestRecordingWinsAndRetrievingThem(t *testing.T) { 10 | store := NewInMemoryPlayerStore() 11 | server := PlayerServer{store} 12 | player := "Pepper" 13 | 14 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 15 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 16 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 17 | 18 | response := httptest.NewRecorder() 19 | server.ServeHTTP(response, newGetScoreRequest(player)) 20 | assertStatus(t, response.Code, http.StatusOK) 21 | 22 | assertResponseBody(t, response.Body.String(), "3") 23 | } 24 | -------------------------------------------------------------------------------- /json/v2/in_memory_player_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NewInMemoryPlayerStore initialises an empty player store. 4 | func NewInMemoryPlayerStore() *InMemoryPlayerStore { 5 | return &InMemoryPlayerStore{map[string]int{}} 6 | } 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct { 10 | store map[string]int 11 | } 12 | 13 | // RecordWin will record a player's win. 14 | func (i *InMemoryPlayerStore) RecordWin(name string) { 15 | i.store[name]++ 16 | } 17 | 18 | // GetPlayerScore retrieves scores for a given player. 19 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 20 | return i.store[name] 21 | } 22 | -------------------------------------------------------------------------------- /json/v2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | server := NewPlayerServer(NewInMemoryPlayerStore()) 10 | log.Fatal(http.ListenAndServe(":5000", server)) 11 | } 12 | -------------------------------------------------------------------------------- /json/v2/server_integration_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestRecordingWinsAndRetrievingThem(t *testing.T) { 10 | store := NewInMemoryPlayerStore() 11 | server := NewPlayerServer(store) 12 | player := "Pepper" 13 | 14 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 15 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 16 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 17 | 18 | response := httptest.NewRecorder() 19 | server.ServeHTTP(response, newGetScoreRequest(player)) 20 | assertStatus(t, response.Code, http.StatusOK) 21 | 22 | assertResponseBody(t, response.Body.String(), "3") 23 | } 24 | -------------------------------------------------------------------------------- /json/v3/in_memory_player_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NewInMemoryPlayerStore initialises an empty player store. 4 | func NewInMemoryPlayerStore() *InMemoryPlayerStore { 5 | return &InMemoryPlayerStore{map[string]int{}} 6 | } 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct { 10 | store map[string]int 11 | } 12 | 13 | // RecordWin will record a player's win. 14 | func (i *InMemoryPlayerStore) RecordWin(name string) { 15 | i.store[name]++ 16 | } 17 | 18 | // GetPlayerScore retrieves scores for a given player. 19 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 20 | return i.store[name] 21 | } 22 | -------------------------------------------------------------------------------- /json/v3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | server := NewPlayerServer(NewInMemoryPlayerStore()) 10 | log.Fatal(http.ListenAndServe(":5000", server)) 11 | } 12 | -------------------------------------------------------------------------------- /json/v3/server_integration_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestRecordingWinsAndRetrievingThem(t *testing.T) { 10 | store := NewInMemoryPlayerStore() 11 | server := NewPlayerServer(store) 12 | player := "Pepper" 13 | 14 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 15 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 16 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 17 | 18 | response := httptest.NewRecorder() 19 | server.ServeHTTP(response, newGetScoreRequest(player)) 20 | assertStatus(t, response.Code, http.StatusOK) 21 | 22 | assertResponseBody(t, response.Body.String(), "3") 23 | } 24 | -------------------------------------------------------------------------------- /json/v4/in_memory_player_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NewInMemoryPlayerStore initialises an empty player store. 4 | func NewInMemoryPlayerStore() *InMemoryPlayerStore { 5 | return &InMemoryPlayerStore{map[string]int{}} 6 | } 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct { 10 | store map[string]int 11 | } 12 | 13 | // GetLeague currently doesn't work, but it should return the player league. 14 | func (i *InMemoryPlayerStore) GetLeague() []Player { 15 | return nil 16 | } 17 | 18 | // RecordWin will record a player's win. 19 | func (i *InMemoryPlayerStore) RecordWin(name string) { 20 | i.store[name]++ 21 | } 22 | 23 | // GetPlayerScore retrieves scores for a given player. 24 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 25 | return i.store[name] 26 | } 27 | -------------------------------------------------------------------------------- /json/v4/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | server := NewPlayerServer(NewInMemoryPlayerStore()) 10 | log.Fatal(http.ListenAndServe(":5000", server)) 11 | } 12 | -------------------------------------------------------------------------------- /json/v4/server_integration_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestRecordingWinsAndRetrievingThem(t *testing.T) { 10 | store := NewInMemoryPlayerStore() 11 | server := NewPlayerServer(store) 12 | player := "Pepper" 13 | 14 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 15 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 16 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 17 | 18 | response := httptest.NewRecorder() 19 | server.ServeHTTP(response, newGetScoreRequest(player)) 20 | assertStatus(t, response.Code, http.StatusOK) 21 | 22 | assertResponseBody(t, response.Body.String(), "3") 23 | } 24 | -------------------------------------------------------------------------------- /json/v5/in_memory_player_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NewInMemoryPlayerStore initialises an empty player store. 4 | func NewInMemoryPlayerStore() *InMemoryPlayerStore { 5 | return &InMemoryPlayerStore{map[string]int{}} 6 | } 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct { 10 | store map[string]int 11 | } 12 | 13 | // GetLeague currently doesn't work, but it should return the player league. 14 | func (i *InMemoryPlayerStore) GetLeague() []Player { 15 | return nil 16 | } 17 | 18 | // RecordWin will record a player's win. 19 | func (i *InMemoryPlayerStore) RecordWin(name string) { 20 | i.store[name]++ 21 | } 22 | 23 | // GetPlayerScore retrieves scores for a given player. 24 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 25 | return i.store[name] 26 | } 27 | -------------------------------------------------------------------------------- /json/v5/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | server := NewPlayerServer(NewInMemoryPlayerStore()) 10 | log.Fatal(http.ListenAndServe(":5000", server)) 11 | } 12 | -------------------------------------------------------------------------------- /json/v5/server_integration_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestRecordingWinsAndRetrievingThem(t *testing.T) { 10 | store := NewInMemoryPlayerStore() 11 | server := NewPlayerServer(store) 12 | player := "Pepper" 13 | 14 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 15 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 16 | server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) 17 | 18 | response := httptest.NewRecorder() 19 | server.ServeHTTP(response, newGetScoreRequest(player)) 20 | assertStatus(t, response.Code, http.StatusOK) 21 | 22 | assertResponseBody(t, response.Body.String(), "3") 23 | } 24 | -------------------------------------------------------------------------------- /json/v6/in_memory_player_store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NewInMemoryPlayerStore initialises an empty player store. 4 | func NewInMemoryPlayerStore() *InMemoryPlayerStore { 5 | return &InMemoryPlayerStore{map[string]int{}} 6 | } 7 | 8 | // InMemoryPlayerStore collects data about players in memory. 9 | type InMemoryPlayerStore struct { 10 | store map[string]int 11 | } 12 | 13 | // GetLeague returns a collection of Players. 14 | func (i *InMemoryPlayerStore) GetLeague() []Player { 15 | var league []Player 16 | for name, wins := range i.store { 17 | league = append(league, Player{name, wins}) 18 | } 19 | return league 20 | } 21 | 22 | // RecordWin will record a player's win. 23 | func (i *InMemoryPlayerStore) RecordWin(name string) { 24 | i.store[name]++ 25 | } 26 | 27 | // GetPlayerScore retrieves scores for a given player. 28 | func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { 29 | return i.store[name] 30 | } 31 | -------------------------------------------------------------------------------- /json/v6/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | server := NewPlayerServer(NewInMemoryPlayerStore()) 10 | log.Fatal(http.ListenAndServe(":5000", server)) 11 | } 12 | -------------------------------------------------------------------------------- /maps/v1/dictionary.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Dictionary store definitions to words. 4 | type Dictionary map[string]string 5 | 6 | // Search find a word in the dictionary. 7 | func (d Dictionary) Search(word string) string { 8 | return d[word] 9 | } 10 | -------------------------------------------------------------------------------- /maps/v1/dictionary_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestSearch(t *testing.T) { 6 | dictionary := Dictionary{"test": "this is just a test"} 7 | 8 | got := dictionary.Search("test") 9 | want := "this is just a test" 10 | 11 | assertStrings(t, got, want) 12 | } 13 | 14 | func assertStrings(t testing.TB, got, want string) { 15 | t.Helper() 16 | 17 | if got != want { 18 | t.Errorf("got error %q want %q", got, want) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /maps/v2/dictionary.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "errors" 4 | 5 | // Dictionary store definitions to words. 6 | type Dictionary map[string]string 7 | 8 | // ErrNotFound means the definition could not be found for the given word. 9 | var ErrNotFound = errors.New("could not find the word you were looking for") 10 | 11 | // Search find a word in the dictionary. 12 | func (d Dictionary) Search(word string) (string, error) { 13 | definition, ok := d[word] 14 | if !ok { 15 | return "", ErrNotFound 16 | } 17 | 18 | return definition, nil 19 | } 20 | -------------------------------------------------------------------------------- /maps/v2/dictionary_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSearch(t *testing.T) { 8 | dictionary := Dictionary{"test": "this is just a test"} 9 | 10 | t.Run("known word", func(t *testing.T) { 11 | got, _ := dictionary.Search("test") 12 | want := "this is just a test" 13 | 14 | assertStrings(t, got, want) 15 | }) 16 | 17 | t.Run("unknown word", func(t *testing.T) { 18 | _, got := dictionary.Search("unknown") 19 | 20 | assertError(t, got, ErrNotFound) 21 | }) 22 | } 23 | 24 | func assertStrings(t testing.TB, got, want string) { 25 | t.Helper() 26 | 27 | if got != want { 28 | t.Errorf("got %q want %q", got, want) 29 | } 30 | } 31 | 32 | func assertError(t testing.TB, got, want error) { 33 | t.Helper() 34 | 35 | if got != want { 36 | t.Errorf("got error %q want %q", got, want) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /maps/v3/dictionary.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "errors" 4 | 5 | // Dictionary store definitions to words. 6 | type Dictionary map[string]string 7 | 8 | // ErrNotFound means the definition could not be found for the given word. 9 | var ErrNotFound = errors.New("could not find the word you were looking for") 10 | 11 | // Search find a word in the dictionary. 12 | func (d Dictionary) Search(word string) (string, error) { 13 | definition, ok := d[word] 14 | if !ok { 15 | return "", ErrNotFound 16 | } 17 | 18 | return definition, nil 19 | } 20 | 21 | // Add inserts a word and definition into the dictionary. 22 | func (d Dictionary) Add(word, definition string) { 23 | d[word] = definition 24 | } 25 | -------------------------------------------------------------------------------- /math/clock.template.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 16 | 17 | 19 | 20 | -------------------------------------------------------------------------------- /math/example_clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 16 | 17 | 19 | 20 | -------------------------------------------------------------------------------- /math/images/unit_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/math/images/unit_circle.png -------------------------------------------------------------------------------- /math/images/unit_circle_12_oclock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/math/images/unit_circle_12_oclock.png -------------------------------------------------------------------------------- /math/images/unit_circle_coords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/math/images/unit_circle_coords.png -------------------------------------------------------------------------------- /math/images/unit_circle_params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/math/images/unit_circle_params.png -------------------------------------------------------------------------------- /math/v1/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import "time" 4 | 5 | // A Point represents a two dimensional Cartesian coordinate. 6 | type Point struct { 7 | X float64 8 | Y float64 9 | } 10 | 11 | // SecondHand is the unit vector of the second hand of an analogue clock at time `t`. 12 | // represented as a Point. 13 | func SecondHand(t time.Time) Point { 14 | return Point{150, 60} 15 | } 16 | -------------------------------------------------------------------------------- /math/v1/clockface/clockface_test.go: -------------------------------------------------------------------------------- 1 | package clockface_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v1/clockface" 8 | ) 9 | 10 | func TestSecondHandAtMidnight(t *testing.T) { 11 | tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC) 12 | 13 | want := clockface.Point{X: 150, Y: 150 - 90} 14 | got := clockface.SecondHand(tm) 15 | 16 | if got != want { 17 | t.Errorf("Got %v, wanted %v", got, want) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /math/v10/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // A Point represents a two dimensional Cartesian coordinate. 9 | type Point struct { 10 | X float64 11 | Y float64 12 | } 13 | 14 | func secondsInRadians(t time.Time) float64 { 15 | return (math.Pi / (30 / float64(t.Second()))) 16 | } 17 | 18 | func secondHandPoint(t time.Time) Point { 19 | return angleToPoint(secondsInRadians(t)) 20 | } 21 | 22 | func minutesInRadians(t time.Time) float64 { 23 | return (secondsInRadians(t) / 60) + 24 | (math.Pi / (30 / float64(t.Minute()))) 25 | } 26 | 27 | func minuteHandPoint(t time.Time) Point { 28 | return angleToPoint(minutesInRadians(t)) 29 | } 30 | 31 | func hoursInRadians(t time.Time) float64 { 32 | return (minutesInRadians(t) / 12) + 33 | (math.Pi / (6 / float64(t.Hour()%12))) 34 | } 35 | 36 | func angleToPoint(angle float64) Point { 37 | x := math.Sin(angle) 38 | y := math.Cos(angle) 39 | 40 | return Point{x, y} 41 | } 42 | -------------------------------------------------------------------------------- /math/v10/clockface/clockface/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /math/v10/clockface/clockface/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v10/clockface" 8 | ) 9 | 10 | func main() { 11 | t := time.Now() 12 | clockface.SVGWriter(os.Stdout, t) 13 | } 14 | -------------------------------------------------------------------------------- /math/v11/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // A Point represents a two dimensional Cartesian coordinate. 9 | type Point struct { 10 | X float64 11 | Y float64 12 | } 13 | 14 | func secondsInRadians(t time.Time) float64 { 15 | return (math.Pi / (30 / float64(t.Second()))) 16 | } 17 | 18 | func secondHandPoint(t time.Time) Point { 19 | return angleToPoint(secondsInRadians(t)) 20 | } 21 | 22 | func minutesInRadians(t time.Time) float64 { 23 | return (secondsInRadians(t) / 60) + 24 | (math.Pi / (30 / float64(t.Minute()))) 25 | } 26 | 27 | func minuteHandPoint(t time.Time) Point { 28 | return angleToPoint(minutesInRadians(t)) 29 | } 30 | 31 | func hoursInRadians(t time.Time) float64 { 32 | return (minutesInRadians(t) / 12) + 33 | (math.Pi / (6 / float64(t.Hour()%12))) 34 | } 35 | 36 | func hourHandPoint(t time.Time) Point { 37 | return angleToPoint(hoursInRadians(t)) 38 | } 39 | 40 | func angleToPoint(angle float64) Point { 41 | x := math.Sin(angle) 42 | y := math.Cos(angle) 43 | 44 | return Point{x, y} 45 | } 46 | -------------------------------------------------------------------------------- /math/v11/clockface/clockface/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /math/v11/clockface/clockface/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v9/clockface" 8 | ) 9 | 10 | func main() { 11 | t := time.Now() 12 | clockface.SVGWriter(os.Stdout, t) 13 | } 14 | -------------------------------------------------------------------------------- /math/v12/clockface/clockface/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /math/v12/clockface/clockface/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v12/clockface" 8 | ) 9 | 10 | func main() { 11 | t := time.Now() 12 | clockface.SVGWriter(os.Stdout, t) 13 | } 14 | -------------------------------------------------------------------------------- /math/v2/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // A Point represents a two dimensional Cartesian coordinate. 9 | type Point struct { 10 | X float64 11 | Y float64 12 | } 13 | 14 | // SecondHand is the unit vector of the second hand of an analogue clock at time `t`. 15 | // represented as a Point. 16 | func SecondHand(t time.Time) Point { 17 | return Point{150, 60} 18 | } 19 | 20 | func secondsInRadians(t time.Time) float64 { 21 | return math.Pi 22 | } 23 | -------------------------------------------------------------------------------- /math/v2/clockface/clockface_acceptance_test.go: -------------------------------------------------------------------------------- 1 | package clockface_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v1/clockface" 8 | ) 9 | 10 | func TestSecondHandAtMidnight(t *testing.T) { 11 | tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC) 12 | 13 | want := clockface.Point{X: 150, Y: 150 - 90} 14 | got := clockface.SecondHand(tm) 15 | 16 | if got != want { 17 | t.Errorf("Got %v, wanted %v", got, want) 18 | } 19 | } 20 | 21 | // func TestSecondHandAt30Seconds(t *testing.T) { 22 | // tm := time.Date(1337, time.January, 1, 0, 0, 30, 0, time.UTC) 23 | 24 | // want := clockface.Point{X: 150, Y: 150 + 90} 25 | // got := clockface.SecondHand(tm) 26 | 27 | // if got != want { 28 | // t.Errorf("Got %v, wanted %v", got, want) 29 | // } 30 | // } 31 | -------------------------------------------------------------------------------- /math/v2/clockface/clockface_test.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestSecondsInRadians(t *testing.T) { 10 | thirtySeconds := time.Date(312, time.October, 28, 0, 0, 30, 0, time.UTC) 11 | want := math.Pi 12 | got := secondsInRadians(thirtySeconds) 13 | 14 | if want != got { 15 | t.Fatalf("Wanted %v radians, but got %v", want, got) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /math/v3/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // A Point represents a two dimensional Cartesian coordinate. 9 | type Point struct { 10 | X float64 11 | Y float64 12 | } 13 | 14 | // SecondHand is the unit vector of the second hand of an analogue clock at time `t`. 15 | // represented as a Point. 16 | func SecondHand(t time.Time) Point { 17 | return Point{150, 60} 18 | } 19 | 20 | func secondsInRadians(t time.Time) float64 { 21 | return (math.Pi / (30 / (float64(t.Second())))) 22 | } 23 | -------------------------------------------------------------------------------- /math/v3/clockface/clockface_acceptance_test.go: -------------------------------------------------------------------------------- 1 | package clockface_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v1/clockface" 8 | ) 9 | 10 | func TestSecondHandAtMidnight(t *testing.T) { 11 | tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC) 12 | 13 | want := clockface.Point{X: 150, Y: 150 - 90} 14 | got := clockface.SecondHand(tm) 15 | 16 | if got != want { 17 | t.Errorf("Got %v, wanted %v", got, want) 18 | } 19 | } 20 | 21 | // func TestSecondHandAt30Seconds(t *testing.T) { 22 | // tm := time.Date(1337, time.January, 1, 0, 0, 30, 0, time.UTC) 23 | 24 | // want := clockface.Point{X: 150, Y: 150 + 90} 25 | // got := clockface.SecondHand(tm) 26 | 27 | // if got != want { 28 | // t.Errorf("Got %v, wanted %v", got, want) 29 | // } 30 | // } 31 | -------------------------------------------------------------------------------- /math/v3/clockface/clockface_test.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestSecondsInRadians(t *testing.T) { 10 | cases := []struct { 11 | time time.Time 12 | angle float64 13 | }{ 14 | {simpleTime(0, 0, 30), math.Pi}, 15 | {simpleTime(0, 0, 0), 0}, 16 | {simpleTime(0, 0, 45), (math.Pi / 2) * 3}, 17 | {simpleTime(0, 0, 7), (math.Pi / 30) * 7}, 18 | } 19 | 20 | for _, c := range cases { 21 | t.Run(testName(c.time), func(t *testing.T) { 22 | got := secondsInRadians(c.time) 23 | if got != c.angle { 24 | t.Fatalf("Wanted %v radians, but got %v", c.angle, got) 25 | } 26 | }) 27 | } 28 | } 29 | 30 | func simpleTime(hours, minutes, seconds int) time.Time { 31 | return time.Date(312, time.October, 28, hours, minutes, seconds, 0, time.UTC) 32 | } 33 | 34 | func testName(t time.Time) string { 35 | return t.Format("15:04:05") 36 | } 37 | -------------------------------------------------------------------------------- /math/v4/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // A Point represents a two dimensional Cartesian coordinate. 9 | type Point struct { 10 | X float64 11 | Y float64 12 | } 13 | 14 | // SecondHand is the unit vector of the second hand of an analogue clock at time `t`. 15 | // represented as a Point. 16 | func SecondHand(t time.Time) Point { 17 | return Point{150, 60} 18 | } 19 | 20 | func secondsInRadians(t time.Time) float64 { 21 | return (math.Pi / (30 / (float64(t.Second())))) 22 | } 23 | 24 | func secondHandPoint(t time.Time) Point { 25 | angle := secondsInRadians(t) 26 | x := math.Sin(angle) 27 | y := math.Cos(angle) 28 | 29 | return Point{x, y} 30 | } 31 | -------------------------------------------------------------------------------- /math/v4/clockface/clockface_acceptance_test.go: -------------------------------------------------------------------------------- 1 | package clockface_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v1/clockface" 8 | ) 9 | 10 | func TestSecondHandAtMidnight(t *testing.T) { 11 | tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC) 12 | 13 | want := clockface.Point{X: 150, Y: 150 - 90} 14 | got := clockface.SecondHand(tm) 15 | 16 | if got != want { 17 | t.Errorf("Got %v, wanted %v", got, want) 18 | } 19 | } 20 | 21 | // func TestSecondHandAt30Seconds(t *testing.T) { 22 | // tm := time.Date(1337, time.January, 1, 0, 0, 30, 0, time.UTC) 23 | 24 | // want := clockface.Point{X: 150, Y: 150 + 90} 25 | // got := clockface.SecondHand(tm) 26 | 27 | // if got != want { 28 | // t.Errorf("Got %v, wanted %v", got, want) 29 | // } 30 | // } 31 | -------------------------------------------------------------------------------- /math/v5/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // A Point represents a two dimensional Cartesian coordinate. 9 | type Point struct { 10 | X float64 11 | Y float64 12 | } 13 | 14 | const secondHandLength = 90 15 | const clockCentreX = 150 16 | const clockCentreY = 150 17 | 18 | // SecondHand is the unit vector of the second hand of an analogue clock at time `t`. 19 | // represented as a Point. 20 | func SecondHand(t time.Time) Point { 21 | p := secondHandPoint(t) 22 | p = Point{p.X * secondHandLength, p.Y * secondHandLength} 23 | p = Point{p.X, -p.Y} 24 | p = Point{p.X + clockCentreX, p.Y + clockCentreY} //translate 25 | return p 26 | } 27 | 28 | func secondsInRadians(t time.Time) float64 { 29 | return (math.Pi / (30 / (float64(t.Second())))) 30 | } 31 | 32 | func secondHandPoint(t time.Time) Point { 33 | angle := secondsInRadians(t) 34 | x := math.Sin(angle) 35 | y := math.Cos(angle) 36 | 37 | return Point{x, y} 38 | } 39 | -------------------------------------------------------------------------------- /math/v5/clockface/clockface_acceptance_test.go: -------------------------------------------------------------------------------- 1 | package clockface_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v5/clockface" 8 | ) 9 | 10 | func TestSecondHandAtMidnight(t *testing.T) { 11 | tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC) 12 | 13 | want := clockface.Point{X: 150, Y: 150 - 90} 14 | got := clockface.SecondHand(tm) 15 | 16 | if got != want { 17 | t.Errorf("Got %v, wanted %v", got, want) 18 | } 19 | } 20 | 21 | func TestSecondHandAt30Seconds(t *testing.T) { 22 | tm := time.Date(1337, time.January, 1, 0, 0, 30, 0, time.UTC) 23 | 24 | want := clockface.Point{X: 150, Y: 150 + 90} 25 | got := clockface.SecondHand(tm) 26 | 27 | if got != want { 28 | t.Errorf("Got %v, wanted %v", got, want) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /math/v6/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // A Point represents a two dimensional Cartesian coordinate. 9 | type Point struct { 10 | X float64 11 | Y float64 12 | } 13 | 14 | const secondHandLength = 90 15 | const clockCentreX = 150 16 | const clockCentreY = 150 17 | 18 | // SecondHand is the unit vector of the second hand of an analogue clock at time `t`. 19 | // represented as a Point. 20 | func SecondHand(t time.Time) Point { 21 | p := secondHandPoint(t) 22 | p = Point{p.X * secondHandLength, p.Y * secondHandLength} 23 | p = Point{p.X, -p.Y} 24 | p = Point{p.X + clockCentreX, p.Y + clockCentreY} //translate 25 | return p 26 | } 27 | 28 | func secondsInRadians(t time.Time) float64 { 29 | return (math.Pi / (30 / (float64(t.Second())))) 30 | } 31 | 32 | func secondHandPoint(t time.Time) Point { 33 | angle := secondsInRadians(t) 34 | x := math.Sin(angle) 35 | y := math.Cos(angle) 36 | 37 | return Point{x, y} 38 | } 39 | -------------------------------------------------------------------------------- /math/v6/clockface/clockface/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /math/v6/clockface/clockface_acceptance_test.go: -------------------------------------------------------------------------------- 1 | package clockface_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v6/clockface" 8 | ) 9 | 10 | func TestSecondHandAtMidnight(t *testing.T) { 11 | tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC) 12 | 13 | want := clockface.Point{X: 150, Y: 150 - 90} 14 | got := clockface.SecondHand(tm) 15 | 16 | if got != want { 17 | t.Errorf("Got %v, wanted %v", got, want) 18 | } 19 | } 20 | 21 | func TestSecondHandAt30Seconds(t *testing.T) { 22 | tm := time.Date(1337, time.January, 1, 0, 0, 30, 0, time.UTC) 23 | 24 | want := clockface.Point{X: 150, Y: 150 + 90} 25 | got := clockface.SecondHand(tm) 26 | 27 | if got != want { 28 | t.Errorf("Got %v, wanted %v", got, want) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /math/v7/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // A Point represents a two dimensional Cartesian coordinate. 9 | type Point struct { 10 | X float64 11 | Y float64 12 | } 13 | 14 | func secondsInRadians(t time.Time) float64 { 15 | return (math.Pi / (30 / (float64(t.Second())))) 16 | } 17 | 18 | func secondHandPoint(t time.Time) Point { 19 | angle := secondsInRadians(t) 20 | x := math.Sin(angle) 21 | y := math.Cos(angle) 22 | 23 | return Point{x, y} 24 | } 25 | -------------------------------------------------------------------------------- /math/v7/clockface/clockface/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /math/v7/clockface/clockface/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v7/clockface" 8 | ) 9 | 10 | func main() { 11 | t := time.Now() 12 | clockface.SVGWriter(os.Stdout, t) 13 | } 14 | -------------------------------------------------------------------------------- /math/v7/clockface/clockface_acceptance_test.go: -------------------------------------------------------------------------------- 1 | package clockface_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | 8 | "github.com/quii/learn-go-with-tests/math/v7/clockface" 9 | ) 10 | 11 | func TestSVGWriterAtMidnight(t *testing.T) { 12 | tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC) 13 | 14 | var b strings.Builder 15 | clockface.SVGWriter(&b, tm) 16 | got := b.String() 17 | 18 | want := ` 2 | 3 | -------------------------------------------------------------------------------- /math/v7b/clockface/clockface/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v7/clockface" 8 | ) 9 | 10 | func main() { 11 | t := time.Now() 12 | clockface.SVGWriter(os.Stdout, t) 13 | } 14 | -------------------------------------------------------------------------------- /math/v7c/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // A Point represents a two dimensional Cartesian coordinate. 9 | type Point struct { 10 | X float64 11 | Y float64 12 | } 13 | 14 | func secondsInRadians(t time.Time) float64 { 15 | return (math.Pi / (30 / (float64(t.Second())))) 16 | } 17 | 18 | func secondHandPoint(t time.Time) Point { 19 | angle := secondsInRadians(t) 20 | x := math.Sin(angle) 21 | y := math.Cos(angle) 22 | 23 | return Point{x, y} 24 | } 25 | -------------------------------------------------------------------------------- /math/v7c/clockface/clockface/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /math/v7c/clockface/clockface/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v7/clockface" 8 | ) 9 | 10 | func main() { 11 | t := time.Now() 12 | clockface.SVGWriter(os.Stdout, t) 13 | } 14 | -------------------------------------------------------------------------------- /math/v8/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // A Point represents a two dimensional Cartesian coordinate. 9 | type Point struct { 10 | X float64 11 | Y float64 12 | } 13 | 14 | func secondsInRadians(t time.Time) float64 { 15 | return (math.Pi / (30 / float64(t.Second()))) 16 | } 17 | 18 | func secondHandPoint(t time.Time) Point { 19 | angle := secondsInRadians(t) 20 | x := math.Sin(angle) 21 | y := math.Cos(angle) 22 | 23 | return Point{x, y} 24 | } 25 | 26 | func minutesInRadians(t time.Time) float64 { 27 | return (secondsInRadians(t) / 60) + 28 | (math.Pi / (30 / float64(t.Minute()))) 29 | } 30 | -------------------------------------------------------------------------------- /math/v8/clockface/clockface/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /math/v8/clockface/clockface/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v7/clockface" 8 | ) 9 | 10 | func main() { 11 | t := time.Now() 12 | clockface.SVGWriter(os.Stdout, t) 13 | } 14 | -------------------------------------------------------------------------------- /math/v9/clockface/clockface.go: -------------------------------------------------------------------------------- 1 | package clockface 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // A Point represents a two dimensional Cartesian coordinate. 9 | type Point struct { 10 | X float64 11 | Y float64 12 | } 13 | 14 | func secondsInRadians(t time.Time) float64 { 15 | return (math.Pi / (30 / float64(t.Second()))) 16 | } 17 | 18 | func secondHandPoint(t time.Time) Point { 19 | return angleToPoint(secondsInRadians(t)) 20 | } 21 | 22 | func minutesInRadians(t time.Time) float64 { 23 | return (secondsInRadians(t) / 60) + 24 | (math.Pi / (30 / float64(t.Minute()))) 25 | } 26 | 27 | func minuteHandPoint(t time.Time) Point { 28 | return angleToPoint(minutesInRadians(t)) 29 | } 30 | 31 | func angleToPoint(angle float64) Point { 32 | x := math.Sin(angle) 33 | y := math.Cos(angle) 34 | 35 | return Point{x, y} 36 | } 37 | -------------------------------------------------------------------------------- /math/v9/clockface/clockface/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /math/v9/clockface/clockface/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/quii/learn-go-with-tests/math/v9/clockface" 8 | ) 9 | 10 | func main() { 11 | t := time.Now() 12 | clockface.SVGWriter(os.Stdout, t) 13 | } 14 | -------------------------------------------------------------------------------- /math/vFinal/clockface/clockface/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /math/vFinal/clockface/clockface/main.go: -------------------------------------------------------------------------------- 1 | // Writes an SVG clockface of the current time to Stdout. 2 | package main 3 | 4 | import ( 5 | "os" 6 | "time" 7 | 8 | "github.com/quii/learn-go-with-tests/math/vFinal/clockface/svg" 9 | ) 10 | 11 | func main() { 12 | t := time.Now() 13 | svg.Write(os.Stdout, t) 14 | } 15 | -------------------------------------------------------------------------------- /meta.tmpl.tex: -------------------------------------------------------------------------------- 1 | \usepackage{fancyhdr} 2 | 3 | \usepackage{xeCJK} 4 | \usepackage{fontspec}% set Chinese fonts, as follows 5 | \setmainfont{DejaVu Sans} 6 | \setmonofont{DejaVu Sans} 7 | \setsansfont{DejaVu Sans} 8 | \setCJKmainfont[BoldFont=Noto Sans CJK TC,ItalicFont=Noto Sans CJK TC]{Noto Sans CJK TC} 9 | \setCJKsansfont[BoldFont=Noto Sans CJK TC]{Noto Sans CJK TC} 10 | \setCJKmonofont{Noto Sans Mono CJK TC} 11 | 12 | \pagestyle{fancy} 13 | \fancyhf{} 14 | \cfoot{%%FOOTER_VERSION%%} 15 | -------------------------------------------------------------------------------- /mocking/v1/countdown_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestCountdown(t *testing.T) { 9 | buffer := &bytes.Buffer{} 10 | 11 | Countdown(buffer) 12 | 13 | got := buffer.String() 14 | want := "3" 15 | 16 | if got != want { 17 | t.Errorf("got %q want %q", got, want) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mocking/v1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // Countdown prints a countdown from 3 to out. 10 | func Countdown(out io.Writer) { 11 | fmt.Fprint(out, "3") 12 | } 13 | 14 | func main() { 15 | Countdown(os.Stdout) 16 | } 17 | -------------------------------------------------------------------------------- /mocking/v2/countdown_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestCountdown(t *testing.T) { 9 | buffer := &bytes.Buffer{} 10 | 11 | Countdown(buffer) 12 | 13 | got := buffer.String() 14 | want := `3 15 | 2 16 | 1 17 | Go!` 18 | 19 | if got != want { 20 | t.Errorf("got %q want %q", got, want) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /mocking/v2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | const finalWord = "Go!" 10 | const countdownStart = 3 11 | 12 | // Countdown prints a countdown from 3 to out. 13 | func Countdown(out io.Writer) { 14 | for i := countdownStart; i > 0; i-- { 15 | fmt.Fprintln(out, i) 16 | } 17 | fmt.Fprint(out, finalWord) 18 | } 19 | 20 | func main() { 21 | Countdown(os.Stdout) 22 | } 23 | -------------------------------------------------------------------------------- /mocking/v3/countdown_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestCountdown(t *testing.T) { 9 | buffer := &bytes.Buffer{} 10 | spySleeper := &SpySleeper{} 11 | 12 | Countdown(buffer, spySleeper) 13 | 14 | got := buffer.String() 15 | want := `3 16 | 2 17 | 1 18 | Go!` 19 | 20 | if got != want { 21 | t.Errorf("got %q want %q", got, want) 22 | } 23 | 24 | if spySleeper.Calls != 3 { 25 | t.Errorf("not enough calls to sleeper, want 3 got %d", spySleeper.Calls) 26 | } 27 | } 28 | 29 | type SpySleeper struct { 30 | Calls int 31 | } 32 | 33 | func (s *SpySleeper) Sleep() { 34 | s.Calls++ 35 | } 36 | -------------------------------------------------------------------------------- /mocking/v3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "time" 8 | ) 9 | 10 | // Sleeper allows you to put delays. 11 | type Sleeper interface { 12 | Sleep() 13 | } 14 | 15 | // DefaultSleeper is an implementation of Sleeper with a predefined delay. 16 | type DefaultSleeper struct{} 17 | 18 | // Sleep will pause execution for the defined Duration. 19 | func (d *DefaultSleeper) Sleep() { 20 | time.Sleep(1 * time.Second) 21 | } 22 | 23 | const finalWord = "Go!" 24 | const countdownStart = 3 25 | 26 | // Countdown prints a countdown from 3 to out with a delay between count provided by Sleeper. 27 | func Countdown(out io.Writer, sleeper Sleeper) { 28 | for i := countdownStart; i > 0; i-- { 29 | fmt.Fprintln(out, i) 30 | sleeper.Sleep() 31 | } 32 | 33 | fmt.Fprint(out, finalWord) 34 | } 35 | 36 | func main() { 37 | sleeper := &DefaultSleeper{} 38 | Countdown(os.Stdout, sleeper) 39 | } 40 | -------------------------------------------------------------------------------- /mocking/v4/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "time" 8 | ) 9 | 10 | // Sleeper allows you to put delays. 11 | type Sleeper interface { 12 | Sleep() 13 | } 14 | 15 | // DefaultSleeper is an implementation of Sleeper with a predefined delay. 16 | type DefaultSleeper struct{} 17 | 18 | // Sleep will pause execution for the defined Duration. 19 | func (d *DefaultSleeper) Sleep() { 20 | time.Sleep(1 * time.Second) 21 | } 22 | 23 | const finalWord = "Go!" 24 | const countdownStart = 3 25 | 26 | // Countdown prints a countdown from 3 to out with a delay between count provided by Sleeper. 27 | func Countdown(out io.Writer, sleeper Sleeper) { 28 | 29 | for i := countdownStart; i > 0; i-- { 30 | fmt.Fprintln(out, i) 31 | sleeper.Sleep() 32 | } 33 | 34 | fmt.Fprint(out, finalWord) 35 | } 36 | 37 | func main() { 38 | sleeper := &DefaultSleeper{} 39 | Countdown(os.Stdout, sleeper) 40 | } 41 | -------------------------------------------------------------------------------- /mocking/v5/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "time" 8 | ) 9 | 10 | // Sleeper allows you to put delays. 11 | type Sleeper interface { 12 | Sleep() 13 | } 14 | 15 | // ConfigurableSleeper is an implementation of Sleeper with a defined delay. 16 | type ConfigurableSleeper struct { 17 | duration time.Duration 18 | sleep func(time.Duration) 19 | } 20 | 21 | // Sleep will pause execution for the defined Duration. 22 | func (c *ConfigurableSleeper) Sleep() { 23 | c.sleep(c.duration) 24 | } 25 | 26 | const finalWord = "Go!" 27 | const countdownStart = 3 28 | 29 | // Countdown prints a countdown from 3 to out with a delay between count provided by Sleeper. 30 | func Countdown(out io.Writer, sleeper Sleeper) { 31 | 32 | for i := countdownStart; i > 0; i-- { 33 | fmt.Fprintln(out, i) 34 | sleeper.Sleep() 35 | } 36 | 37 | fmt.Fprint(out, finalWord) 38 | } 39 | 40 | func main() { 41 | sleeper := &ConfigurableSleeper{1 * time.Second, time.Sleep} 42 | Countdown(os.Stdout, sleeper) 43 | } 44 | -------------------------------------------------------------------------------- /pdf-cover.md: -------------------------------------------------------------------------------- 1 | ![](epub-cover.png) 2 | -------------------------------------------------------------------------------- /pdf-cover.tex: -------------------------------------------------------------------------------- 1 | \includegraphics{epub-cover.png} 2 | \thispagestyle{empty} 3 | -------------------------------------------------------------------------------- /pointers/v1/wallet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | // Bitcoin represents a number of Bitcoins. 6 | type Bitcoin int 7 | 8 | func (b Bitcoin) String() string { 9 | return fmt.Sprintf("%d BTC", b) 10 | } 11 | 12 | // Wallet stores the number of Bitcoin someone owns. 13 | type Wallet struct { 14 | balance Bitcoin 15 | } 16 | 17 | // Deposit will add some Bitcoin to a wallet. 18 | func (w *Wallet) Deposit(amount Bitcoin) { 19 | w.balance += amount 20 | } 21 | 22 | // Balance returns the number of Bitcoin a wallet has. 23 | func (w *Wallet) Balance() Bitcoin { 24 | return w.balance 25 | } 26 | -------------------------------------------------------------------------------- /pointers/v1/wallet_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWallet(t *testing.T) { 8 | 9 | wallet := Wallet{} 10 | 11 | wallet.Deposit(Bitcoin(10)) 12 | 13 | got := wallet.Balance() 14 | 15 | want := Bitcoin(10) 16 | 17 | if got != want { 18 | t.Errorf("got %s want %s", got, want) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pointers/v2/wallet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | // Bitcoin represents a number of Bitcoins. 6 | type Bitcoin int 7 | 8 | func (b Bitcoin) String() string { 9 | return fmt.Sprintf("%d BTC", b) 10 | } 11 | 12 | // Wallet stores the number of Bitcoin someone owns. 13 | type Wallet struct { 14 | balance Bitcoin 15 | } 16 | 17 | // Deposit will add some Bitcoin to a wallet. 18 | func (w *Wallet) Deposit(amount Bitcoin) { 19 | w.balance += amount 20 | } 21 | 22 | // Withdraw subtracts some Bitcoin from the wallet. 23 | func (w *Wallet) Withdraw(amount Bitcoin) { 24 | w.balance -= amount 25 | } 26 | 27 | // Balance returns the number of Bitcoin a wallet has. 28 | func (w *Wallet) Balance() Bitcoin { 29 | return w.balance 30 | } 31 | -------------------------------------------------------------------------------- /pointers/v2/wallet_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWallet(t *testing.T) { 8 | 9 | assertBalance := func(t testing.TB, wallet Wallet, want Bitcoin) { 10 | t.Helper() 11 | got := wallet.Balance() 12 | 13 | if got != want { 14 | t.Errorf("got %s want %s", got, want) 15 | } 16 | } 17 | 18 | t.Run("deposit", func(t *testing.T) { 19 | wallet := Wallet{} 20 | wallet.Deposit(Bitcoin(10)) 21 | assertBalance(t, wallet, Bitcoin(10)) 22 | }) 23 | 24 | t.Run("withdraw", func(t *testing.T) { 25 | wallet := Wallet{balance: Bitcoin(20)} 26 | wallet.Withdraw(10) 27 | assertBalance(t, wallet, Bitcoin(10)) 28 | }) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /pointers/v3/wallet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // Bitcoin represents a number of Bitcoins. 9 | type Bitcoin int 10 | 11 | func (b Bitcoin) String() string { 12 | return fmt.Sprintf("%d BTC", b) 13 | } 14 | 15 | // Wallet stores the number of Bitcoin someone owns. 16 | type Wallet struct { 17 | balance Bitcoin 18 | } 19 | 20 | // Deposit will add some Bitcoin to a wallet. 21 | func (w *Wallet) Deposit(amount Bitcoin) { 22 | w.balance += amount 23 | } 24 | 25 | // Withdraw subtracts some Bitcoin from the wallet, returning an error if it cannot be performed. 26 | func (w *Wallet) Withdraw(amount Bitcoin) error { 27 | 28 | if amount > w.balance { 29 | return errors.New("oh no") 30 | } 31 | 32 | w.balance -= amount 33 | return nil 34 | } 35 | 36 | // Balance returns the number of Bitcoin a wallet has. 37 | func (w *Wallet) Balance() Bitcoin { 38 | return w.balance 39 | } 40 | -------------------------------------------------------------------------------- /q-and-a/context-aware-reader/context_aware_reader.go: -------------------------------------------------------------------------------- 1 | package cancelreader 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // NewCancellableReader will stop reading to rdr if ctx is cancelled. 9 | func NewCancellableReader(ctx context.Context, rdr io.Reader) io.Reader { 10 | return &readerCtx{ 11 | ctx: ctx, 12 | delegate: rdr, 13 | } 14 | } 15 | 16 | type readerCtx struct { 17 | ctx context.Context 18 | delegate io.Reader 19 | } 20 | 21 | func (r *readerCtx) Read(p []byte) (n int, err error) { 22 | if err := r.ctx.Err(); err != nil { 23 | return 0, err 24 | } 25 | return r.delegate.Read(p) 26 | } 27 | -------------------------------------------------------------------------------- /q-and-a/http-handlers-revisited/basic_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func Teapot(res http.ResponseWriter, req *http.Request) { 10 | res.WriteHeader(http.StatusTeapot) 11 | } 12 | 13 | func TestTeapotHandler(t *testing.T) { 14 | req := httptest.NewRequest(http.MethodGet, "/", nil) 15 | res := httptest.NewRecorder() 16 | 17 | Teapot(res, req) 18 | 19 | if res.Code != http.StatusTeapot { 20 | t.Errorf("got status %d but wanted %d", res.Code, http.StatusTeapot) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /q-and-a/os-exec/msg.xml: -------------------------------------------------------------------------------- 1 | 2 | Happy New Year! 3 | 4 | -------------------------------------------------------------------------------- /reading-files/blogposts.go: -------------------------------------------------------------------------------- 1 | package blogposts 2 | 3 | import ( 4 | "io/fs" 5 | ) 6 | 7 | // NewPostsFromFS returns a collection of blog posts from a file system. If it does not conform to the format then it'll return an error 8 | func NewPostsFromFS(fileSystem fs.FS) ([]Post, error) { 9 | dir, err := fs.ReadDir(fileSystem, ".") 10 | if err != nil { 11 | return nil, err 12 | } 13 | var posts []Post 14 | for _, f := range dir { 15 | post, err := getPost(fileSystem, f) 16 | if err != nil { 17 | return nil, err //todo: needs clarification, should we totally fail if one file fails? or just ignore? 18 | } 19 | posts = append(posts, post) 20 | } 21 | return posts, nil 22 | } 23 | 24 | func getPost(fileSystem fs.FS, f fs.DirEntry) (Post, error) { 25 | postFile, err := fileSystem.Open(f.Name()) 26 | if err != nil { 27 | return Post{}, err 28 | } 29 | defer postFile.Close() 30 | 31 | return newPost(postFile) 32 | } 33 | -------------------------------------------------------------------------------- /red-green-blue-gophers-smaller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/red-green-blue-gophers-smaller.png -------------------------------------------------------------------------------- /red-green-blue-gophers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quii/learn-go-with-tests/9980f1332d7ff3b6a047cf67658451107c4550a8/red-green-blue-gophers.png -------------------------------------------------------------------------------- /reflection/v1/reflection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "reflect" 4 | 5 | func walk(x interface{}, fn func(input string)) { 6 | val := reflect.ValueOf(x) 7 | field := val.Field(0) 8 | fn(field.String()) 9 | } 10 | -------------------------------------------------------------------------------- /reflection/v1/reflection_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestWalk(t *testing.T) { 9 | 10 | cases := []struct { 11 | Name string 12 | Input interface{} 13 | ExpectedCalls []string 14 | }{ 15 | { 16 | "struct with one string field", 17 | struct { 18 | Name string 19 | }{"Chris"}, 20 | []string{"Chris"}, 21 | }, 22 | } 23 | 24 | for _, test := range cases { 25 | t.Run(test.Name, func(t *testing.T) { 26 | var got []string 27 | walk(test.Input, func(input string) { 28 | got = append(got, input) 29 | }) 30 | 31 | if !reflect.DeepEqual(got, test.ExpectedCalls) { 32 | t.Errorf("got %v, want %v", got, test.ExpectedCalls) 33 | } 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /reflection/v2/reflection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "reflect" 4 | 5 | func walk(x interface{}, fn func(input string)) { 6 | val := reflect.ValueOf(x) 7 | 8 | for i := 0; i < val.NumField(); i++ { 9 | field := val.Field(i) 10 | fn(field.String()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /reflection/v2/reflection_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestWalk(t *testing.T) { 9 | 10 | cases := []struct { 11 | Name string 12 | Input interface{} 13 | ExpectedCalls []string 14 | }{ 15 | { 16 | "struct with one string field", 17 | struct { 18 | Name string 19 | }{"Chris"}, 20 | []string{"Chris"}, 21 | }, 22 | { 23 | "struct with two string fields", 24 | struct { 25 | Name string 26 | City string 27 | }{"Chris", "London"}, 28 | []string{"Chris", "London"}, 29 | }, 30 | } 31 | 32 | for _, test := range cases { 33 | t.Run(test.Name, func(t *testing.T) { 34 | var got []string 35 | walk(test.Input, func(input string) { 36 | got = append(got, input) 37 | }) 38 | 39 | if !reflect.DeepEqual(got, test.ExpectedCalls) { 40 | t.Errorf("got %v, want %v", got, test.ExpectedCalls) 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /reflection/v3/reflection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func walk(x interface{}, fn func(input string)) { 8 | val := reflect.ValueOf(x) 9 | 10 | for i := 0; i < val.NumField(); i++ { 11 | field := val.Field(i) 12 | 13 | if field.Kind() == reflect.String { 14 | fn(field.String()) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /reflection/v4/reflection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func walk(x interface{}, fn func(input string)) { 8 | val := reflect.ValueOf(x) 9 | 10 | for i := 0; i < val.NumField(); i++ { 11 | field := val.Field(i) 12 | 13 | switch field.Kind() { 14 | case reflect.String: 15 | fn(field.String()) 16 | case reflect.Struct: 17 | walk(field.Interface(), fn) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /reflection/v5/reflection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func walk(x interface{}, fn func(input string)) { 8 | val := getValue(x) 9 | 10 | for i := 0; i < val.NumField(); i++ { 11 | field := val.Field(i) 12 | 13 | switch field.Kind() { 14 | case reflect.String: 15 | fn(field.String()) 16 | case reflect.Struct: 17 | walk(field.Interface(), fn) 18 | } 19 | } 20 | } 21 | 22 | func getValue(x interface{}) reflect.Value { 23 | val := reflect.ValueOf(x) 24 | 25 | if val.Kind() == reflect.Ptr { 26 | val = val.Elem() 27 | } 28 | 29 | return val 30 | } 31 | -------------------------------------------------------------------------------- /reflection/v6/reflection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func walk(x interface{}, fn func(input string)) { 8 | val := getValue(x) 9 | 10 | numberOfValues := 0 11 | var getField func(int) reflect.Value 12 | 13 | switch val.Kind() { 14 | case reflect.String: 15 | fn(val.String()) 16 | case reflect.Struct: 17 | numberOfValues = val.NumField() 18 | getField = val.Field 19 | case reflect.Slice: 20 | numberOfValues = val.Len() 21 | getField = val.Index 22 | } 23 | 24 | for i := 0; i < numberOfValues; i++ { 25 | walk(getField(i).Interface(), fn) 26 | } 27 | } 28 | 29 | func getValue(x interface{}) reflect.Value { 30 | val := reflect.ValueOf(x) 31 | 32 | if val.Kind() == reflect.Ptr { 33 | val = val.Elem() 34 | } 35 | 36 | return val 37 | } 38 | -------------------------------------------------------------------------------- /reflection/v7/reflection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func walk(x interface{}, fn func(input string)) { 8 | val := getValue(x) 9 | 10 | numberOfValues := 0 11 | var getField func(int) reflect.Value 12 | 13 | switch val.Kind() { 14 | case reflect.String: 15 | fn(val.String()) 16 | case reflect.Struct: 17 | numberOfValues = val.NumField() 18 | getField = val.Field 19 | case reflect.Slice, reflect.Array: 20 | numberOfValues = val.Len() 21 | getField = val.Index 22 | } 23 | 24 | for i := 0; i < numberOfValues; i++ { 25 | walk(getField(i).Interface(), fn) 26 | } 27 | } 28 | 29 | func getValue(x interface{}) reflect.Value { 30 | val := reflect.ValueOf(x) 31 | 32 | if val.Kind() == reflect.Ptr { 33 | val = val.Elem() 34 | } 35 | 36 | return val 37 | } 38 | -------------------------------------------------------------------------------- /reflection/v8/reflection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func walk(x interface{}, fn func(input string)) { 8 | val := getValue(x) 9 | 10 | walkValue := func(value reflect.Value) { 11 | walk(value.Interface(), fn) 12 | } 13 | 14 | switch val.Kind() { 15 | case reflect.String: 16 | fn(val.String()) 17 | case reflect.Struct: 18 | for i := 0; i < val.NumField(); i++ { 19 | walkValue(val.Field(i)) 20 | } 21 | case reflect.Slice, reflect.Array: 22 | for i := 0; i < val.Len(); i++ { 23 | walkValue(val.Index(i)) 24 | } 25 | case reflect.Map: 26 | for _, key := range val.MapKeys() { 27 | walkValue(val.MapIndex(key)) 28 | } 29 | } 30 | } 31 | 32 | func getValue(x interface{}) reflect.Value { 33 | val := reflect.ValueOf(x) 34 | 35 | if val.Kind() == reflect.Ptr { 36 | val = val.Elem() 37 | } 38 | 39 | return val 40 | } 41 | -------------------------------------------------------------------------------- /reflection/v9/reflection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func walk(x interface{}, fn func(input string)) { 8 | val := getValue(x) 9 | 10 | walkValue := func(value reflect.Value) { 11 | walk(value.Interface(), fn) 12 | } 13 | 14 | switch val.Kind() { 15 | case reflect.String: 16 | fn(val.String()) 17 | case reflect.Struct: 18 | for i := 0; i < val.NumField(); i++ { 19 | walkValue(val.Field(i)) 20 | } 21 | case reflect.Slice, reflect.Array: 22 | for i := 0; i < val.Len(); i++ { 23 | walkValue(val.Index(i)) 24 | } 25 | case reflect.Map: 26 | for _, key := range val.MapKeys() { 27 | walkValue(val.MapIndex(key)) 28 | } 29 | case reflect.Chan: 30 | for v, ok := val.Recv(); ok; v, ok = val.Recv() { 31 | walkValue(v) 32 | } 33 | } 34 | } 35 | 36 | func getValue(x interface{}) reflect.Value { 37 | val := reflect.ValueOf(x) 38 | 39 | if val.Kind() == reflect.Ptr { 40 | val = val.Elem() 41 | } 42 | 43 | return val 44 | } 45 | -------------------------------------------------------------------------------- /roman-numerals/v1/numeral_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import "testing" 4 | 5 | func TestRomanNumerals(t *testing.T) { 6 | got := ConvertToRoman(1) 7 | want := "I" 8 | 9 | if got != want { 10 | t.Errorf("got %q, want %q", got, want) 11 | } 12 | } 13 | 14 | func ConvertToRoman(arabic int) string { 15 | return "I" 16 | } 17 | -------------------------------------------------------------------------------- /roman-numerals/v2/numeral_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import "testing" 4 | 5 | func TestRomanNumerals(t *testing.T) { 6 | cases := []struct { 7 | Description string 8 | Arabic int 9 | Want string 10 | }{ 11 | {"1 gets converted to I", 1, "I"}, 12 | {"2 gets converted to II", 2, "II"}, 13 | } 14 | 15 | for _, test := range cases { 16 | t.Run(test.Description, func(t *testing.T) { 17 | got := ConvertToRoman(test.Arabic) 18 | if got != test.Want { 19 | t.Errorf("got %q, want %q", got, test.Want) 20 | } 21 | }) 22 | } 23 | } 24 | 25 | func ConvertToRoman(arabic int) string { 26 | if arabic == 2 { 27 | return "II" 28 | } 29 | return "I" 30 | } 31 | -------------------------------------------------------------------------------- /roman-numerals/v3/numeral_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestRomanNumerals(t *testing.T) { 9 | cases := []struct { 10 | Description string 11 | Arabic int 12 | Want string 13 | }{ 14 | {"1 gets converted to I", 1, "I"}, 15 | {"2 gets converted to II", 2, "II"}, 16 | {"3 gets converted to III", 3, "III"}, 17 | } 18 | 19 | for _, test := range cases { 20 | t.Run(test.Description, func(t *testing.T) { 21 | got := ConvertToRoman(test.Arabic) 22 | if got != test.Want { 23 | t.Errorf("got %q, want %q", got, test.Want) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | func ConvertToRoman(arabic int) string { 30 | 31 | var result strings.Builder 32 | 33 | for i := 0; i < arabic; i++ { 34 | result.WriteString("I") 35 | } 36 | 37 | return result.String() 38 | } 39 | -------------------------------------------------------------------------------- /roman-numerals/v4/numeral_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestRomanNumerals(t *testing.T) { 9 | cases := []struct { 10 | Description string 11 | Arabic int 12 | Want string 13 | }{ 14 | {"1 gets converted to I", 1, "I"}, 15 | {"2 gets converted to II", 2, "II"}, 16 | {"3 gets converted to III", 3, "III"}, 17 | {"4 gets converted to IV (can't repeat more than 3 times)", 4, "IV"}, 18 | } 19 | 20 | for _, test := range cases { 21 | t.Run(test.Description, func(t *testing.T) { 22 | got := ConvertToRoman(test.Arabic) 23 | if got != test.Want { 24 | t.Errorf("got %q, want %q", got, test.Want) 25 | } 26 | }) 27 | } 28 | } 29 | 30 | func ConvertToRoman(arabic int) string { 31 | 32 | var result strings.Builder 33 | 34 | for i := arabic; i > 0; i-- { 35 | if i == 4 { 36 | result.WriteString("IV") 37 | break 38 | } 39 | result.WriteString("I") 40 | } 41 | 42 | return result.String() 43 | } 44 | -------------------------------------------------------------------------------- /select/v1/racer.go: -------------------------------------------------------------------------------- 1 | package racer 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // Racer compares the response times of a and b, returning the fastest one. 9 | func Racer(a, b string) (winner string) { 10 | aDuration := measureResponseTime(a) 11 | bDuration := measureResponseTime(b) 12 | 13 | if aDuration < bDuration { 14 | return a 15 | } 16 | 17 | return b 18 | } 19 | 20 | func measureResponseTime(url string) time.Duration { 21 | start := time.Now() 22 | http.Get(url) 23 | return time.Since(start) 24 | } 25 | -------------------------------------------------------------------------------- /select/v1/racer_test.go: -------------------------------------------------------------------------------- 1 | package racer 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestRacer(t *testing.T) { 11 | 12 | slowServer := makeDelayedServer(20 * time.Millisecond) 13 | fastServer := makeDelayedServer(0 * time.Millisecond) 14 | 15 | defer slowServer.Close() 16 | defer fastServer.Close() 17 | 18 | slowURL := slowServer.URL 19 | fastURL := fastServer.URL 20 | 21 | want := fastURL 22 | got := Racer(slowURL, fastURL) 23 | 24 | if got != want { 25 | t.Errorf("got %q, want %q", got, want) 26 | } 27 | } 28 | 29 | func makeDelayedServer(delay time.Duration) *httptest.Server { 30 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | time.Sleep(delay) 32 | w.WriteHeader(http.StatusOK) 33 | })) 34 | } 35 | -------------------------------------------------------------------------------- /select/v2/racer.go: -------------------------------------------------------------------------------- 1 | package racer 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Racer compares the response times of a and b, returning the fastest one. 8 | func Racer(a, b string) (winner string) { 9 | select { 10 | case <-ping(a): 11 | return a 12 | case <-ping(b): 13 | return b 14 | } 15 | } 16 | 17 | func ping(url string) chan struct{} { 18 | ch := make(chan struct{}) 19 | go func() { 20 | http.Get(url) 21 | close(ch) 22 | }() 23 | return ch 24 | } 25 | -------------------------------------------------------------------------------- /select/v2/racer_test.go: -------------------------------------------------------------------------------- 1 | package racer 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestRacer(t *testing.T) { 11 | 12 | slowServer := makeDelayedServer(20 * time.Millisecond) 13 | fastServer := makeDelayedServer(0 * time.Millisecond) 14 | 15 | defer slowServer.Close() 16 | defer fastServer.Close() 17 | 18 | slowURL := slowServer.URL 19 | fastURL := fastServer.URL 20 | 21 | want := fastURL 22 | got := Racer(slowURL, fastURL) 23 | 24 | if got != want { 25 | t.Errorf("got %q, want %q", got, want) 26 | } 27 | } 28 | 29 | func makeDelayedServer(delay time.Duration) *httptest.Server { 30 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | time.Sleep(delay) 32 | w.WriteHeader(http.StatusOK) 33 | })) 34 | } 35 | -------------------------------------------------------------------------------- /select/v3/racer.go: -------------------------------------------------------------------------------- 1 | package racer 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | var tenSecondTimeout = 10 * time.Second 10 | 11 | // Racer compares the response times of a and b, returning the fastest one, timing out after 10s. 12 | func Racer(a, b string) (winner string, error error) { 13 | return ConfigurableRacer(a, b, tenSecondTimeout) 14 | } 15 | 16 | // ConfigurableRacer compares the response times of a and b, returning the fastest one. 17 | func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, error error) { 18 | select { 19 | case <-ping(a): 20 | return a, nil 21 | case <-ping(b): 22 | return b, nil 23 | case <-time.After(timeout): 24 | return "", fmt.Errorf("timed out waiting for %s and %s", a, b) 25 | } 26 | } 27 | 28 | func ping(url string) chan struct{} { 29 | ch := make(chan struct{}) 30 | go func() { 31 | http.Get(url) 32 | close(ch) 33 | }() 34 | return ch 35 | } 36 | -------------------------------------------------------------------------------- /structs/v1/shapes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Perimeter returns the perimeter of a rectangle. 4 | func Perimeter(width float64, height float64) float64 { 5 | return 2 * (width + height) 6 | } 7 | -------------------------------------------------------------------------------- /structs/v1/shapes_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestPerimeter(t *testing.T) { 6 | got := Perimeter(10.0, 10.0) 7 | want := 40.0 8 | 9 | if got != want { 10 | t.Errorf("got %.2f want %.2f", got, want) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /structs/v2/shapes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Perimeter returns the perimeter of a rectangle. 4 | func Perimeter(width float64, height float64) float64 { 5 | return 2 * (width + height) 6 | } 7 | 8 | // Area returns the area of a rectangle. 9 | func Area(width float64, height float64) float64 { 10 | return width * height 11 | } 12 | -------------------------------------------------------------------------------- /structs/v2/shapes_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestPerimeter(t *testing.T) { 6 | got := Perimeter(10.0, 10.0) 7 | want := 40.0 8 | 9 | if got != want { 10 | t.Errorf("got %.2f want %.2f", got, want) 11 | } 12 | } 13 | 14 | func TestArea(t *testing.T) { 15 | got := Area(12.0, 6.0) 16 | want := 72.0 17 | 18 | if got != want { 19 | t.Errorf("got %.2f want %.2f", got, want) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /structs/v3/shapes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Rectangle has the dimensions of a rectangle. 4 | type Rectangle struct { 5 | Width float64 6 | Height float64 7 | } 8 | 9 | // Perimeter returns the perimeter of the rectangle. 10 | func Perimeter(rectangle Rectangle) float64 { 11 | return 2 * (rectangle.Width + rectangle.Height) 12 | } 13 | 14 | // Area returns the area of the rectangle. 15 | func Area(rectangle Rectangle) float64 { 16 | return rectangle.Width * rectangle.Height 17 | } 18 | -------------------------------------------------------------------------------- /structs/v3/shapes_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestPerimeter(t *testing.T) { 6 | rectangle := Rectangle{10.0, 10.0} 7 | got := Perimeter(rectangle) 8 | want := 40.0 9 | 10 | if got != want { 11 | t.Errorf("got %.2f want %.2f", got, want) 12 | } 13 | } 14 | 15 | func TestArea(t *testing.T) { 16 | rectangle := Rectangle{12.0, 6.0} 17 | got := Area(rectangle) 18 | want := 72.0 19 | 20 | if got != want { 21 | t.Errorf("got %.2f want %.2f", got, want) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /structs/v4/shapes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "math" 4 | 5 | // Rectangle has the dimensions of a rectangle. 6 | type Rectangle struct { 7 | Width float64 8 | Height float64 9 | } 10 | 11 | // Area returns the area of the rectangle. 12 | func (r Rectangle) Area() float64 { 13 | return r.Width * r.Height 14 | } 15 | 16 | // Perimeter returns the perimeter of a rectangle. 17 | func Perimeter(rectangle Rectangle) float64 { 18 | return 2 * (rectangle.Width + rectangle.Height) 19 | } 20 | 21 | // Circle represents a circle... 22 | type Circle struct { 23 | Radius float64 24 | } 25 | 26 | // Area returns the area of the circle. 27 | func (c Circle) Area() float64 { 28 | return math.Pi * c.Radius * c.Radius 29 | } 30 | -------------------------------------------------------------------------------- /structs/v4/shapes_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPerimeter(t *testing.T) { 8 | rectangle := Rectangle{10.0, 10.0} 9 | got := Perimeter(rectangle) 10 | want := 40.0 11 | 12 | if got != want { 13 | t.Errorf("got %g want %g", got, want) 14 | } 15 | } 16 | 17 | func TestArea(t *testing.T) { 18 | 19 | t.Run("rectangles", func(t *testing.T) { 20 | rectangle := Rectangle{12, 6} 21 | got := rectangle.Area() 22 | want := 72.0 23 | 24 | if got != want { 25 | t.Errorf("got %g want %g", got, want) 26 | } 27 | }) 28 | 29 | t.Run("circles", func(t *testing.T) { 30 | circle := Circle{10} 31 | got := circle.Area() 32 | want := 314.1592653589793 33 | 34 | if got != want { 35 | t.Errorf("got %g want %g", got, want) 36 | } 37 | }) 38 | 39 | } 40 | -------------------------------------------------------------------------------- /structs/v5/shapes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "math" 4 | 5 | // Shape is implemented by anything that can tell us its Area. 6 | type Shape interface { 7 | Area() float64 8 | } 9 | 10 | // Rectangle has the dimensions of a rectangle. 11 | type Rectangle struct { 12 | Width float64 13 | Height float64 14 | } 15 | 16 | // Area returns the area of the rectangle. 17 | func (r Rectangle) Area() float64 { 18 | return r.Width * r.Height 19 | } 20 | 21 | // Perimeter returns the perimeter of a rectangle. 22 | func Perimeter(rectangle Rectangle) float64 { 23 | return 2 * (rectangle.Width + rectangle.Height) 24 | } 25 | 26 | // Circle represents a circle... 27 | type Circle struct { 28 | Radius float64 29 | } 30 | 31 | // Area returns the area of the circle. 32 | func (c Circle) Area() float64 { 33 | return math.Pi * c.Radius * c.Radius 34 | } 35 | -------------------------------------------------------------------------------- /structs/v5/shapes_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPerimeter(t *testing.T) { 8 | rectangle := Rectangle{10.0, 10.0} 9 | got := Perimeter(rectangle) 10 | want := 40.0 11 | 12 | if got != want { 13 | t.Errorf("got %g want %g", got, want) 14 | } 15 | } 16 | 17 | func TestArea(t *testing.T) { 18 | 19 | checkArea := func(t testing.TB, shape Shape, want float64) { 20 | t.Helper() 21 | got := shape.Area() 22 | if got != want { 23 | t.Errorf("got %g want %g", got, want) 24 | } 25 | } 26 | 27 | t.Run("rectangles", func(t *testing.T) { 28 | rectangle := Rectangle{12, 6} 29 | checkArea(t, rectangle, 72.0) 30 | }) 31 | 32 | t.Run("circles", func(t *testing.T) { 33 | circle := Circle{10} 34 | checkArea(t, circle, 314.1592653589793) 35 | }) 36 | 37 | } 38 | -------------------------------------------------------------------------------- /structs/v6/shapes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "math" 4 | 5 | // Shape is implemented by anything that can tell us its Area. 6 | type Shape interface { 7 | Area() float64 8 | } 9 | 10 | // Rectangle has the dimensions of a rectangle. 11 | type Rectangle struct { 12 | Width float64 13 | Height float64 14 | } 15 | 16 | // Area returns the area of the rectangle. 17 | func (r Rectangle) Area() float64 { 18 | return r.Width * r.Height 19 | } 20 | 21 | // Perimeter returns the perimeter of a rectangle. 22 | func Perimeter(rectangle Rectangle) float64 { 23 | return 2 * (rectangle.Width + rectangle.Height) 24 | } 25 | 26 | // Circle represents a circle... 27 | type Circle struct { 28 | Radius float64 29 | } 30 | 31 | // Area returns the area of the circle. 32 | func (c Circle) Area() float64 { 33 | return math.Pi * c.Radius * c.Radius 34 | } 35 | -------------------------------------------------------------------------------- /structs/v6/shapes_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPerimeter(t *testing.T) { 8 | rectangle := Rectangle{10.0, 10.0} 9 | got := Perimeter(rectangle) 10 | want := 40.0 11 | 12 | if got != want { 13 | t.Errorf("got %g want %g", got, want) 14 | } 15 | } 16 | 17 | func TestArea(t *testing.T) { 18 | 19 | areaTests := []struct { 20 | shape Shape 21 | want float64 22 | }{ 23 | {Rectangle{12, 6}, 72.0}, 24 | {Circle{10}, 314.1592653589793}, 25 | } 26 | 27 | for _, tt := range areaTests { 28 | got := tt.shape.Area() 29 | if got != tt.want { 30 | t.Errorf("got %g want %g", got, tt.want) 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /structs/v7/shapes_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPerimeter(t *testing.T) { 8 | rectangle := Rectangle{10.0, 10.0} 9 | got := Perimeter(rectangle) 10 | want := 40.0 11 | 12 | if got != want { 13 | t.Errorf("got %g want %g", got, want) 14 | } 15 | } 16 | 17 | func TestArea(t *testing.T) { 18 | 19 | areaTests := []struct { 20 | shape Shape 21 | want float64 22 | }{ 23 | {Rectangle{12, 6}, 72.0}, 24 | {Circle{10}, 314.1592653589793}, 25 | {Triangle{12, 6}, 36.0}, 26 | } 27 | 28 | for _, tt := range areaTests { 29 | got := tt.shape.Area() 30 | if got != tt.want { 31 | t.Errorf("got %g want %g", got, tt.want) 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /structs/v8/shapes_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPerimeter(t *testing.T) { 8 | rectangle := Rectangle{10.0, 10.0} 9 | got := Perimeter(rectangle) 10 | want := 40.0 11 | 12 | if got != want { 13 | t.Errorf("got %g want %g", got, want) 14 | } 15 | } 16 | 17 | func TestArea(t *testing.T) { 18 | 19 | areaTests := []struct { 20 | name string 21 | shape Shape 22 | hasArea float64 23 | }{ 24 | {name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0}, 25 | {name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793}, 26 | {name: "Triangle", shape: Triangle{Base: 12, Height: 6}, hasArea: 36.0}, 27 | } 28 | 29 | for _, tt := range areaTests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | got := tt.shape.Area() 32 | if got != tt.hasArea { 33 | t.Errorf("%#v got %g want %g", tt.shape, got, tt.hasArea) 34 | } 35 | }) 36 | 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /sync/v1/sync.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | // Counter will increment a number. 4 | type Counter struct { 5 | value int 6 | } 7 | 8 | // Inc the count. 9 | func (c *Counter) Inc() { 10 | c.value++ 11 | } 12 | 13 | // Value returns the current count. 14 | func (c *Counter) Value() int { 15 | return c.value 16 | } 17 | -------------------------------------------------------------------------------- /sync/v1/sync_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCounter(t *testing.T) { 8 | 9 | t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) { 10 | counter := Counter{} 11 | counter.Inc() 12 | counter.Inc() 13 | counter.Inc() 14 | 15 | assertCounter(t, counter, 3) 16 | }) 17 | } 18 | 19 | func assertCounter(t testing.TB, got Counter, want int) { 20 | t.Helper() 21 | if got.Value() != want { 22 | t.Errorf("got %d, want %d", got.Value(), want) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sync/v2/sync.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import "sync" 4 | 5 | // Counter will increment a number. 6 | type Counter struct { 7 | mu sync.Mutex 8 | value int 9 | } 10 | 11 | // NewCounter returns a new Counter. 12 | func NewCounter() *Counter { 13 | return &Counter{} 14 | } 15 | 16 | // Inc the count. 17 | func (c *Counter) Inc() { 18 | c.mu.Lock() 19 | defer c.mu.Unlock() 20 | c.value++ 21 | } 22 | 23 | // Value returns the current count. 24 | func (c *Counter) Value() int { 25 | return c.value 26 | } 27 | -------------------------------------------------------------------------------- /sync/v2/sync_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func TestCounter(t *testing.T) { 9 | 10 | t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) { 11 | counter := NewCounter() 12 | counter.Inc() 13 | counter.Inc() 14 | counter.Inc() 15 | 16 | assertCounter(t, counter, 3) 17 | }) 18 | 19 | t.Run("it runs safely concurrently", func(t *testing.T) { 20 | wantedCount := 1000 21 | counter := NewCounter() 22 | 23 | var wg sync.WaitGroup 24 | wg.Add(wantedCount) 25 | 26 | for i := 0; i < wantedCount; i++ { 27 | go func() { 28 | counter.Inc() 29 | wg.Done() 30 | }() 31 | } 32 | wg.Wait() 33 | 34 | assertCounter(t, counter, wantedCount) 35 | }) 36 | 37 | } 38 | 39 | func assertCounter(t testing.TB, got *Counter, want int) { 40 | t.Helper() 41 | if got.Value() != want { 42 | t.Errorf("got %d, want %d", got.Value(), want) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /template.md: -------------------------------------------------------------------------------- 1 | # Chapter template 2 | 3 | Some intro 4 | 5 | ## Write the test first 6 | ## Try to run the test 7 | ## Write the minimal amount of code for the test to run and check the failing test output 8 | ## Write enough code to make it pass 9 | ## Refactor 10 | 11 | ## Repeat for new requirements 12 | ## Wrapping up 13 | -------------------------------------------------------------------------------- /time/v1/blind_alerter.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | // BlindAlerter schedules alerts for blind amounts. 10 | type BlindAlerter interface { 11 | ScheduleAlertAt(duration time.Duration, amount int) 12 | } 13 | 14 | // BlindAlerterFunc allows you to implement BlindAlerter with a function. 15 | type BlindAlerterFunc func(duration time.Duration, amount int) 16 | 17 | // ScheduleAlertAt is BlindAlerterFunc implementation of BlindAlerter. 18 | func (a BlindAlerterFunc) ScheduleAlertAt(duration time.Duration, amount int) { 19 | a(duration, amount) 20 | } 21 | 22 | // StdOutAlerter will schedule alerts and print them to os.Stdout. 23 | func StdOutAlerter(duration time.Duration, amount int) { 24 | time.AfterFunc(duration, func() { 25 | fmt.Fprintf(os.Stdout, "Blind is now %d\n", amount) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /time/v1/cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | poker "github.com/quii/learn-go-with-tests/time/v1" 9 | ) 10 | 11 | const dbFileName = "game.db.json" 12 | 13 | func main() { 14 | store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName) 15 | 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | defer close() 20 | 21 | fmt.Println("Let's play poker") 22 | fmt.Println("Type {Name} wins to record a win") 23 | poker.NewCLI(store, os.Stdin, poker.BlindAlerterFunc(poker.StdOutAlerter)).PlayPoker() 24 | } 25 | -------------------------------------------------------------------------------- /time/v1/cmd/webserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quii/learn-go-with-tests/time/v1" 5 | "log" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | const dbFileName = "game.db.json" 11 | 12 | func main() { 13 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 14 | 15 | if err != nil { 16 | log.Fatalf("problem opening %s %v", dbFileName, err) 17 | } 18 | 19 | store, err := poker.NewFileSystemPlayerStore(db) 20 | 21 | if err != nil { 22 | log.Fatalf("problem creating file system player store, %v ", err) 23 | } 24 | 25 | server := poker.NewPlayerServer(store) 26 | log.Fatal(http.ListenAndServe(":5000", server)) 27 | } 28 | -------------------------------------------------------------------------------- /time/v1/league.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a league. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a league from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing league, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /time/v1/tape.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type tape struct { 9 | file *os.File 10 | } 11 | 12 | func (t *tape) Write(p []byte) (n int, err error) { 13 | t.file.Truncate(0) 14 | t.file.Seek(0, io.SeekStart) 15 | return t.file.Write(p) 16 | } 17 | -------------------------------------------------------------------------------- /time/v1/tape_test.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func TestTape_Write(t *testing.T) { 9 | file, clean := createTempFile(t, "12345") 10 | defer clean() 11 | 12 | tape := &tape{file} 13 | 14 | tape.Write([]byte("abc")) 15 | 16 | file.Seek(0, io.SeekStart) 17 | newFileContents, _ := io.ReadAll(file) 18 | 19 | got := string(newFileContents) 20 | want := "abc" 21 | 22 | if got != want { 23 | t.Errorf("got %q want %q", got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /time/v2/blind_alerter.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | // BlindAlerter schedules alerts for blind amounts. 10 | type BlindAlerter interface { 11 | ScheduleAlertAt(duration time.Duration, amount int) 12 | } 13 | 14 | // BlindAlerterFunc allows you to implement BlindAlerter with a function. 15 | type BlindAlerterFunc func(duration time.Duration, amount int) 16 | 17 | // ScheduleAlertAt is BlindAlerterFunc implementation of BlindAlerter. 18 | func (a BlindAlerterFunc) ScheduleAlertAt(duration time.Duration, amount int) { 19 | a(duration, amount) 20 | } 21 | 22 | // StdOutAlerter will schedule alerts and print them to os.Stdout. 23 | func StdOutAlerter(duration time.Duration, amount int) { 24 | time.AfterFunc(duration, func() { 25 | fmt.Fprintf(os.Stdout, "Blind is now %d\n", amount) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /time/v2/cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | poker "github.com/quii/learn-go-with-tests/time/v2" 9 | ) 10 | 11 | const dbFileName = "game.db.json" 12 | 13 | func main() { 14 | store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName) 15 | 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | defer close() 20 | 21 | game := poker.NewTexasHoldem(poker.BlindAlerterFunc(poker.StdOutAlerter), store) 22 | cli := poker.NewCLI(os.Stdin, os.Stdout, game) 23 | 24 | fmt.Println("Let's play poker") 25 | fmt.Println("Type {Name} wins to record a win") 26 | cli.PlayPoker() 27 | } 28 | -------------------------------------------------------------------------------- /time/v2/cmd/webserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quii/learn-go-with-tests/command-line/v2" 5 | "log" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | const dbFileName = "game.db.json" 11 | 12 | func main() { 13 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 14 | 15 | if err != nil { 16 | log.Fatalf("problem opening %s %v", dbFileName, err) 17 | } 18 | 19 | store, err := poker.NewFileSystemPlayerStore(db) 20 | 21 | if err != nil { 22 | log.Fatalf("problem creating file system player store, %v ", err) 23 | } 24 | 25 | server := poker.NewPlayerServer(store) 26 | log.Fatal(http.ListenAndServe(":5000", server)) 27 | } 28 | -------------------------------------------------------------------------------- /time/v2/league.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a League. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a League from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing League, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /time/v2/tape.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type tape struct { 9 | file *os.File 10 | } 11 | 12 | func (t *tape) Write(p []byte) (n int, err error) { 13 | t.file.Truncate(0) 14 | t.file.Seek(0, io.SeekStart) 15 | return t.file.Write(p) 16 | } 17 | -------------------------------------------------------------------------------- /time/v2/tape_test.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func TestTape_Write(t *testing.T) { 9 | file, clean := createTempFile(t, "12345") 10 | defer clean() 11 | 12 | tape := &tape{file} 13 | 14 | tape.Write([]byte("abc")) 15 | 16 | file.Seek(0, io.SeekStart) 17 | newFileContents, _ := io.ReadAll(file) 18 | 19 | got := string(newFileContents) 20 | want := "abc" 21 | 22 | if got != want { 23 | t.Errorf("got %q want %q", got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /time/v2/texas_holdem.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import "time" 4 | 5 | // TexasHoldem manages a game of poker. 6 | type TexasHoldem struct { 7 | alerter BlindAlerter 8 | store PlayerStore 9 | } 10 | 11 | // NewTexasHoldem returns a new game. 12 | func NewTexasHoldem(alerter BlindAlerter, store PlayerStore) *TexasHoldem { 13 | return &TexasHoldem{ 14 | alerter: alerter, 15 | store: store, 16 | } 17 | } 18 | 19 | // Start will schedule blind alerts dependant on the number of players. 20 | func (p *TexasHoldem) Start(numberOfPlayers int) { 21 | blindIncrement := time.Duration(5+numberOfPlayers) * time.Minute 22 | 23 | blinds := []int{100, 200, 300, 400, 500, 600, 800, 1000, 2000, 4000, 8000} 24 | blindTime := 0 * time.Second 25 | for _, blind := range blinds { 26 | p.alerter.ScheduleAlertAt(blindTime, blind) 27 | blindTime = blindTime + blindIncrement 28 | } 29 | } 30 | 31 | // Finish ends the game, recording the winner. 32 | func (p *TexasHoldem) Finish(winner string) { 33 | p.store.RecordWin(winner) 34 | } 35 | -------------------------------------------------------------------------------- /time/v3/BlindAlerter.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | // BlindAlerter schedules alerts for blind amounts. 10 | type BlindAlerter interface { 11 | ScheduleAlertAt(duration time.Duration, amount int) 12 | } 13 | 14 | // BlindAlerterFunc allows you to implement BlindAlerter with a function. 15 | type BlindAlerterFunc func(duration time.Duration, amount int) 16 | 17 | // ScheduleAlertAt is BlindAlerterFunc implementation of BlindAlerter. 18 | func (a BlindAlerterFunc) ScheduleAlertAt(duration time.Duration, amount int) { 19 | a(duration, amount) 20 | } 21 | 22 | // StdOutAlerter will schedule alerts and print them to os.Stdout. 23 | func StdOutAlerter(duration time.Duration, amount int) { 24 | time.AfterFunc(duration, func() { 25 | fmt.Fprintf(os.Stdout, "Blind is now %d\n", amount) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /time/v3/cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | poker "github.com/quii/learn-go-with-tests/time/v3" 6 | "log" 7 | "os" 8 | ) 9 | 10 | const dbFileName = "game.db.json" 11 | 12 | func main() { 13 | store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName) 14 | 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | defer close() 19 | 20 | game := poker.NewTexasHoldem(poker.BlindAlerterFunc(poker.StdOutAlerter), store) 21 | cli := poker.NewCLI(os.Stdin, os.Stdout, game) 22 | 23 | fmt.Println("Let's play poker") 24 | fmt.Println("Type {Name} wins to record a win") 25 | cli.PlayPoker() 26 | } 27 | -------------------------------------------------------------------------------- /time/v3/cmd/webserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quii/learn-go-with-tests/command-line/v3" 5 | "log" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | const dbFileName = "game.db.json" 11 | 12 | func main() { 13 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 14 | 15 | if err != nil { 16 | log.Fatalf("problem opening %s %v", dbFileName, err) 17 | } 18 | 19 | store, err := poker.NewFileSystemPlayerStore(db) 20 | 21 | if err != nil { 22 | log.Fatalf("problem creating file system player store, %v ", err) 23 | } 24 | 25 | server := poker.NewPlayerServer(store) 26 | 27 | log.Fatal(http.ListenAndServe(":5000", server)) 28 | } 29 | -------------------------------------------------------------------------------- /time/v3/game.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | // Game manages the state of a game. 4 | type Game interface { 5 | Start(numberOfPlayers int) 6 | Finish(winner string) 7 | } 8 | -------------------------------------------------------------------------------- /time/v3/league.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a League. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a League from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing League, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /time/v3/tape.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type tape struct { 9 | file *os.File 10 | } 11 | 12 | func (t *tape) Write(p []byte) (n int, err error) { 13 | t.file.Truncate(0) 14 | t.file.Seek(0, io.SeekStart) 15 | return t.file.Write(p) 16 | } 17 | -------------------------------------------------------------------------------- /time/v3/tape_test.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func TestTape_Write(t *testing.T) { 9 | file, clean := createTempFile(t, "12345") 10 | defer clean() 11 | 12 | tape := &tape{file} 13 | 14 | tape.Write([]byte("abc")) 15 | 16 | file.Seek(0, io.SeekStart) 17 | newFileContents, _ := io.ReadAll(file) 18 | 19 | got := string(newFileContents) 20 | want := "abc" 21 | 22 | if got != want { 23 | t.Errorf("got %q want %q", got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /time/v3/texas_holdem.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import "time" 4 | 5 | // TexasHoldem manages a game of poker. 6 | type TexasHoldem struct { 7 | alerter BlindAlerter 8 | store PlayerStore 9 | } 10 | 11 | // NewTexasHoldem returns a new game. 12 | func NewTexasHoldem(alerter BlindAlerter, store PlayerStore) *TexasHoldem { 13 | return &TexasHoldem{ 14 | alerter: alerter, 15 | store: store, 16 | } 17 | } 18 | 19 | // Start will schedule blind alerts dependant on the number of players. 20 | func (p *TexasHoldem) Start(numberOfPlayers int) { 21 | blindIncrement := time.Duration(5+numberOfPlayers) * time.Minute 22 | 23 | blinds := []int{100, 200, 300, 400, 500, 600, 800, 1000, 2000, 4000, 8000} 24 | blindTime := 0 * time.Second 25 | for _, blind := range blinds { 26 | p.alerter.ScheduleAlertAt(blindTime, blind) 27 | blindTime = blindTime + blindIncrement 28 | } 29 | } 30 | 31 | // Finish ends the game, recording the winner. 32 | func (p *TexasHoldem) Finish(winner string) { 33 | p.store.RecordWin(winner) 34 | } 35 | -------------------------------------------------------------------------------- /title.txt: -------------------------------------------------------------------------------- 1 | --- 2 | title: Learn Go with Tests 3 | author: Chris James 4 | rights: MIT License 5 | lang: en-UK 6 | cover-image: epub-cover.png 7 | ... 8 | -------------------------------------------------------------------------------- /websockets/v1/Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d" 6 | name = "github.com/gorilla/websocket" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d" 10 | version = "v1.4.0" 11 | 12 | [solve-meta] 13 | analyzer-name = "dep" 14 | analyzer-version = 1 15 | input-imports = ["github.com/gorilla/websocket"] 16 | solver-name = "gps-cdcl" 17 | solver-version = 1 18 | -------------------------------------------------------------------------------- /websockets/v1/Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/gorilla/websocket" 30 | version = "1.4.0" 31 | 32 | [prune] 33 | go-tests = true 34 | unused-packages = true 35 | -------------------------------------------------------------------------------- /websockets/v1/blind_alerter.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | // BlindAlerter schedules alerts for blind amounts. 10 | type BlindAlerter interface { 11 | ScheduleAlertAt(duration time.Duration, amount int) 12 | } 13 | 14 | // BlindAlerterFunc allows you to implement BlindAlerter with a function. 15 | type BlindAlerterFunc func(duration time.Duration, amount int) 16 | 17 | // ScheduleAlertAt is BlindAlerterFunc implementation of BlindAlerter. 18 | func (a BlindAlerterFunc) ScheduleAlertAt(duration time.Duration, amount int) { 19 | a(duration, amount) 20 | } 21 | 22 | // StdOutAlerter will schedule alerts and print them to os.Stdout. 23 | func StdOutAlerter(duration time.Duration, amount int) { 24 | time.AfterFunc(duration, func() { 25 | fmt.Fprintf(os.Stdout, "Blind is now %d\n", amount) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /websockets/v1/cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | poker "github.com/quii/learn-go-with-tests/websockets/v1" 9 | ) 10 | 11 | const dbFileName = "game.db.json" 12 | 13 | func main() { 14 | store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName) 15 | 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | defer close() 20 | 21 | game := poker.NewTexasHoldem(poker.BlindAlerterFunc(poker.StdOutAlerter), store) 22 | cli := poker.NewCLI(os.Stdin, os.Stdout, game) 23 | 24 | fmt.Println("Let's play poker") 25 | fmt.Println("Type {Name} wins to record a win") 26 | cli.PlayPoker() 27 | } 28 | -------------------------------------------------------------------------------- /websockets/v1/cmd/webserver/game.html: -------------------------------------------------------------------------------- 1 | ../../game.html -------------------------------------------------------------------------------- /websockets/v1/cmd/webserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quii/learn-go-with-tests/websockets/v1" 5 | "log" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | const dbFileName = "game.db.json" 11 | 12 | func main() { 13 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 14 | 15 | if err != nil { 16 | log.Fatalf("problem opening %s %v", dbFileName, err) 17 | } 18 | 19 | store, err := poker.NewFileSystemPlayerStore(db) 20 | 21 | if err != nil { 22 | log.Fatalf("problem creating file system player store, %v ", err) 23 | } 24 | 25 | server, err := poker.NewPlayerServer(store) 26 | 27 | if err != nil { 28 | log.Fatalf("problem creating player server %v", err) 29 | } 30 | 31 | log.Fatal(http.ListenAndServe(":5000", server)) 32 | } 33 | -------------------------------------------------------------------------------- /websockets/v1/game.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | // Game manages the state of a game. 4 | type Game interface { 5 | Start(numberOfPlayers int) 6 | Finish(winner string) 7 | } 8 | -------------------------------------------------------------------------------- /websockets/v1/game.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lets play poker 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 | 16 | 29 | 30 | -------------------------------------------------------------------------------- /websockets/v1/league.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a League. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a League from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing League, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /websockets/v1/tape.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type tape struct { 9 | file *os.File 10 | } 11 | 12 | func (t *tape) Write(p []byte) (n int, err error) { 13 | t.file.Truncate(0) 14 | t.file.Seek(0, io.SeekStart) 15 | return t.file.Write(p) 16 | } 17 | -------------------------------------------------------------------------------- /websockets/v1/tape_test.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func TestTape_Write(t *testing.T) { 9 | file, clean := createTempFile(t, "12345") 10 | defer clean() 11 | 12 | tape := &tape{file} 13 | 14 | tape.Write([]byte("abc")) 15 | 16 | file.Seek(0, io.SeekStart) 17 | newFileContents, _ := io.ReadAll(file) 18 | 19 | got := string(newFileContents) 20 | want := "abc" 21 | 22 | if got != want { 23 | t.Errorf("got %q want %q", got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /websockets/v1/texas_holdem.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import "time" 4 | 5 | // TexasHoldem manages a game of poker. 6 | type TexasHoldem struct { 7 | alerter BlindAlerter 8 | store PlayerStore 9 | } 10 | 11 | // NewTexasHoldem returns a new game. 12 | func NewTexasHoldem(alerter BlindAlerter, store PlayerStore) *TexasHoldem { 13 | return &TexasHoldem{ 14 | alerter: alerter, 15 | store: store, 16 | } 17 | } 18 | 19 | // Start will schedule blind alerts dependant on the number of players. 20 | func (p *TexasHoldem) Start(numberOfPlayers int) { 21 | blindIncrement := time.Duration(5+numberOfPlayers) * time.Minute 22 | 23 | blinds := []int{100, 200, 300, 400, 500, 600, 800, 1000, 2000, 4000, 8000} 24 | blindTime := 0 * time.Second 25 | for _, blind := range blinds { 26 | p.alerter.ScheduleAlertAt(blindTime, blind) 27 | blindTime = blindTime + blindIncrement 28 | } 29 | } 30 | 31 | // Finish ends the game, recording the winner. 32 | func (p *TexasHoldem) Finish(winner string) { 33 | p.store.RecordWin(winner) 34 | } 35 | -------------------------------------------------------------------------------- /websockets/v1/vendor/github.com/gorilla/websocket/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | .idea/ 25 | *.iml 26 | -------------------------------------------------------------------------------- /websockets/v1/vendor/github.com/gorilla/websocket/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - go: 1.7.x 7 | - go: 1.8.x 8 | - go: 1.9.x 9 | - go: 1.10.x 10 | - go: 1.11.x 11 | - go: tip 12 | allow_failures: 13 | - go: tip 14 | 15 | script: 16 | - go get -t -v ./... 17 | - diff -u <(echo -n) <(gofmt -d .) 18 | - go vet $(go list ./... | grep -v /vendor/) 19 | - go test -v -race ./... 20 | -------------------------------------------------------------------------------- /websockets/v1/vendor/github.com/gorilla/websocket/AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of Gorilla WebSocket authors for copyright 2 | # purposes. 3 | # 4 | # Please keep the list sorted. 5 | 6 | Gary Burd 7 | Google LLC (https://opensource.google.com/) 8 | Joachim Bauch 9 | 10 | -------------------------------------------------------------------------------- /websockets/v1/vendor/github.com/gorilla/websocket/client_clone.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build go1.8 6 | 7 | package websocket 8 | 9 | import "crypto/tls" 10 | 11 | func cloneTLSConfig(cfg *tls.Config) *tls.Config { 12 | if cfg == nil { 13 | return &tls.Config{} 14 | } 15 | return cfg.Clone() 16 | } 17 | -------------------------------------------------------------------------------- /websockets/v1/vendor/github.com/gorilla/websocket/conn_write.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build go1.8 6 | 7 | package websocket 8 | 9 | import "net" 10 | 11 | func (c *Conn) writeBufs(bufs ...[]byte) error { 12 | b := net.Buffers(bufs) 13 | _, err := b.WriteTo(c.conn) 14 | return err 15 | } 16 | -------------------------------------------------------------------------------- /websockets/v1/vendor/github.com/gorilla/websocket/conn_write_legacy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !go1.8 6 | 7 | package websocket 8 | 9 | func (c *Conn) writeBufs(bufs ...[]byte) error { 10 | for _, buf := range bufs { 11 | if len(buf) > 0 { 12 | if _, err := c.conn.Write(buf); err != nil { 13 | return err 14 | } 15 | } 16 | } 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /websockets/v1/vendor/github.com/gorilla/websocket/mask_safe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of 2 | // this source code is governed by a BSD-style license that can be found in the 3 | // LICENSE file. 4 | 5 | // +build appengine 6 | 7 | package websocket 8 | 9 | func maskBytes(key [4]byte, pos int, b []byte) int { 10 | for i := range b { 11 | b[i] ^= key[pos&3] 12 | pos++ 13 | } 14 | return pos & 3 15 | } 16 | -------------------------------------------------------------------------------- /websockets/v1/vendor/github.com/gorilla/websocket/trace.go: -------------------------------------------------------------------------------- 1 | // +build go1.8 2 | 3 | package websocket 4 | 5 | import ( 6 | "crypto/tls" 7 | "net/http/httptrace" 8 | ) 9 | 10 | func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { 11 | if trace.TLSHandshakeStart != nil { 12 | trace.TLSHandshakeStart() 13 | } 14 | err := doHandshake(tlsConn, cfg) 15 | if trace.TLSHandshakeDone != nil { 16 | trace.TLSHandshakeDone(tlsConn.ConnectionState(), err) 17 | } 18 | return err 19 | } 20 | -------------------------------------------------------------------------------- /websockets/v1/vendor/github.com/gorilla/websocket/trace_17.go: -------------------------------------------------------------------------------- 1 | // +build !go1.8 2 | 3 | package websocket 4 | 5 | import ( 6 | "crypto/tls" 7 | "net/http/httptrace" 8 | ) 9 | 10 | func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { 11 | return doHandshake(tlsConn, cfg) 12 | } 13 | -------------------------------------------------------------------------------- /websockets/v2/Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d" 6 | name = "github.com/gorilla/websocket" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d" 10 | version = "v1.4.0" 11 | 12 | [solve-meta] 13 | analyzer-name = "dep" 14 | analyzer-version = 1 15 | input-imports = ["github.com/gorilla/websocket"] 16 | solver-name = "gps-cdcl" 17 | solver-version = 1 18 | -------------------------------------------------------------------------------- /websockets/v2/Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/gorilla/websocket" 30 | version = "1.4.0" 31 | 32 | [prune] 33 | go-tests = true 34 | unused-packages = true 35 | -------------------------------------------------------------------------------- /websockets/v2/blind_alerter.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | ) 8 | 9 | // BlindAlerter schedules alerts for blind amounts. 10 | type BlindAlerter interface { 11 | ScheduleAlertAt(duration time.Duration, amount int, to io.Writer) 12 | } 13 | 14 | // BlindAlerterFunc allows you to implement BlindAlerter with a function. 15 | type BlindAlerterFunc func(duration time.Duration, amount int, to io.Writer) 16 | 17 | // ScheduleAlertAt is BlindAlerterFunc implementation of BlindAlerter. 18 | func (a BlindAlerterFunc) ScheduleAlertAt(duration time.Duration, amount int, to io.Writer) { 19 | a(duration, amount, to) 20 | } 21 | 22 | // Alerter will schedule alerts and print them to "to". 23 | func Alerter(duration time.Duration, amount int, to io.Writer) { 24 | time.AfterFunc(duration, func() { 25 | fmt.Fprintf(to, "Blind is now %d\n", amount) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /websockets/v2/cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | poker "github.com/quii/learn-go-with-tests/websockets/v2" 9 | ) 10 | 11 | const dbFileName = "game.db.json" 12 | 13 | func main() { 14 | store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName) 15 | 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | defer close() 20 | 21 | game := poker.NewTexasHoldem(poker.BlindAlerterFunc(poker.Alerter), store) 22 | cli := poker.NewCLI(os.Stdin, os.Stdout, game) 23 | 24 | fmt.Println("Let's play poker") 25 | fmt.Println("Type {Name} wins to record a win") 26 | cli.PlayPoker() 27 | } 28 | -------------------------------------------------------------------------------- /websockets/v2/cmd/webserver/game.html: -------------------------------------------------------------------------------- 1 | ../../game.html -------------------------------------------------------------------------------- /websockets/v2/cmd/webserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quii/learn-go-with-tests/websockets/v2" 5 | "log" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | const dbFileName = "game.db.json" 11 | 12 | func main() { 13 | db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) 14 | 15 | if err != nil { 16 | log.Fatalf("problem opening %s %v", dbFileName, err) 17 | } 18 | 19 | store, err := poker.NewFileSystemPlayerStore(db) 20 | 21 | if err != nil { 22 | log.Fatalf("problem creating file system player store, %v ", err) 23 | } 24 | 25 | game := poker.NewTexasHoldem(poker.BlindAlerterFunc(poker.Alerter), store) 26 | 27 | server, err := poker.NewPlayerServer(store, game) 28 | 29 | if err != nil { 30 | log.Fatalf("problem creating player server %v", err) 31 | } 32 | 33 | log.Fatal(http.ListenAndServe(":5000", server)) 34 | } 35 | -------------------------------------------------------------------------------- /websockets/v2/game.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import "io" 4 | 5 | // Game manages the state of a game. 6 | type Game interface { 7 | Start(numberOfPlayers int, alertsDestination io.Writer) 8 | Finish(winner string) 9 | } 10 | -------------------------------------------------------------------------------- /websockets/v2/league.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // League stores a collection of players. 10 | type League []Player 11 | 12 | // Find tries to return a player from a League. 13 | func (l League) Find(name string) *Player { 14 | for i, p := range l { 15 | if p.Name == name { 16 | return &l[i] 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // NewLeague creates a League from JSON. 23 | func NewLeague(rdr io.Reader) (League, error) { 24 | var league []Player 25 | err := json.NewDecoder(rdr).Decode(&league) 26 | 27 | if err != nil { 28 | err = fmt.Errorf("problem parsing League, %v", err) 29 | } 30 | 31 | return league, err 32 | } 33 | -------------------------------------------------------------------------------- /websockets/v2/player_server_ws.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | type playerServerWS struct { 10 | *websocket.Conn 11 | } 12 | 13 | func (w *playerServerWS) Write(p []byte) (n int, err error) { 14 | err = w.WriteMessage(websocket.TextMessage, p) 15 | 16 | if err != nil { 17 | return 0, err 18 | } 19 | 20 | return len(p), nil 21 | } 22 | 23 | func newPlayerServerWS(w http.ResponseWriter, r *http.Request) *playerServerWS { 24 | conn, err := wsUpgrader.Upgrade(w, r, nil) 25 | 26 | if err != nil { 27 | log.Printf("problem upgrading connection to websockets %v\n", err) 28 | } 29 | 30 | return &playerServerWS{conn} 31 | } 32 | 33 | func (w *playerServerWS) WaitForMsg() string { 34 | _, msg, err := w.ReadMessage() 35 | if err != nil { 36 | log.Printf("error reading from websocket %v\n", err) 37 | } 38 | return string(msg) 39 | } 40 | -------------------------------------------------------------------------------- /websockets/v2/tape.go: -------------------------------------------------------------------------------- 1 | package poker 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | // Tape represents an os.File that will re-write from the start on every Write call. 9 | type Tape struct { 10 | File *os.File 11 | } 12 | 13 | func (t *Tape) Write(p []byte) (n int, err error) { 14 | t.File.Truncate(0) 15 | t.File.Seek(0, io.SeekStart) 16 | return t.File.Write(p) 17 | } 18 | -------------------------------------------------------------------------------- /websockets/v2/tape_test.go: -------------------------------------------------------------------------------- 1 | package poker_test 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | poker "github.com/quii/learn-go-with-tests/websockets/v2" 8 | ) 9 | 10 | func TestTape_Write(t *testing.T) { 11 | file, clean := createTempFile(t, "12345") 12 | defer clean() 13 | 14 | tape := &poker.Tape{File: file} 15 | 16 | tape.Write([]byte("abc")) 17 | 18 | file.Seek(0, io.SeekStart) 19 | newFileContents, _ := io.ReadAll(file) 20 | 21 | got := string(newFileContents) 22 | want := "abc" 23 | 24 | if got != want { 25 | t.Errorf("got %q want %q", got, want) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /websockets/v2/vendor/github.com/gorilla/websocket/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | .idea/ 25 | *.iml 26 | -------------------------------------------------------------------------------- /websockets/v2/vendor/github.com/gorilla/websocket/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - go: 1.7.x 7 | - go: 1.8.x 8 | - go: 1.9.x 9 | - go: 1.10.x 10 | - go: 1.11.x 11 | - go: tip 12 | allow_failures: 13 | - go: tip 14 | 15 | script: 16 | - go get -t -v ./... 17 | - diff -u <(echo -n) <(gofmt -d .) 18 | - go vet $(go list ./... | grep -v /vendor/) 19 | - go test -v -race ./... 20 | -------------------------------------------------------------------------------- /websockets/v2/vendor/github.com/gorilla/websocket/AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of Gorilla WebSocket authors for copyright 2 | # purposes. 3 | # 4 | # Please keep the list sorted. 5 | 6 | Gary Burd 7 | Google LLC (https://opensource.google.com/) 8 | Joachim Bauch 9 | 10 | -------------------------------------------------------------------------------- /websockets/v2/vendor/github.com/gorilla/websocket/client_clone.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build go1.8 6 | 7 | package websocket 8 | 9 | import "crypto/tls" 10 | 11 | func cloneTLSConfig(cfg *tls.Config) *tls.Config { 12 | if cfg == nil { 13 | return &tls.Config{} 14 | } 15 | return cfg.Clone() 16 | } 17 | -------------------------------------------------------------------------------- /websockets/v2/vendor/github.com/gorilla/websocket/conn_write.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build go1.8 6 | 7 | package websocket 8 | 9 | import "net" 10 | 11 | func (c *Conn) writeBufs(bufs ...[]byte) error { 12 | b := net.Buffers(bufs) 13 | _, err := b.WriteTo(c.conn) 14 | return err 15 | } 16 | -------------------------------------------------------------------------------- /websockets/v2/vendor/github.com/gorilla/websocket/conn_write_legacy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !go1.8 6 | 7 | package websocket 8 | 9 | func (c *Conn) writeBufs(bufs ...[]byte) error { 10 | for _, buf := range bufs { 11 | if len(buf) > 0 { 12 | if _, err := c.conn.Write(buf); err != nil { 13 | return err 14 | } 15 | } 16 | } 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /websockets/v2/vendor/github.com/gorilla/websocket/mask_safe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of 2 | // this source code is governed by a BSD-style license that can be found in the 3 | // LICENSE file. 4 | 5 | // +build appengine 6 | 7 | package websocket 8 | 9 | func maskBytes(key [4]byte, pos int, b []byte) int { 10 | for i := range b { 11 | b[i] ^= key[pos&3] 12 | pos++ 13 | } 14 | return pos & 3 15 | } 16 | -------------------------------------------------------------------------------- /websockets/v2/vendor/github.com/gorilla/websocket/trace.go: -------------------------------------------------------------------------------- 1 | // +build go1.8 2 | 3 | package websocket 4 | 5 | import ( 6 | "crypto/tls" 7 | "net/http/httptrace" 8 | ) 9 | 10 | func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { 11 | if trace.TLSHandshakeStart != nil { 12 | trace.TLSHandshakeStart() 13 | } 14 | err := doHandshake(tlsConn, cfg) 15 | if trace.TLSHandshakeDone != nil { 16 | trace.TLSHandshakeDone(tlsConn.ConnectionState(), err) 17 | } 18 | return err 19 | } 20 | -------------------------------------------------------------------------------- /websockets/v2/vendor/github.com/gorilla/websocket/trace_17.go: -------------------------------------------------------------------------------- 1 | // +build !go1.8 2 | 3 | package websocket 4 | 5 | import ( 6 | "crypto/tls" 7 | "net/http/httptrace" 8 | ) 9 | 10 | func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { 11 | return doHandshake(tlsConn, cfg) 12 | } 13 | --------------------------------------------------------------------------------