/exercism-go

Exercism exercises in Go.

Primary LanguageGoMIT LicenseMIT

Exercism Go Track

Configlet Status Exercise Test Status

Exercism exercises in Go.

Issues/Feedback

⚠️ Please be aware that this repository currently does not accept community contributions. This blog post explains the details.

If you have any feedback or experience problems, you can bring them up in the Go section of the Exercism forum.

Development setup

If you work on this repository, you should follow some standard Go development practices. You should have a recent version of Go installed, ideally either the current release or previous release.

You will need a github account and you will need to fork exercism/go to your account. See GitHub Help if you are unfamiliar with the process. Clone your fork with the command: git clone https://github.com/<you>/go. Test your clone by cding to the go directory and typing bin/fetch-golangci-lint and then bin/run-tests. You should see tests pass for all exercises.

Note that unlike most other Go code, it is not necessary to clone this to your GOPATH. This is because this repo only imports from the standard library and isn't expected to be imported by other packages.

Your Go code should be formatted using the gofmt tool.

There is a misspelling tool. You can install and occasionally run it to find low hanging typo problems. #570 It's not added into CI since it could give false positives.

Contributing Guide

Please be familiar with the contributing guide in the docs repository. This describes some great ways to get involved. In particular, please read the Pull Request Guidelines before opening a pull request.

Exercism Go style

Let's walk through an example, non-existent, exercise, which we'll call fizzbuzz to see what could be included in its implementation.

Exercise configuration

An exercise is configured via an entry in the exercises array in config.json file. If fizzbuzz is an optional exercise, it would have an entry below the core exercises that might look like:

{
  "slug": "fizzbuzz",
  "uuid": "mumblety-peg-whatever",
  "core": false,
  "unlocked_by": "two-fer",
  "difficulty": 1,
  "topics": ["conditionals"]
}

See Exercism Docs: config.json for more info.

Exercise files: Overview

For any exercise you may see a number of files present in a directory under exercises/:

~/go/exercises/fizzbuzz
$ tree -a
.
├── cases_test.go
├── example.go
├── fizzbuzz.go
├── fizzbuzz_test.go
├── .meta
│   └── description.md
│   └── gen.go
│   └── hints.md
│   └── metadata.yml
└── README.md

This list of files can vary across exercises. Not all exercises use all of these files. Exercises originate their test data and README text from the Exercism problem-specification repository. This repository collects common information for all exercises across all tracks. However, should track-specific documentation need to be included with the exercise, files in an exercise's .meta/ directory can be used to override or augment the exercise's README.

Let's briefly describe each file:

  • cases_test.go - Contains generated test cases, using test data sourced from the problem-specifications repository. Only in some exercises. Automatically generated by .meta/gen.go.

  • example.go - An example solution for the exercise used to verify the test suite. Ignored by the exercism fetch command. See also ignored files.

  • fizzbuzz.go - A stub file, in some early exercises to give users a starting point.

  • fizzbuzz_test.go - The main test file for the exercise.

  • .meta/ - Contains files not to be included when a user fetches an exercise: See also ignored files.

  • .meta/description.md - Use to generate a track specific description of the exercise in the exercise's README.

  • .meta/gen.go - Generates cases_test.go when present. See also synchronizing exercises with problem specifications.

  • .meta/hints.md - Use to add track specific information in addition to the generic exercise's problem-specification description in the README.

  • .meta/metadata.yml - Track specific exercise metadata, overrides the exercise metadata from the problem-specifications repository.

In some exercises there can be extra files, for instance the series exercise contains extra test files.

Ignored files

When a user fetches an exercise, they do not need to get all the files within an exercise directory. For instance; the example.go files that contain an example solution, or the gen.go files used to generate an exercise's test cases. Therefore there are certain files and directories that are ignored when an exercise is fetched. These are:

  • The .meta directory and anything within it.
  • Any file that matches the ignore_pattern defined in config.json file. This currently matches any filename that contains the word example, unless it is followed by the word test, with any number of characters in between.

Example solutions

example.go is a reference solution. It is a valid solution that the CI (continuous integration) service can run tests against. Files with "example" in the file name are skipped by the exercism fetch command. Because of this, there is less need for this code to be a model of style, expression and readability, or to use the best algorithm. Examples can be plain, simple, concise, even naïve, as long as they are correct.

