You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

95 lines
3.0 KiB
Markdown

---
draft: false
title: "Data Interfaces in Go"
aliases: ["Data Interfaces in Go"]
series: []
author: "Nick Dumas"
cover: ""
description: "Playing with interfaces"
showFullContent: false
tags:
- genesis
- golang
- procedural-generation
date: "2019-02-06"
---
# interfaces
I'm a fan of Go's interfaces. They're really simple and don't require a lot of legwork.
```go
type Mover interface {
func Move(x, y int) (int, int)
}
type Dog struct {
Name string
}
func (d Dog) Move(x, y int) (int, int) {
return x, y
}
```
Dog is now a Mover! No need for keywords like `implements`. The compiler just checks at the various boundaries in your app, like struct definitions and function signatures.
```go
type Map struct {
Actors []Mover
}
func something(m Mover, x,y int) bool {
// do something
}
```
# Functionality vs Data
This is where things get tricky. Interfaces describe *functionality*. What if you want the compiler to enforce the existence of specific members of a struct? I encountered this problem in a project of mine recently and I'll use it as a case study for a few possible solutions.
## Concrete Types
If your only expectation is that the compiler enforce the existence of specific struct members, specifying a concrete type works nicely.
```golang
type Issue struct {
Key string
Title string
Created time.Time
Updated time.Time
Body string
Attrs map[string][]string
}
type IssueService interface {
Get() []Issue
}
```
There's a few benefits to this. Because Go will automatically zero out all members of a struct on initialization, one only has to fill in what you explicitly want or need to provide. For example, the `Issue` type may represent a Jira ticket, or a Gitlab ticket, or possibly something as simple as lines in a TODO.txt file in a project's root directory.
In the context of this project, the user provides their own functions which "process" these issues. By virtue of being responsible for both the production and consumption of issues, the user/caller doesn't have to worry about mysteriously unpopulated data causing issues.
Where this falls apart, though, is when you want to allow for flexible/arbitrary implementations of functionality while still enforcing presence of "data" members.
## Composition
I think the correct solution involves breaking your struct apart: you have the `Data` and the `Thinger` that needs the data. Instead of making `Issue` itself have methods, `Issue` can have a member that satisfies a given interface. This seems to offer the best of both worlds. You don't lose type safety, while allowing consumers of your API to plug-and-play their own concrete implementations of functionality while still respecting your requirements.
```go
type Issue struct {
Key string
Title string
Created time.Time
Updated time.Time
Body string
Checker IssueChecker
Attrs map[string][]string
}
type IssueChecker interface {
Check(Issue) bool
}
```