ph.codeberg.pagehttps://ph.codeberg.page2025-12-19T16:52:32ZChanges on this website.Piotr Husiatyńskihttps://ph.codeberg.page/2025-12-19-changes_on_this_website2025-12-19T00:00:00Z<p>There are two changes to this website worth mentioning.</p>
<p>First, hosting has moved from GitHub to Codeberg.</p>
<p>Second, I have switched from Hugo to a custom rendering engine.</p>
<p>I never needed the advanced functionality Hugo provides.
After an upgrade, my custom theme broke and I could no longer compile this website.
Instead of spending hours figuring out and fixing the issue, I decided to finally roll out my own minimal compiler.
Excluding the templates, the implementation is less than <a href="https://codeberg.org/ph/pages/src/branch/main/main.go">330 lines of simple Go code</a>, with goldmark as the single dependency to convert Markdown to HTML.</p>
<p>The build process and deployment are handled by <a href="https://codeberg.org/ph/pages/src/commit/a1977a805984d1793149965d2dc338c58eb2b76c/run#L15-L30">a bash function</a>, which compiles the content and publishes it via a designated Git branch.</p>
<p>The feed URL is now <a href="https://ph.codeberg.page/feed.xml">https://ph.codeberg.page/feed.xml</a>.</p>
Routing enhancements.Piotr Husiatyńskihttps://ph.codeberg.page/2024-06-12-routing_enhancements2024-06-12T00:00:00Z<p>Go 1.22 ships with <a href="https://go.dev/blog/routing-enhancements">router Enhancements</a>.
The <a href="https://godocs.io/net/http#ServeMux"><code>net/http.ServeMux</code></a> can now match requests by method, host and a simple path wildcard.</p>
<p>With the new ServeMux, it is no longer necessary to <a href="https://benhoyt.com/writings/go-routing/">struggle</a> to find the best routing method. For most cases, standard library should be the best choice.
And with the next release, you can align your declarations with <a href="https://github.com/golang/go/commit/7b583fd1a1aeda98daa5a9d485b35786c031e941">any number of spaces</a>.</p>
<pre><code class="language-go">func run() {
rt := http.NewServeMux()
rt.Handle(`POST /users`, &demoHandler{info: "create user"})
rt.Handle(`GET /users/{name}`, &demoHandler{info: "show user"})
rt.Handle(`GET /users/{name}/profile`, &demoHandler{info: "show user profile"})
_ = http.ListenAndServe("localhost:8000", rt)
}
type demoHandler struct {
info string
}
func (h *demoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, h.info, r.PathValue("name"))
}
</code></pre>
<pre><code class="language-sh">% curl localhost:8000/users
Method Not Allowed
% curl localhost:8000/users -X POST
create user
% curl localhost:8000/users/andy
show user andy
% curl localhost:8000/users/andy/profile
show user profile andy
% curl localhost:8000/users/andy/profile -X POST
Method Not Allowed
</code></pre>
Tasks script.Piotr Husiatyńskihttps://ph.codeberg.page/2024-05-28-tasks_script2024-05-28T00:00:00Z<p>Every project grows to a point where a set of custom tasks must be executed on various occasions.
It is good to write those commands down, so that they don't get lost and anyone can execute them.
I used to maintain a <a href="https://makefiletutorial.com/"><em>Makefile</em></a> as a simple way to organize and share tasks with others.</p>
<p>As the complexity grows, and more functionality is needed, Makefile becomes more unreadable.
It feels like using the wrong tool for the job.</p>
<p>And indeed, there is a better tool - shell scripting.</p>
<p>The below <a href="https://en.wikipedia.org/wiki/Bash_(Unix_shell)">Bash</a> script is a solid base to extend, for managing a collection of tasks.</p>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail
function task:help {
# Print this script help.
local tasks
local self_path
local desc
printf "%s <task> [args]
Tasks:
" "${0}"
tasks=$(compgen -A function | sed -En 's/task:(.*)//p')
self_path=$(realpath "$0")
for task in ${tasks}; do
desc=$(grep "function task:$task {" "$self_path" -A 1 | sed -En 's/.*# (.*)//p')
printf " %-32s %s
" "$task" "$desc"
done
}
# shellcheck disable=SC2145
"task:${@:-help}"
</code></pre>
<p>In order to register a new task, define a <code>task:<name></code> function.
The first line, when comment, is used as that task documentation.</p>
<pre><code class="language-bash">function task:say-hello {
# Greet the user.
echo "Hello $USER"
}
</code></pre>
<p>When the script is called with no arguments, it runs <code>help</code> that renders the list of available tasks with their description.</p>
<pre><code class="language-sh">% ./run
./run <task> [args]
Tasks:
say-hello Greet the user.
help Print this script help.
</code></pre>
Testing in go.Piotr Husiatyńskihttps://ph.codeberg.page/2020-01-30-testing_in_go2020-01-30T00:00:00Z<p>This is a collection of testing techniques and patterns that I have learned
throughout my career of being a Go programmer.</p>
<h2><code>testing</code> package basics</h2>
<p>The Go standard library comes with the
<a href="https://golang.org/pkg/testing/"><code>testing</code></a> package which provides a solid
base for writing tests.</p>
<p>Each test should be a separate function. A test function must accept a single
argument of type <a href="https://golang.org/pkg/testing/#T"><code>*testing.T</code></a>.</p>
<p>A test for a functoin <code>isEven</code> could look like this:</p>
<pre><code class="language-go">func TestIsEven(t *testing.T) {
if !isEven(2) {
t.Fatal("2 is even")
}
if isEven(1) {
t.Fatal("1 is odd")
}
}
</code></pre>
<p>Run your test by using the <a href="https://golang.org/cmd/go/#hdr-Test_packages"><code>go test</code></a> command, for example</p>
<pre><code class="language-sh"># Test this directory
$ go test .
# Test the whole project recursively.
$ go test a-package.com/path/...
</code></pre>
<h3>Failing and messages</h3>
<p>Each test accepts one argument, a <code>T</code> instance. <code>T</code> provides methods that
allow to print information and control the flow of a test.</p>
<p>Use <code>t.Log</code> and <code>t.Logf</code> methods to write a message.</p>
<p>Use <code>t.Error</code> and <code>t.Errorf</code> methods to write a message and mark the test as
failed.</p>
<p>Use <code>t.Fatal</code> and <code>t.Fatalf</code> methods to write a message, mark the test as
failed and instantly terminate that test execution.</p>
<h4>Write good error messages</h4>
<p>A good error message is concise and short. Sprinkle each result with a bit of
context.</p>
<pre><code class="language-go">if isEven(1) {
t.Fatal("1 is an odd number")
}
if want, got := 42, compute(); want != got {
t.Fatalf("want %d, got %d", want, got)
}
</code></pre>
<p>By declaring <code>got</code> and <code>want</code> I am sure that what is tested for is what I
print. If the <code>compute</code> function was changed and in the new implementation <code>want</code>
should be <code>33</code> I cannot make the mistake of not updating the error message.
Both <code>got</code> and <code>want</code> are scoped to the <code>if</code> statement only.</p>
<p>When writing a table test, declaring an expected value might not be necessary.
The expected value can be easily found in the test declaration.</p>
<h3>Skipping a test</h3>
<p>Some tests should run only under special circumstances. For example, you want
to run a test only if a database is available. <code>t.Skip</code> and <code>t.Skipf</code> methods
allow to cancel (skip) the currently running test without failing it.</p>
<pre><code class="language-go">func TestDatabaseIntegration(t *testing.T) {
db, err := connectToDatabase("test-database")
if err != nil {
t.Skipf("cannot connect to database: %s", err)
}
defer db.Close()
// ...
}
</code></pre>
<h3>Test helpers</h3>
<p>Often times many tests require similar dependencies, for example running a
service or preparing a state. Instead of repeating the preparation code extract
each functionality to a separate function.</p>
<h3>Test helpers: Setting up dependencies</h3>
<p>If you are testing code that depends on an external database, this is how the
beginning of a test function might look like:</p>
<pre><code class="language-go">func TestDatabaseIntegration(t *testing.T) {
db, err := connectToDatabase("test-database")
if err != nil {
t.Skipf("cannot connect to database: %s", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
t.Fatalf("cannot ping database: %s", err)
}
for i, migration := range databaseMigrations {
if err := db.ApplyMigration(migration); err != nil {
t.Fatalf("cannot apply %d migration: %s", i, err)
}
}
mycollection := NewCollection(db)
// The actual test starts below.
// ...
}
</code></pre>
<p>A solution to code repetition can be to create a function that will encapsulate
certain functionality. The whole setup and teardown process for a test can be
extracted.</p>
<pre><code class="language-go">func TestDatabaseIntegration(t *testing.T) {
mycollection, cleanup := ensureMyCollection(t, "test-database")
defer cleanup()
// The actual test starts below.
// ...
}
func ensureMyCollection(t testing.TB), dbName string (MyCollection, func(){} {
t.Helper()
db, err := connectToDatabase(dbName)
if err != nil {
t.Skipf("cannot connect to database: %s", err)
}
if err := db.Ping(); err != nil {
db.Close()
t.Fatalf("cannot ping database: %s", err)
}
for i, migration := range databaseMigrations {
if err := db.ApplyMigration(migration); err != nil {
db.Close()
t.Fatalf("cannot apply %d migration: %s", i, err)
}
}
collection := NewCollection(db)
cleanup := func() {
db.Close()
}
return collection, cleanup
}
</code></pre>
<p>With the above solution, <code>ensureMyCollection</code> can be used by many test
functions to ensure that a collection using a database as a backend is
available. A helper function hides the for the test logic irrelevant part of
setting up an environment and ensuring all components are provided.</p>
<p>A helper function accepts <a href="https://golang.org/pkg/testing/#TB"><code>testing.TB</code></a>
interface instead of <code>t *testing.T</code>. That makes it useful for both test and
<a href="https://golang.org/pkg/testing/#hdr-Benchmarks">benchmark functions</a>.</p>
<p>A helper function does not return an error. Instead, it directly terminates the
test by calling <code>t.Fatal</code>.</p>
<p>At the beginning of the helper function the
<a href="https://golang.org/pkg/testing/#T.Helper"><code>t.Helper()</code></a> method is called. This
marks this function and when it fails the stack information and error will be
more helpful.</p>
<p><code>ensureMyCollection</code> returns a cleanup function. This is a convenient way of
cleaning up all created resources. The user of this helper must call it once
the returned resource is not needed anymore. The cleanup function should not
return anything nor fail the test.</p>
<h3>Blackbox package testing</h3>
<blockquote>
<p>Test files that declare a package with the suffix "_test" will be compiled as
a separate package, and then linked and run with the main test binary.
-- <a href="https://golang.org/cmd/go/#hdr-Test_packages">golang.org</a></p>
</blockquote>
<p>Test files for your package are located in the same directory as the code they
test. Your tests can belong to the same package as the rest of the code. It is
also possible to enforce a black-box test for your package. Your test files can
be in the same directory as your package code and use a different package name.</p>
<pre><code class="language-go">package xxx_test
</code></pre>
<p>Using a different test package name enforces that only the public interface of
the tested package is accessible. This is for example <a href="https://golang.org/src/strings/compare_test.go">how
<code>strings</code></a> and <a href="https://golang.org/src/bytes/reader_test.go"><code>bytes</code>
packages</a> are tested.</p>
<h3>Third party test helper packages</h3>
<p>I do not use any additional packages for testing. I am of an opinion that
<a href="https://golang.org/doc/faq#testing_framework">assert functions are not as helpful as one may
think</a>. Introducing an external
package requires learning a new API.</p>
<p>Someone else wrote <a href="https://web.archive.org/web/20210411084609/https://danmux.com/posts/the_cult_of_go_test/">a great summary</a> on the topic.</p>
<p>Complex comparisons can usually be done using
<a href="#reflectdeepequal"><code>reflect.DeepEqual</code></a> function.</p>
<h2><code>reflect.DeepEqual</code></h2>
<p>Those values that cannot be compared with <code>==</code>, most of the time can be
compared with <a href="https://golang.org/pkg/reflect/#DeepEqual"><code>reflect.DeepEqual</code></a>.</p>
<h2>Table tests</h2>
<p>When testing a functionality a single input is often not enough to ensure
correctness. Repeating the same operation for many cases can be implemented
using <a href="https://github.com/golang/go/wiki/TableDrivenTests">table tests</a>.</p>
<p>Use a map with strings as keys to provide a description of each test case.</p>
<pre><code class="language-go">func TestDiv(t *testing.T) {
cases := map[string]struct{
A int
B int
WantRes int
WantErr error
}{
"two positive numbers": {
A: 4,
B: 2,
WantRes: 2,
},
"divide by zero": {
A: 4,
B: 0,
WantErr: errors.ErrZeroDivision,
},
}
for testName, tc := range cases {
t.Run(testName, func(t *testing.T) {
res, err := Div(tc.A, tc.B)
if !errors.Is(err, tc.WantErr) {
t.Fatalf("unexpected error: %q", err)
}
if res != tc.WantRes {
t.Fatalf("unlexpected result: %d", res)
}
})
}
}
</code></pre>
<p>When declaring a test case, always use field names. This increases the
readability and you have to provide only non zero values.</p>
<pre><code class="language-go">cases := map[string]struct{
DB *Database
Req *Request
WantRes int
WantErr error
}{
// BAD
{nil, myrequest, 32, nil},
// GOOD
{
Req: myrequest,
WantRes: 32,
},
}
</code></pre>
<h2>Mocking</h2>
<p>Write your code to accept interfaces. Using interfaces allows you to test a
single layer of a functionality at a time.</p>
<p>For example, if you are writing an application that is storing data in an SQL
database, instead of accessing the database directly through a <code>*sql.DB</code>
instance <a href="../accessing-data-in-go/#mocking-for-tests">use a wrapper</a>. Using
a data access abstraction allows for mocking.</p>
<p>When writing a mock you do not have to implement all methods. For the compiler
it is enough to include the interface in the mock declaration. Implement only
methods that you intend to call.</p>
<pre><code class="language-go">type Collection interface {
One(id uint64) (*Entity, error)
List() ([]*Entity, error)
Add(Entity) error
Delete(id uint64) error
}
type CollectionMock struct {
Collection
Err error
}
func (c *CollectionMock) Add(Entity) error {
return c.Err
}
</code></pre>
<p><code>CollectionMock</code> implements the <code>Collection</code> interface, but using any other
method than <code>Add</code> will panic. See <a href="https://play.golang.org/p/GVc2tOJoAHX">the full
example</a>.</p>
<h3>Your code should provide a mock</h3>
<p>When writing a package that is used by others provide test implementations of
your interfaces.</p>
<p>This approach is taken by the standard library. For example,
<a href="https://golang.org/pkg/net/http/httptest/#ResponseRecorder"><code>httptest.ResponseRecorder</code></a>
allows to test your HTTP handler without using a real <code>http.ResponseWriter</code>.</p>
<h2>Test flags</h2>
<p>You can add your own flags to the <code>go test</code> command in order to customize your
tests. Use the <a href="https://golang.org/pkg/flag/"><code>flag</code></a> package and declare your
flags globally.</p>
<pre><code class="language-go">var dbFl = flag.String("db", "", "Use given database DSN.")
</code></pre>
<h2>Environment variables</h2>
<p>Instead of <code>flag</code> you can control your tests using environment variables. If
you follow the <a href="https://12factor.net/config">12 factor app</a> principles then
your application is already utilizing environment variables for the
configuration.</p>
<pre><code class="language-go">var dbDSN = os.Getenv("DATABASE_DSN")
</code></pre>
<h2>Fixtures</h2>
<p>If your test requires fixtures <code>/testdata</code> is the directory you should consider
keeping them in.</p>
<blockquote>
<p>The go tool will ignore a directory named "testdata", making it available to
hold ancillary data needed by the tests.
-- <a href="https://golang.org/cmd/go/#hdr-Test_packages">golang.org</a></p>
</blockquote>
<p>When running tests each test function is executed with its working directory
set to the source directory of the tested package. That means that when
accessing files in <code>/testdata</code> you can safely use relative path</p>
<pre><code class="language-go">fd, err := os.Open(filepath.Join("testdata", "some-fixture.json"))
</code></pre>
<h2>Golden files</h2>
<p><a href="https://softwareengineering.stackexchange.com/q/358786">Golden files</a> are a
great way to validate and keep track of a test output. Together with a version
control system they are much easier to maintain than strings hard coded in
functions.</p>
<pre><code class="language-go">var goldFl = flag.Bool("gold", false, "Write result to golden files instead of comparing with them.")
func TestExample(t *testing.T) {
// Test logic.
result := ...
if *goldFl {
writeGoldenFile(t, result)
}
compareWithGoldenFile(t, result)
}
</code></pre>
<p>This technique comes in very helpful combined with <a href="#table-tests">table tests</a>.</p>
<h2>Integration tests</h2>
<p>For a well written application <a href="https://en.wikipedia.org/wiki/Integration_testing">integration
testing</a> should not require
more work than usual testing. For each external resource provide a single
function to <a href="#test-helpers-setting-up-dependencies">setup and teardown the
resource</a>.</p>
<h2>Build constraints</h2>
<p>You can use a <a href="https://golang.org/pkg/go/build/#hdr-Build_Constraints">build
constraint</a> to
conditionally build code in a file.</p>
<pre><code class="language-sh">$ head -n 1 app_intergration_test.go
// +build integration
</code></pre>
<p>To run tests including those tagged as <code>integration</code> use <code>-tag</code> flag.</p>
<pre><code class="language-sh">$ go test -tag integration .
</code></pre>
<h2>Setup/teardown</h2>
<p>When using the <code>testing</code> package, it is possible to overwrite the <a href="https://golang.org/pkg/testing/#hdr-Main"><code>test main</code></a> function.</p>
<p>Using a custom test main function allows to execute code before and after
executing all discovered tests. This can be running an external dependency like
a database instance or building a binary that tested functionality might depend
on.</p>
<pre><code class="language-go">func TestMain(m *testing.M) {
// Setup code.
// defer Teardown code.
os.Exit(m.Run())
}
</code></pre>
<h2><code>-race</code></h2>
<p>Run tests with <code>-race</code> flag to enable data race detection.</p>
<p>This functionality is not available on <a href="https://www.musl-libc.org/">musl</a> based systems.</p>
<h2>Testing FAQ</h2>
<p>Check the <a href="https://golang.org/doc/faq#Packages_Testing">FAQ at golang.org</a>.</p>
Error handling.Piotr Husiatyńskihttps://ph.codeberg.page/2019-02-20-error_handling2019-02-20T00:00:00Z<p><em>In Go 1.13 <a href="https://golang.org/doc/go1.13#error_wrapping">error wrapping</a> was
introduced as part of the standard library. This post was written before the
update to the <code>errors</code> package.</em></p>
<p>Go is a language that does not provide exceptions. Instead, an operation can
return an error. <a href="https://blog.golang.org/errors-are-values">Errors are values</a>
that implement the <code>error</code> interface.</p>
<p>I have worked with several errors handling patterns over the years and I would
like to summarize my experience focusing on the good solutions.</p>
<p>For the purpose of this post, let us imagine a very simple banking application.
Accounts are represented by their numeric ID and we only know how much money
each account holds. No account balance can get below zero. A bank service must
implement the interface below.</p>
<pre><code class="language-go">type BankService interface {
// NewAccount registers a new account in this bank. The account is
// initialized with given funds.
NewAccount(accountID int64, funds uint64) error
// Transfer moves funds between two accounts. It fails if an operation
// would cause the balance of the source account to go below zero.
Transfer(from, to int64, amount uint64) error
}
</code></pre>
<p>To keep the examples short and simple an in-memory storage is used. Anything
more serious would use a database instead.</p>
<h2>Inline error creation</h2>
<p>It is a common thing to create errors using <code>errors.New</code> and <code>fmt.Errorf</code> as
they are needed. When an operation fails you can handle the failure by creating
an error instance and returning it. The created error should contain
information about the cause of the failure. With that in mind let us create the
first version of a banking service.</p>
<p>{{< highlight go "hl_lines=14 24 26 30" >}}
func NewBank() *Bank {
return &Bank{
accounts: make(map[int64]uint64),
}
}</p>
<p>type Bank struct {
accounts map[int64]uint64
}</p>
<p>// Create new account with given funds. Account ID must be unique.
func (b *Bank) NewAccount(accountID int64, funds uint64) error {
if _, ok := b.accounts[accountID]; ok {
return errors.New("account exists")
}
b.accounts[accountID] = funds
return nil
}</p>
<p>// Transfer moves funds from one account to another.
func (b *Bank) Transfer(from, to int64, amount uint64) error {
switch fromFunds, ok := b.accounts[from]; {
case !ok:
return fmt.Errorf("source account %d not found", from)
case fromFunds < amount:
return fmt.Errorf("cannot transfer %d from %d account: insufficient funds", amount, fromFunds)
}</p>
<pre><code>if _, ok := b.accounts[to]; !ok {
return fmt.Errorf("destination account %d not found", to)
}
b.accounts[from] -= amount
b.accounts[to] += amount
return nil
</code></pre>
<p>}
{{< /highlight >}}</p>
<p>Above code presents a common way of dealing with errors. If a failure cannot be
dealt with then return the error. If possible provide additional information,
for example, an account ID. This is often an acceptable solution but sometimes
it might not be good enough. As soon as we use the <code>Bank</code> instance the
shortcomings are more visible.</p>
<pre><code class="language-go">bank := NewBank()
// ...
if err := bank.Transfer(111, 222, 10); err != nil {
// Why did the transfer fail?
}
</code></pre>
<p>If the <code>Transfer</code> call returns an error it is not possible to learn about the
reason and distinguish different cases. As a human analyzing the text message,
we can tell what went wrong. If you want your code to react differently if one
of the accounts does not exist and do something else when there are not enough
funds on the source account then you have a problem.</p>
<h2>Predefined errors</h2>
<p>To provide more insights into the <code>Transfer</code> method failures one may declare
all expected errors upfront.</p>
<p>For each failure case declare a corresponding error instance. Compare an error
returned by the <code>Transfer</code> method with all error definitions it can return to
discover the cause.</p>
<p>{{< highlight go "hl_lines=9 11 15" >}}
// Transfer moves funds from one account to another.
// Upon failure returns one of
// ErrNoSourceAccount
// ErrNoDestinationAccount
// ErrInsufficientFunds
func (b *Bank) Transfer(from, to int64, amount uint64) error {
switch fromFunds, ok := b.accounts[from]; {
case !ok:
return ErrNoSourceAccount
case fromFunds < amount:
return ErrInsufficientFunds
}</p>
<pre><code>if _, ok := b.accounts[to]; !ok {
return ErrNoDestinationAccount
}
b.accounts[from] -= amount
b.accounts[to] += amount
return nil
</code></pre>
<p>}</p>
<p>var (
// ErrNoSourceAccount is returned when the source account does not
// exist.
ErrNoSourceAccount = errors.New("no source account")</p>
<pre><code>// ErrNoDestinationAccount is returned when the destination account
// does not exist.
ErrNoDestinationAccount = errors.New("no destination account")
// ErrInsufficientFunds is returned when a transfer cannot be completed
// because there are not enough funds on the source account.
ErrInsufficientFunds = errors.New("insufficient funds")
</code></pre>
<p>)
{{< /highlight >}}</p>
<p>This is similar to how the <a href="https://golang.org/pkg/io/#pkg-variables"><code>io</code></a>
package deals with errors.</p>
<p>Returning a different error instance for each error case allows us to handle
different failure cases accordingly. Test the returned error for being one of
the predefined instances.</p>
<pre><code class="language-go">bank := NewBank()
// ...
switch err := bank.Transfer(1, 2); err {
case nil:
println("money transferred")
case ErrNoSourceAccount:
panic("source account does not exist")
case ErrNoDestinationAccount:
panic("destination account does not exist")
case ErrInsufficientFunds:
panic("not enough money")
default:
panic("unexpected error")
}
</code></pre>
<p>This is in my opinion a step in the right direction but it is too verbose. This
patten requires too much code to be written. You can no longer create errors
when you need them. All failure cases and respective errors must be declared
upfront.</p>
<p>In addition, you are losing the context information that you were building
using <code>fmt.Errorf</code>. When returning <code>ErrInsufficientFunds</code> you no longer know
which account caused it. <code>fmt.Errorf</code> must no longer be used for the error
instance comparison to work.</p>
<h2>Error inheritance</h2>
<p>In Python - a language with exceptions and type inheritance - <a href="https://docs.python.org/3/library/exceptions.html#exception-hierarchy">exceptions form
a hierarchy</a>.
Because each error is an instance of a class belonging to that class hierarchy
each exception instance can contain a custom message and be captured by its
type or any type it inherits from.</p>
<p>This is how a banking service could be used if implemented in Python.</p>
<pre><code class="language-python">try:
bank.transfer(from, to, amount)
except ErrAccountNotFound as e:
print(e) # either source or destination account not found
except ErrInsufficientFunds:
print("not enough money")
except Exception:
print("unexpected condition")
</code></pre>
<p>Because in Python implementation both <code>ErrNoSourceAccount</code> and
<code>ErrNoDestinationAccount</code> would inherit from <code>ErrAccountNotFound</code>, both cases
can be handled with a single statement <code>except ErrAccountNotFound</code>.</p>
<p>When capturing an exception <code>e</code> refers to the exception instance containing the
detailed information that can be helpful during debugging or consumed by the
client. It can contain more information than just a human readable description.</p>
<h3><code>Causer</code> interface</h3>
<p>Inheritance is not a requirement to achieve the functionality provided by
Python exceptions. When considering an error it is enough if we are able to
tell what was the cause of it. This is not possible with errors created using
the standard library (<code>errors</code> or <code>fmt</code> packages). Instead of using the
standard library, we must create our own error implementation.</p>
<p>What is needed is an <code>Error</code> structure that implements the
<a href="https://golang.org/pkg/builtin/#error"><code>error</code></a> interface and a <code>Wrap</code>
function that will take an error together with an additional description.</p>
<pre><code class="language-go">// Wrap returns an error that is having given error set as the cause.
func Wrap(err error, description string, args ...interface{}) *Error {
return &Error{
parent: err,
desc: fmt.Sprintf(description, args...),
}
}
type Error struct {
// Parent error if any.
parent error
// This error description.
desc string
}
func (e *Error) Error() string {
if e.parent == nil {
return e.desc
}
return fmt.Sprintf("%s: %s", e.desc, e.parent)
}
</code></pre>
<p>In addition, it will provide a <code>Cause</code> method that will return the wrapped error
instance or <code>nil</code>.</p>
<pre><code class="language-go">// Cause returns the cause of this error or nil if this is the root cause
// error.
func (e *Error) Cause() error {
return e.parent
}
</code></pre>
<p>One more function is necessary for this to be complete. We must be able to
compare an error with another error or its cause. The <code>error</code> interface does
not provide <code>Cause</code> method so we must use type casting to determine if an error
instance implements the <code>causer</code> interface.</p>
<p>Instead of a function a method of the <code>Error</code> structure provides a nicer API.</p>
<pre><code class="language-go">
// Is returns true if given error or its cause is the same kind.
// If cause error provides Cause method then a comparison is made with all
// parents as well.
func (kind *Error) Is(err error) bool {
type causer interface {
Cause() error
}
for {
if err == kind {
return true
}
if e, ok := err.(causer); ok {
err = e.Cause()
} else {
return false
}
}
}
</code></pre>
<p>Let us test the <code>Error</code>. All errors are created using the <code>Wrap</code> function which
builds an error hierarchy. It is possible to attach additional information by
including it in the description string.</p>
<pre><code class="language-go">root := Wrap(nil, "root")
child1 := Wrap(root, "child one")
child2 := Wrap(root, "child two")
fmt.Println("child 1 is root", root.Is(child1))
// child 1 is root true
fmt.Println("child 2 is root", root.Is(child2))
// child 2 is root true
fmt.Println("root is child 1", child1.Is(root))
// root is child 1 false
fmt.Println("child 2 is child 1", child1.Is(child2))
// child 2 is child 1 false
inlinedErr := Wrap(child2, "current time: %s", time.Now())
fmt.Println("inlined child 2 is root", root.Is(inlinedErr))
// inlined child 2 is root true
fmt.Println("inlined child 2 is child 2", child2.Is(inlinedErr))
// inlined child 2 is child 2 true
fmt.Println("fmt error is root", root.Is(fmt.Errorf("fmt error")))
// fmt error is root false
</code></pre>
<p>Above <code>Error</code> implementation is a powerful solution to error handling. It is
easy to implement, does not require much code and it is portable without
creating an explicit dependency on the <code>causer</code> interface.</p>
<h2>Predefined errors with inheritance</h2>
<p>If an error implements the <code>causer</code> interface we can unwind it and retrieve the
previous error instance! This means that no matter how many times we will wrap
an error, as long as all layers implement the <code>causer</code> interface we can
retrieve the parent error instance.</p>
<p>Back to the <code>Bank.Transfer</code> example. All error instances were wrapped before
returning and provide all the details one may expect an error to provide.</p>
<p>{{< highlight go "hl_lines=4 6-7 11" >}}
func (b *Bank) Transfer(from, to int64, amount uint64) error {
switch fromFunds, ok := b.accounts[from]; {
case !ok:
return Wrap(ErrNoSourceAccount, "ID %d", from)
case fromFunds < amount:
return Wrap(ErrInsufficientFunds,
"cannot transfer %d from %d account", amount, fromFunds)
}</p>
<pre><code>if _, ok := b.accounts[to]; !ok {
return Wrap(ErrNoDestinationAccount, "ID %d", to)
}
b.accounts[from] -= amount
b.accounts[to] += amount
return nil
</code></pre>
<p>}</p>
<p>var (
// ErrAccountNotFound is return when an operation fails because the
// requested account does not exist.
ErrAccountNotFound = Wrap(nil, "account not found")</p>
<pre><code>// ErrNoSourceAccount is returned when the source account does not
// exist.
ErrNoSourceAccount = Wrap(ErrAccountNotFound, "no source")
// ErrNoDestinationAccount is returned when the destination account
// does not exist.
ErrNoDestinationAccount = Wrap(ErrAccountNotFound, "no destination")
// ErrInsufficientFunds is returned when a transfer cannot be completed
// because there are not enough funds on the source account.
ErrInsufficientFunds = Wrap(nil, "insufficient funds")
</code></pre>
<p>)
{{< /highlight >}}</p>
<p>Errors can be tested on any granularity level. It is valid to compare with the
high level <code>ErrAccountNotFound</code> or more precise <code>ErrNoSourceAccount</code>.</p>
<pre><code class="language-go">bank := NewBank()
// ...
switch err := bank.Transfer(1, 2, 100); {
case err == nil:
println("money transferred")
case ErrNoDestinationAccount.Is(err):
panic("destination account does not exist")
case ErrInsufficientFunds.Is(err):
panic("not enough money " + err.Error()) // err provides more details
default:
panic("unexpected error")
}
</code></pre>
<h2>Don't Drink Too Much Cool Aid</h2>
<p>What I have presented is a powerful pattern. You may use the <code>causer</code> interface
to extract attributes or custom error implementations that were wrapped,
attaching helpful information on each execution step. This might be great for
example during input validation, where together with an error you want to
return information about the invalid fields in a way that can be extracted
later.</p>
<p>You can use the <code>causer</code> interface and the <code>Wrap</code> function to declare a complex
tree of errors that are several layers deep and cover every possible case. If
you do, think again about your use case and if such granularity is helpful.
Usually, just a handful of errors declared upfront do the job better. I tend to
always inline error creation first and only if a case requires more attention
declare a previously inlined error.</p>
<p>Regardless of what you do try to avoid blindly importing any error package.
Consider your use cases and try to <a href="https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html">tailor your errors implementation to suit
your needs</a>.</p>
Cache stampede protection.Piotr Husiatyńskihttps://ph.codeberg.page/2018-09-14-cache_stampede_protection2018-09-14T00:00:00Z<p>Writing an application that handles concurrent traffic from a lot of clients in
a performant way is not an easy task. To narrow this problem to web
applications only, serving as many HTTP requests as possible in a short time is
often a challenge.</p>
<p>In most cases of an HTTP application, optimizing access to the database can be
the easiest and the best first step.</p>
<h2>Caching database access</h2>
<p>Using a database that provides plenty of functionality and storing data in
<a href="https://en.wikipedia.org/wiki/Denormalization">denormalized form</a> makes
development easier. This comes at the cost of the database having to execute
complex queries and do more computation in order to return a result.</p>
<p>Making several database queries, even simple ones, to handle a request adds up
and makes our request handing slower. Even if a query is executed instantly,
the database client must transfer the data over the network each time.</p>
<p>Most databases implement some kind of internal caching. They optimize access to
popular data if the query complexity allows to do so. Why add an external cache
layer in front of the database then?</p>
<p>A cache layer can be added to remember:</p>
<ol>
<li>the result of a <strong>heavy query</strong> that takes time and puts heavy load on the
database.</li>
<li>the result of a <strong>repeating query</strong> that causes the database to waste
resources on returning the same data all the time.</li>
</ol>
<h3>The fastest code is the code that never runs</h3>
<p>Imagine a very popular web application that displays the details of an <em>item</em>.
An <em>item</em> is an entity identifiable by a unique number, that is rarely
changing. Due to heavy traffic, the database that stores <em>items</em> is all the
time asked about the same entity.</p>
<p><img src="./statics/direct-access.svg" alt="Direct data access"></p>
<p>To offload some of the repeating requests, we introduce a cache layer.
Whenever an item is needed, serve it from the cache. The database is queried
only if an item does not exist in the cache.</p>
<p><img src="./statics/cached-access.svg" alt="Cached data access"></p>
<p>For the purpose of this post, let us assume we have a store and a cache
implementation available that implement the following interfaces. Delegating
<a href="./2018-09-02-accessing_data_in_go.html">database access</a> allows for a
cache implementation that is not tightly coupled to the original
implementation.</p>
<pre><code class="language-go">type ItemStore interface {
// FindItem item returns an item with given ID or ErrNotFound
FindItem(ctx context.Context, itemID int64) (*Item, error)
}
type CacheStore interface {
// Get loads value under given key into destValue. ErrMiss is returned
// if key does not exist.
Get(ctx context.Context, key string, destValue interface{}) error
// Set value of given key.
Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error
}
</code></pre>
<p>Instead of directly calling the database each time an <em>item</em> is needed, a cache
layer is used.</p>
<pre><code class="language-go">func CacheItemStore(cache CacheStore, store ItemStore) ItemStore {
return &cachedItemStore{
store: store,
cache: cache,
}
}
type cachedItemStore struct {
store ItemStore
cache CacheStore
}
func (c *cachedItemStore) FindItem(ctx context.Context, itemID int64) (*Item, error) {
cacheKey := fmt.Sprintf("item:%d", itemID)
var item Item
switch err := c.cache.Get(ctx, cacheKey, &item); err {
case nil:
return &item, nil
case ErrMiss:
// Not in cache, fetch from the database.
default:
// Cache error is not critical for providing this
// functionality, log it and continue.
log.Printf("cannot get from cache: %s", err)
}
item, err := c.store.FindItem(ctx, itemID)
// To simplify this example, do not cache ErrNotFound. Depending on the use
// case, remembering that an item does not exist might be desired.
if err == nil {
if err := c.cache.Set(ctx, cacheKey, &item, time.Minute); err != nil {
// Cache error is not critical for providing this
// functionality, log it and continue.
log.Printf("cannot set in cache: %s", err)
}
}
return item, err
}
</code></pre>
<p>Cache errors are not critical for the functionality of the <code>FindItem</code> method.
They are logged, so that we have a good insight into our application.</p>
<p>To decide for how long a result can be cached and when the value must be
refreshed, a domain knowledge is required.</p>
<h2>Cache stampede problem</h2>
<p>Adding a cache layer can reduce the amount of calls to a resource that the
cache protects. Whether the resource is a database, an external service or a
local computation task, the amount of communication that happens can be
significantly reduced.</p>
<blockquote>
<p>There are two hard things in computer science: cache invalidation, naming
things, and off-by-one errors.</p>
<p>-- <a href="https://twitter.com/codinghorror/status/506010907021828096">Jeff Atwood</a></p>
</blockquote>
<p>Most cache implementations store data with an expiration time, after which it
is removed. Using <a href="https://en.wikipedia.org/wiki/Time_to_live">time to live
(TTL)</a> is an easy compromise to
ensure that stored data is never too old. Instead of trying to keep track of
when a certain query result is changing, remember the result for a short period
to minimize the possible errors.</p>
<p>Cache expiration introduces a new problem. Take our example of a web
application all the time displaying an item with ID 1. Hundreds of requests per
second and all of them require this item's details to be served. When the item
with ID 1 is served from a cache, the database can allocate resources to do
something else.</p>
<p><img src="./statics/cached-access.svg" alt="Cached data access"></p>
<p>Our cache is using TTL to ensure that served data is never too old. <code>FindItem</code>
will cache the result for one minute. After one minute, the value expires (it
is being removed from the cache) to force a refresh.</p>
<p>Keep in mind, that there are hundreds of requests happening every second. All
of them need the item with ID 1 to be served. The item is not in the cache
anymore, so the only place to get it is the database. This problem is called
<a href="https://en.wikipedia.org/wiki/Cache_stampede">cache stampede</a>.</p>
<p><img src="./statics/cached-access-expired.svg" alt="Cached data access with an empty cache"></p>
<p>The database is not being shielded by an external cache layer anymore. It also
does not have its own cache ready.</p>
<h3>Access Locking</h3>
<p>To prevent the same query being executed multiple times when a value is not
cached, we can introduce a locking mechanism. Before asking the database,
acquire a "query lock".</p>
<p>Our lock will be implemented using a cache service. In addition to <code>Get</code> and
<code>Set</code> operations, <code>SetNx</code> is required.</p>
<pre><code class="language-go">type CacheStore interface {
Get(ctx context.Context, key string, destValue interface{}) error
Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error
// SetNx sets the value of a given key only if it does not exist.
// Returns ErrConflict if the key is already in use.
SetNx(ctx context.Context, key string, value interface{}, ttl time.Duration) error
}
</code></pre>
<p>When <code>FindItem</code> is called, we first try to read the item from the cache. If the
item does not exist in the cache, we either acquire a lock and get data from
the database or keep checking the cache. A value will be cached by another
client or we will get the lock.</p>
<pre><code class="language-go">func (c *cachedItemStore) FindItem(ctx context.Context, itemID int64) (*Item, error) {
cacheKey := fmt.Sprintf("item:%d", itemID)
readFromCache:
for {
var item Item
switch err := c.cache.Get(ctx, cacheKey, &item); err {
case nil:
return &item, nil
case ErrMiss:
// Not in cache, fetch from the database, but only of
// no other client is already doing this.
cacheKeyLock := cacheKey + ":query-lock"
switch err := c.cache.SetNx(ctx, cacheKeyLock, 1, time.Second); err {
case nil:
// We own the lock, ask the database about the value.
break readFromCache
case ErrConflict:
// Another process owns the lock. Wait until
// the value is stored in the cache or the lock
// is released and we can query the database.
//
// Short sleep ensures that we do not overuse
// the cache.
time.Sleep(25 * time.Millisecond)
continue readFromCache
default:
log.Printf("cannot acquire lock in cache: %s", err)
break readFromCache
}
default:
// Cache error is not critical for providing this
// functionality. Log it and continue.
log.Printf("cannot get from cache: %s", err)
break readFromCache
}
}
item, err := c.store.FindItem(ctx, itemID)
if err == nil {
if err := c.cache.Set(ctx, cacheKey, &item, time.Minute); err != nil {
log.Printf("cannot set in cache: %s", err)
}
}
return item, err
}
</code></pre>
<p>Above implementation ensures that at most one client is asking the database
about an item with the same ID. If more than one <code>FindItem</code> call is done at the
same time, only one client will query the database while all others are waiting
for the cached result.</p>
<h3>Early expiration</h3>
<p>The situation has improved for the database. But adding locking means that when
a value expires from the cache, all clients must wait until one of them fills
the cache. All clients waste time and server resources on waiting.</p>
<p>Our cache layer can be further improved by adding an early expiration
functionality. If an <em>item</em> is cached for 1 minute, shortly before the
expiration time is due, tell one of the clients that the value must be updated.</p>
<pre><code class="language-go">func (c *cachedItemStore) FindItem(ctx context.Context, itemID int64) (*Item, error) {
cacheKey := fmt.Sprintf("item:%d", itemID)
cacheKeyLock := cacheKey + ":query-lock"
readFromCache:
for {
var spItem stampedeProtectedItem
switch err := c.cache.Get(ctx, cacheKey, &spItem); err {
case nil:
// If an early expiration time is due, acquire lock to
// fetch item from the database.
if spItem.refreshAt.Before(time.Now()) {
if c.cache.SetNx(ctx, cacheKeyLock, 1, time.Second) == nil {
break readFromCache
}
// If we did not get the lock, we can still
// return the cached data. It will expire soon,
// but it's still valid.
}
return &spItem.item, nil
case ErrMiss:
// Not in cache, fetch from the database, but only of
// no other client is already doing this.
switch err := c.cache.SetNx(ctx, cacheKeyLock, 1, time.Second); err {
case nil:
// We own the lock, ask the database about the value
break readFromCache
case ErrConflict:
// Another process owns the lock. Wait until
// the value is stored in the cache or the lock
// is released and we can query the database.
//
// Short sleep ensures that we do not overuse
// the cache.
time.Sleep(25 * time.Millisecond)
continue readFromCache
default:
log.Printf("cannot acquire lock in cache: %s", err)
break readFromCache
}
default:
log.Printf("cannot get from cache: %s", err)
break readFromCache
}
}
item, err := c.store.FindItem(ctx, itemID)
if err == nil {
spItem := stampedeProtectedItem{
refreshAt: time.Now().Add(55 * time.Second),
item: item,
}
if err := c.cache.Set(ctx, cacheKey, &spItem, time.Minute); err != nil {
log.Printf("cannot set in cache: %s", err)
}
}
return item, err
}
type stampedeProtectedItem struct {
refreshAt time.Time
item *Item
}
</code></pre>
<h2>Conclusion</h2>
<p>Caching data that is often read can increase performance of an application.
Data caching is more complicated than it might look like at first sight.</p>
<p>Above cache stampede protection code example is tightly coupled to <code>ItemStore</code>.</p>
Accessing data in go.Piotr Husiatyńskihttps://ph.codeberg.page/2018-09-02-accessing_data_in_go2018-09-02T00:00:00Z<p>When writing a web application, we have to decide how to access data. Where to
get it from, how to store it, how to manipulate it. Storage engines can vary,
from being a single SQLite file to cache server or even an external service
exposing an API.</p>
<p>There are many ways this topic can be addressed. I will explain how a simple
and straightforward solution can be evolved into a more sophisticated one.</p>
<p>For the purpose of this article, let's assume that our storage engine is an SQL
database with an <code>items</code> table. Our task is to build an endpoint, which returns
a list of all <em>items</em> in the database. <em>Item</em> is an entity with a name and an
ID. It can be represented by the structure below.</p>
<pre><code class="language-go">type Item struct {
ID int64
Name string
}
</code></pre>
<h2>First iteration</h2>
<p>Let's start with a basic HTTP handler. To avoid global variables, let's use
dependency injection. <code>ItemListHandler</code> takes as a parameter what's necessary
for the endpoint to complete our task -- a database connection and a template.
In return we are getting an HTTP handler function.</p>
<pre><code class="language-go">func ItemListHandler(
db *sql.DB,
tmpl *template.Template,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// handler's code below
}
}
</code></pre>
<p>To list all <em>items</em>, we must first query the database. Once we will read all
returned rows, we can use the collected entries to render the template and send
the result back.</p>
<pre><code class="language-go">rows, err := db.QueryContext(r.Context(), `SELECT id, name FROM items`)
if err != nil {
http.Error(w, "Server Error", http.StatusInternalServerError)
return
}
defer rows.Close()
var items []*Item
for rows.Next() {
var it Item
if err := rows.Scan(&it.ID, &it.Name); err != nil {
http.Error(w, "Server Error", http.StatusInternalServerError)
return
}
items = append(items, &it)
}
if err := rows.Err(); err != nil {
http.Error(w, "Server Error", http.StatusInternalServerError)
return
}
_ = tmpl.Execute(w, items)
</code></pre>
<p><em>(To simplify the example, returned error pages are very basic, we do not log
errors and we are assuming that template rendering never fails.)</em></p>
<p>There are many issues with the approach presented above.</p>
<ol>
<li>
<p>Every time we want to get the list of <em>items</em>, we must directly interact
with the database. We must know about the database structure and in case of
schema changes, we must locate all those places and update them.</p>
</li>
<li>
<p>Everything is implemented in a single place. Because we directly access the
database, to test this code, a database must be available, it's schema
prepared and test data inserted.</p>
</li>
<li>
<p>If we wanted to add a cache layer or some form of monitoring like tracing or
metrics, we would have to add more code directly inside of the handler.
That makes the code of the handler larger and testing harder. We can no
longer test functionalities separately.</p>
</li>
</ol>
<h2>Second iteration</h2>
<p>Instead of writing all the code in an HTTP handler, let's extract a part of it
as a function. We can encapsulate fetching items and hide the database
connection from the user.</p>
<p>The same code that was written directly inside of the handler is now provided
by the <code>ListItems</code> method.</p>
<pre><code class="language-go">// NewItemStore returns a store for items.
func NewItemStore(db *sql.DB) *ItemStore {
return &ItemStore{db: db}
}
type ItemStore struct {
db *sql.DB
}
// ListItems returns all stored items.
func (is *ItemStore) ListItems(ctx context.Context) ([]*Item, error) {
rows, err := db.QueryContext(ctx, `SELECT id, name FROM items`)
if err != nil {
return nil, fmt.Errorf("cannot select items: %s", err)
}
defer rows.Close()
var items []*Item
for rows.Next() {
var it Item
if err := rows.Scan(&it.ID, &it.Name); err != nil {
return nil, fmt.Errorf("cannot scan item: %s", err)
}
items = append(items, &it)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("scanner: %s", err)
}
return items, nil
}
</code></pre>
<p>Having such a <em>store</em> available, we no longer have to directly query the
database in our handler. Instead of accepting <code>*sql.DB</code> as an argument,
<code>ItemListHandler</code> can now take <code>*ItemStore</code>. Handler's body can be simplified
to just a few lines.</p>
<pre><code class="language-go">func ItemListHandler(
itemStore *ItemStore,
tmpl *template.Template,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
items, err := itemStore.ListItems(r.Context())
if err != nil {
http.Error(w, "Server Error", http.StatusInternalServerError)
}
_ = tmpl.Execute(w, items)
}
}
</code></pre>
<p>Having this handler, we no longer have to track changes to the database schema.
All details of accessing <em>item</em> data are now in <code>ItemStore</code>. If you need to
create or update an <em>item</em>, add <code>CreateItem</code> and <code>UpdateItem</code> methods.</p>
<h2>Third iteration</h2>
<p>Using <code>*ItemStore</code> for accessing <em>items</em> solved the first issue. Listing items
is now an easy task that takes only a few lines of code.</p>
<p>The last change is to use an interface instead of accepting a structure
pointer. Let's call our interface <code>ItemStore</code>. The previous implementation
using an SQL database is renamed to <code>sqlItemStore</code>.</p>
<pre><code class="language-go">type ItemStore interface {
ListItems(context.Context) ([]*Item, error)
}
// NewItemStore returns a store for items that is using an SQL database
// as a storage engine.
func NewSQLItemStore(db *sql.DB) ItemStore {
return &sqlItemStore{db: db}
}
type sqlItemStore struct {
db *sql.DB
}
func (s *sqlItemStore) ListItems(ctx context.Context) ([]*Item, error) {
// ...
}
func ItemListHandler(
itemStore ItemStore,
tmpl *template.Template,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ...
}
}
</code></pre>
<p>Defining interfaces together with the implementation might feel
counterintuitive in Go. In most cases, it is better to declare an interface
where it is used (not where it is implemented) to help to decouple
functionalities and avoid dependencies.</p>
<p>In this case we do not use an interface to encourage different <code>ItemStore</code>
implementations. Code that is used for accessing <em>items</em> could be put in it's
own package and provide all necessary functionality -- an interface, the main
implementation using an SQL database, a mock implementation for testing and more.</p>
<h3>Mocking for tests</h3>
<p>The <code>sqlItemStore</code> implementation is easy to test independently from any HTTP
handler that is using it. Any handler that is using an <code>ItemStore</code> should also
be testable without the need for any particular <code>ItemStore</code> implementation.</p>
<p>When testing handlers, instead of providing a real <code>ItemStore</code> implementation,
we can use a mock.</p>
<pre><code class="language-go">type ItemStoreMock struct {
Items []*Item
Err error
}
// ensure mock always implements the ItemStore
var _ ItemStore = (*ItemStoreMock)(nil)
func (mock *ItemStoreMock) ListItems(context.Context) ([]*Item, error) {
return mock.Items, mock.Err
}
</code></pre>
<p><code>ItemStoreMock</code> gives us full control over its API. We control what each
method returns, which means we are able to test all cases we want.</p>
<h3>Caching</h3>
<p>Using an interface, allows us to wrap a store with additional functionality.
For example, we can provide a cache layer, that will be invisible to the user.
It can be added or removed without any changes to handler or store
implementations.</p>
<pre><code class="language-go">type CacheStore interface {
// Get loads value under given key into destValue. ErrMiss is returned
// if key does not exist.
Get(ctx context.Context, key string, destValue interface{}) error
// Set value of given key.
Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error
}
func CacheItemStore(cache CacheStore, store ItemStore) ItemStore {
return &cachedItemStore{
cache: cache,
store: store,
ttl: 5 * time.Minute,
}
}
type cachedItemStore struct {
cache CacheStore
store ItemStore
ttl time.Duration
}
func (c *cachedItemStore) ListItems(context.Context) ([]*Item, error) {
var items []*Item
switch err := c.cache.Get(ctx, "items:all", &items); err {
case nil:
return items, nil
case ErrMiss:
// all good, just not in the cache
default:
// log the error and continue
}
items, err := c.store.ListItems(ctx)
if err == nil {
if err := c.cache.Set(ctx, "items:all", items, c.ttl); err != nil {
// log the error and continue
}
}
return items, err
}
</code></pre>
<p>Testing of the <code>cachedItemStore</code> can be done using <code>ItemStoreMock</code> and an
in-memory cache backend.</p>
<h2>Conclusion</h2>
<p>Writing data managers requires more effort, but allows to separate business
logic from storage implementation. Separation of concerns gives us more control
over data.</p>
<p>Thanks to using Go interfaces, we can mock and extend functionality of the
storage implementation. Integration with cache or monitoring tools is easy,
pluggable and can be tested separately.</p>