Stub files

Stub files, such as leap.go, are a starting point for solutions. Not all exercises need to have a stub file, only exercises early in the syllabus. By convention, the stub file for an exercise with slug exercise-slug must be named exercise_slug.go. This is because CI needs to delete stub files to avoid conflicting definitions.

Tests

The test file is fetched for the solver and deserves attention for consistency and appearance.

The leap exercise makes use of data-driven tests. Test cases are defined as data, then a test function iterates over the data. In this exercise, as they are generated, the test cases are defined in the cases_test.go file. The test function that iterates over this data is defined in the leap_test.go file. The cases_test.go file is generated by information found in problem-specifications using generators. To add additional test cases (e.g. test cases that only make sense for Go) add the test cases to <exercise>_test.go. An example of using additional test cases can be found in the exercise two-bucket.

Identifiers within the test function appear in actual-expected order as described at Useful Test Failures. Here the identifier observed is used instead of actual. That's fine. More common are words got and want. They are clear and short. Note Useful Test Failures is part of Code Review Comments. Really we like most of the advice on that page.

In Go we generally have all tests enabled and do not ask the solver to edit the test program, to enable progressive tests for example. t.Fatalf(), as seen in the leap_test.go file, will stop tests at the first failure encountered, so the solver is not faced with too many failures at once.

Testable examples

Some exercises can contain Example tests that document the exercise API. These examples are run alongside the standard exercise tests and will verify that the exercise API is working as expected. They are not required by all exercises and are not intended to replace the data-driven tests. They are most useful for providing examples of how an exercise's API is used. Have a look at the example tests in the clock exercise to see them in action.

Errors

We like errors in Go. It's not idiomatic Go to ignore invalid data or have undefined behavior. Sometimes our Go tests require an error return where other language tracks don't.

Benchmarks

In most test files there will also be benchmark tests, as can be seen at the end of the leap_test.go file. In Go, benchmarking is a first-class citizen of the testing package. We throw in benchmarks because they're interesting, and because it is idiomatic in Go to think about performance. There is no critical use for these though. Usually they will just bench the combined time to run over all the test data rather than attempt precise timings on single function calls. They are useful if they let the solver try a change and see a performance effect.

Synchronizing exercises with problem specifications

Some problems that are implemented in multiple tracks use the same inputs and outputs to define the test suites. Where the problem-specifications repository contains a canonical-data.json file with these inputs and outputs, we can generate the test cases programmatically. The problem-specifications repo also defines the instructions for the exercises, which are also shared across tracks and must also be synchronized.

Test structure

See the gen.go file in the leap exercise for an example of how this can be done.

Test case generators are named gen.go and are kept in a special .meta directory within each exercise that makes use of a test cases generator. This .meta directory will be ignored when a user fetches an exercise.

Whenever the shared JSON data changes, the test cases will need to be regenerated. The generator will first look for a local copy of the problem-specifications repository. If there isn't one it will attempt to get the relevant json data for the exercise from the problem-specifications repository on GitHub. The generator uses the GitHub API to find some information about exercises when it cannot find a local copy of problem-specifications. This can cause throttling issues if working with a large number of exercises. We therefore recommend using a local copy of the repository when possible (remember to keep it current 😄).

To use a local copy of the problem-specifications repository, make sure that it has been cloned into the same parent-directory as the go repository.

$ tree -L 1 .
.
├── problem-specifications
└── go

Adding a test generator to an exercise

For some exercises, a test generator is used to generate the cases_test.go file with the test cases based on information from problem-specifications. To add a new exercise generator to an exercise the following steps are needed:

  1. Create the file gen.go in the directory .meta of the exercise
  2. Add the following template to gen.go:
package main

import (
    "log"
    "text/template"
  
    "../../../../gen"
)

func main() {
	t, err := template.New("").Parse(tmpl)
	if err != nil {
		log.Fatal(err)
	}
	var j = map[string]interface{}{
              "property_1":  &[]Property1Case{},
              "property_2":  &[]Property2Case{},
	}
	if err := gen.Gen("<exercise-name>", j, t); err != nil {
		log.Fatal(err)
	}
}
  1. Insert the name of the exercise to the call of gen.Gen
  2. Add all values for the field property in canonical-data.json to the map j. canonical-data.json can be found at problem-specifications/exercises/<exercise-name>
  3. Create the needed structs for storing the test cases from canonical-data.json (you can for example use JSON-to-Go to convert the JSON to a struct)

NOTE: In some cases, the struct of the data in the input/expected fields is not the same for all test cases of one property. In those situations, an interface{} has to be used to represent the values for these fields. These interface{} values then need to be handled by the test generator. A common way to handle these cases is to create methods on the test case structs that perform type assertions on the interface{} values and return something more meaningful. These methods can then be referenced/called in the tmpl template variable. Examples of this can be found in the exercises forth or bowling.

  1. Add the variable tmpl to gen.go. This template will be used to create the cases_test.go file.

Example:

var tmpl = `package <package of exercise>

{{.Header}}

var testCases = []struct {
	description    string
	input          int
	expected       int       
}{ {{range .J.<property>}}
{
	description: {{printf "%q"  .Description}},
	input: {{printf "%d"  .Score}},
	expected: {{printf "%d"  .Score}},
},{{end}}
}
`
  1. Synchronize the test case using the exercise generator (as described in Synchronizing tests and instructions)
  2. Check the validity of cases_test.go
  3. Use the generated test cases in the <exercise>_test.go file
  4. Check if .meta/example.go passes all tests

Synchronizing tests and instructions

To keep track of which tests are implemented by the exercise the file .meta/tests.toml is used by configlet.

To synchronize the exercise with problem-specifications and to regenerate the tests, navigate into the go directory and perform the following steps:

  1. Synchronize your exercise with exercism/problem-specifications using configlet:
$ configlet sync --update -e <exercise>

configlet synchronizes the following parts, if an updated is needed:

  • docs: .docs/instructions.md, .docs/introduction.md
  • metadata: .meta/config.json
  • tests: .meta/tests.toml
  • filepaths: ./meta/config.json

For further instructions check out configlet.

  1. Run the test case generator to update <exercise>/cases_test.go:
$ GO111MODULE=off go run exercises/practice/<exercise>/.meta/gen.go

NOTE: If you see the error json: cannot unmarshal object into Go value of type []gen.Commit when running the generator you probably have been rate limited by GitHub. Try providing a GitHub access token with the flag -github_token="<Token>". Using the token will result in a higher rate limit. The token does not need any specific scopes as it is only used to fetch infos about commits.

You should see that some/all of the above files have changed. Commit the changes.

Synchronizing all exercises with generators

$ ./bin/run-generators <GitHub Access Token>

NOTE: If you see the error json: cannot unmarshal object into Go value of type []gen.Commit when running the generator you probably have been rate limited by GitHub. Make sure you provided the GitHub access token as first argument to the script as shown above. Using the token will result in a higher rate limit. The token does not need any specific scopes as it is only used to fetch infos about commits.

Managing the Go version

For an easy management of the Go version in the go.mod file in all exercises, we can use gomod-sync. This is a tool made in Go that can be seen in the gomod-sync/ folder.

To update all go.mod files according to the config file (gomod-sync/config.json) run:

$ cd gomod-sync && go run main.go update

To check all exercise go.mod files specify the correct Go version, run:

$ cd gomod-sync && go run main.go check

Pull requests

Pull requests are welcome. You forked, cloned, coded and tested and you have something good? Awesome! Use git to add, commit, and push to your repository. Checkout your repository on the web now. You should see your commit and the invitation to submit a pull request!

Click on that big green button. You have a chance to add more explanation to your pull request here, then send it. Looking at the exercism/go repository now instead of your own, you see this.

That inconspicuous orange dot is important! Hover over it (no, not on this image, on a real page) and you can see it's indicating that a CI build is in progress. After a few minutes (usually) that dot will turn green indicating that tests passed. If there's a problem, it comes up red:

This means you've still got work to do. Click on "details" to go to the CI build details. Look over the build log for clues. Usually error messages will be helpful and you can correct the problem.

Direction

Directions are unlimited. This code is fresh and evolving. Explore the existing code and you will see some new directions being tried. Your fresh ideas and contributions are welcome.

Go icon

The Go logo was designed by Renée French, and has been released under the Creative Commons 3.0 Attributions license.