mirror of
https://github.com/Pungyeon/clean-go-article.git
synced 2024-11-23 14:14:05 +00:00
Edit everything up until empty interface
This commit is contained in:
parent
f5e46d1349
commit
0616cf6ae5
66
README.md
66
README.md
|
@ -948,19 +948,19 @@ func AddName(user *User, name string) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This is *okay* because the variable scope, which is defined by whoever invokes the function, remains the same after the function returns. Combined with the fact that the ownership of the variable remains unchanged (it stays solely with the function invoker), this means that the pointer cannot be manipulated in an unexpected manner.
|
This is *okay* because the variable scope, which is defined by whoever invokes the function, remains the same after the function returns. Combined with the fact that the function invoker remains the sole owner of the variable, this means that the pointer cannot be manipulated in an unexpected manner.
|
||||||
|
|
||||||
### Closures are Function Pointers
|
### Closures are Function Pointers
|
||||||
|
|
||||||
So, before we go to the next topic of using interfaces in Go. I would like to introduce the commonly overseen alternative, which is what C programmers know as 'function pointers' and most other programmers refer to as 'closures'. Closure are quite simple. They are an input parameter for a function, which act like any other parameter, except for the fact that they are a function. In Javascript, it is very common to use closures as callbacks, which is typically used in scenarios where upon we want to invoke a function after an asynchronous operation has finished. In Go, we don't really have this issue, or at the very least, we have other, much nicer, ways of solving this issue. Instead, in Go, we can use closures to solve a different hurdle: The lack of generics.
|
Before we get into the next topic of using interfaces in Go, I would like to introduce a common alternative. It's what C programmers know as "function pointers" and what most other programming languages call <strong>closures</strong>. A closure is simply an input parameter like any other, except it represents a function. In JavaScript, it's quite common to use closures as callbacks, which are just functions that are invoked after some asynchronous operation has finished. In Go, we don't really have this notion. We can, however, use closures to partially overcome a different hurdle: The lack of generics.
|
||||||
|
|
||||||
Now, don't get too excited. We aren't going to substitute the lack of generics. We are simply going to solve a subset of the lack of generics with the use of closures. Consider the following function signature:
|
Consider the following function signature:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func something(closure func(float64) float64) float64 { ... }
|
func something(closure func(float64) float64) float64 { ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
This function takes another function as input and will return a `float64`. The input function, will take a `float64` as input, and will also return a `float64`. This pattern can be particularly useful, for creating a loosely coupled architecture, making it easier to to add functionality, without affecting other parts of the code. An example use case of this, could be for a struct containing data, which we want to manipulate in some form. Through this structures `Do()` method, we can perform operations on this data. If we know the operation ahead of time, we can approach problem this by placing the logic for handling the different operations, directly in our `Do()` method:
|
This function takes another function (a closure) as input and returns a `float64`. The input function takes a `float64` as input and also returns a `float64`. This pattern can be particularly useful for creating a loosely coupled architecture, making it easier to to add functionality without affecting other parts of the code. Suppose we have a struct containing data that we want to manipulate in some form. Through this structure's `Do()` method, we can perform operations on that data. If we know the operation ahead of time, we can obviously handle that logic directly in our `Do()` method:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (datastore *Datastore) Do(operation Operation, data []byte) error {
|
func (datastore *Datastore) Do(operation Operation, data []byte) error {
|
||||||
|
@ -975,9 +975,9 @@ func (datastore *Datastore) Do(operation Operation, data []byte) error {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
As we can imagine, this function will perform a predetermined operation on the data contained in the `Datastore` struct. However, we can also imagine, that at some point we would want to add more operations. Over a longer period of time, this might end up being quite a lot of different operations, making our `Do` method bloated and possibly even hard to maintain. It might also be an issue for people wanting to use our `Datastore` object, who don't have access to edit our package code. Keeping in mind, that there is no way of extending structure methods as there is in most OOP languages. This could also become an issue for developers wanting to use our package.
|
But as you can imagine, this function is quite rigid—it performs a predetermined operation on the data contained in the `Datastore` struct. If at some point we would like to introduce more operations, we'd end up bloating our `Do` method with quite a lot of irrelevant logic that would be hard to maintain. The function would have to always care about what operation it's performing and to cycle through a number of nested options for each operation. It might also be an issue for developers wanting to use our `Datastore` object who don't have access to edit our package code, since there is no way of extending structure methods in Go as there is in most OOP languages.
|
||||||
|
|
||||||
So instead, let's try a different approach, using closures instead:
|
So instead, let's try a different approach using closures:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (datastore *Datastore) Do(operation func(data []byte, data []byte) ([]byte, error), data []byte) error {
|
func (datastore *Datastore) Do(operation func(data []byte, data []byte) ([]byte, error), data []byte) error {
|
||||||
|
@ -1000,9 +1000,9 @@ func main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
However, other than this being a very messy function signature, we also have another issue with this. This function isn't particularly generic. What happens, if we find out that we actually want the `concat` function needs to be able to take multiple byte arrays as input? Or if want to add some completely new functionality, that may also need more or less input values than `(data []byte, data []byte)` ?
|
You'll notice immediately that the function signature for `Do` ends up being quite messy. We also have another issue: The closure isn't particularly generic. What happens if we find out that we actually want the `concat` to be able to take more than just two byte arrays as input? Or if we want to add some completely new functionality that may also need more or fewer input values than `(data []byte, data []byte)`?
|
||||||
|
|
||||||
One way to solve this issue, is to change our concat function. In the example below, I have changed it to only take a single byte array as input argument, but it could just as well have been the opposite case.
|
One way to solve this issue is to change our `concat` function. In the example below, I have changed it to only take a single byte array as an input argument, but it could just as well have been the opposite case:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func concat(data []byte) func(data []byte) ([]byte, error) {
|
func concat(data []byte) func(data []byte) ([]byte, error) {
|
||||||
|
@ -1027,24 +1027,24 @@ func main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice how we have added some of the clutter from the `Do` method signature. The way that we have accomplished this, is by having our `concat` function return a function. Within the returned function, we are storing the input values originally passed in to our `concat` function. The returned function can therefore now take a single input parameter, and within our function logic, we will append it, with our original input value. As a newly introduced concept, this is quite strange, however, getting used to having this as an option can indeed help loosen up program coupling and help get rid of bloated functions.
|
Notice how we've effectively moved some of the clutter out of the `Do` method signature and into the `concat` method signature. Here, the `concat` function returns yet another function. Within the returned function, we store the input values originally passed in to our `concat` function. The returned function can therefore now take a single input parameter; within our function logic, we will append it to our original input value. As a newly introduced concept, this may seem quite strange. However, getting used to having this as an option can help loosen up logic coupling and help get rid of bloated functions.
|
||||||
|
|
||||||
In the next section, we will talk about interfaces, but let's take a short moment to talk about the difference between interfaces and closures. The problems that interfaces solve, definitely overlap with the problems solved by closures. The implementation of interfaces in Go makes the distinction of when to use one or the other, somewhat difficult at times. Usually, whether an interface or a closure is used, is not really of importance and whichever solves the problem in the simplest manner, is the right choice. Typically, closures will be simpler to implement, if the operation is simple by nature. However, as soon as the logic contained within a closure becomes complex, one should strongly consider using an interface instead.
|
In the next section, we'll get into interfaces. Before we do so, let's take a short moment to discuss the difference between interfaces and closures. First, it's worth noting that interfaces and closures definitely solve some common problems. However, the way that interfaces are implemented in Go can sometimes make it tricky to decide whether to use interfaces or closures for a particular problem. Usually, whether an interface or a closure is used isn't really of importance; the right choice is whichever one solves the problem at hand. Typically, closures will be simpler to implement if the operation is simple by nature. However, as soon as the logic contained within a closure becomes complex, one should strongly consider using an interface instead.
|
||||||
|
|
||||||
Dave Cheney has an excellent write up on this topic, and a talk on the same topic:
|
Dave Cheney has an excellent write-up on this topic, as well as a talk:
|
||||||
|
|
||||||
* https://dave.cheney.net/2016/11/13/do-not-fear-first-class-functions
|
* https://dave.cheney.net/2016/11/13/do-not-fear-first-class-functions
|
||||||
* https://www.youtube.com/watch?v=5buaPyJ0XeQ&t=9s
|
* https://www.youtube.com/watch?v=5buaPyJ0XeQ&t=9s
|
||||||
|
|
||||||
Jon Bodner also has a talk about this topic
|
Jon Bodner also has a related talk:
|
||||||
|
|
||||||
* https://www.youtube.com/watch?v=5IKcPMJXkKs
|
* https://www.youtube.com/watch?v=5IKcPMJXkKs
|
||||||
|
|
||||||
### Interfaces in Go
|
### Interfaces in Go
|
||||||
|
|
||||||
In general, the go method for handling `interface`'s is quite different from other languages. Interfaces aren't explicitly implemented, like they would be in Java or C#, but are implicitly implemented if they fulfill the contract of the interface. As an example, this means that any `struct` which has an `Error()` method, implements / fulfills the `Error` interface and can be returned as an `error`. This has it's advantages, as it makes Go feel more fast-paced and dynamic, as interface implementation is extremely easy. There are obviously also disadvantages with this approach to implementing interfaces. As the interface implementation is no longer explicit, it can be difficult to see which interfaces are implemented by a struct. Therefore, the most common way of defining interfaces, is by writing interfaces with as few methods a possible. This way, it will be easier to understand whether or not a struct fulfills the contract of an interface.
|
In general, Go's approach to handling `interface`s is quite different from those of other languages. Interfaces aren't explicitly implemented like they would be in Java or C#; rather, they are implicitly created if they fulfill the contract of the interface. As an example, this means that any `struct` that has an `Error()` method implements (or "satisfies") the `Error` interface and can be returned as an `error`. This manner of implementing interfaces is extremely easy and makes Go feel more fast paced and dynamic. However, there are certainly disadvantages with this approach. As the interface implementation is no longer explicit, it can be difficult to see which interfaces are implemented by a struct. Therefore, it's common to define interfaces with as few methods as possible; this makes it easier to understand whether a particular struct fulfills the contract of the interface.
|
||||||
|
|
||||||
There are other ways of keeping track of whether your structs are fulfilling the interface contract. One method, is to create constructors, which return an interface, rather than the concrete type:
|
An alternative is to create constructors that return an interface rather than the concrete type:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Writer interface {
|
type Writer interface {
|
||||||
|
@ -1063,13 +1063,13 @@ func NewNullWriter() io.Writer {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The above function ensures, that the `NullWriter` struct implements the `Writer` interface. If we were to delete the `Write` method for the `NullWriter` we would get a compilation error, were we to try and build the solution. This is a good way of ensuring our code behaves in the way that we expect and that we can use the compiler as a safety net to ensure that we aren't producing invalid code.
|
The above function ensures that the `NullWriter` struct implements the `Writer` interface. If we were to delete the `Write` method from `NullWriter`, we would get a compilation error. This is a good way of ensuring that our code behaves as expected and that we can rely on the compiler as a safety net in case we try to write invalid code.
|
||||||
|
|
||||||
There is another way of trying to be more explicit about which interfaces a given struct implements. However, this method achieves the opposite of what we wish to achieve. The method being, using embedded interfaces, as a struct property.
|
There's yet another method of trying to be more explicit about which interfaces a given struct implements. However, this third method actually achieves the opposite of what we want. It involves using embedded interfaces as a struct property.
|
||||||
|
|
||||||
> <em>Wait what? – Presumably most people</em>
|
> <em>Wait what? – Presumably most people</em>
|
||||||
|
|
||||||
So, let's rewind a little, before we dive deep into the forbidden forest of smelly Go. In Go, we can use embedded structs, as a type of inheritance in our struct definitions. This is really nice as we can decouple our code, by defining reusable structs.
|
Let's rewind a bit before we dive deep into the forbidden forest of smelly Go. In Go, we can use embedded structs as a type of inheritance in our struct definitions. This is really nice, as we can decouple our code by defining reusable structs.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
|
@ -1089,9 +1089,9 @@ type AudioFile struct {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Above, we are defining a `Metadata` object, which will provide us with property fields that we are likely to use on many different struct types. The neat thing about using the embedded struct, rather than explicitly defining the properties directly in our struct, is that it has decoupled the `Metadata` fields. Should be choose to update our `Metadata` object, we can change it in a single place. As mentioned earlier, we want to ensure that a change one place in our code, doesn't break other parts of our code. Keeping these properties centralised, will keep it clear to users that a structures with embedded `Metadata` have the same properties. Much like, structures that fulfill interfaces, have the same methods.
|
Above, we are defining a `Metadata` object that will provide us with property fields that we are likely to use on many different struct types. The neat thing about using the embedded struct, rather than explicitly defining the properties directly in our struct, is that it has decoupled the `Metadata` fields. Should we choose to update our `Metadata` object, we can change it in just a single place. As we've seen several times so far, we want to ensure that a change in one place in our code doesn't break other parts. Keeping these properties centralised makes it clear that structures with an embedded `Metadata` have the same properties—much like how structures that fulfill interfaces have the same methods.
|
||||||
|
|
||||||
Now, let's look at an example of how we can use a constructor, to further prevent breaking our code, when making changes to our `Metadata` struct:
|
Now, let's look at an example of how we can use a constructor to further prevent breaking our code when making changes to our `Metadata` struct:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func NewMetadata(user types.User) Metadata {
|
func NewMetadata(user types.User) Metadata {
|
||||||
|
@ -1109,7 +1109,7 @@ func NewDocument(title string, body string) Document {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
At a later point in time, we find out, that we would also like a `CreatedAt` field on our `Metadata` object. This is now easily achievable, by simply updating our `NewMetadata` constructor:
|
Suppose that at a later point in time, we decide that we'd also like a `CreatedAt` field on our `Metadata` object. We can now easily achieve this by simply updating our `NewMetadata` constructor:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func NewMetadata(user types.User) Metadata {
|
func NewMetadata(user types.User) Metadata {
|
||||||
|
@ -1120,7 +1120,7 @@ func NewMetadata(user types.User) Metadata {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, both our `Document` and `AudioFile` structures are updated, to also populate these fields on construction. This is the core principle behind decoupling and an excellent example of ensuring maintainability of code. We can also add new methods, without breaking our code:
|
Now, both our `Document` and `AudioFile` structures are updated to also populate these fields on construction. This is the core principle behind decoupling and an excellent example of ensuring maintainability of code. We can also add new methods without breaking our existing code:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
|
@ -1136,9 +1136,9 @@ func (metadata *Metadata) AddUpdateInfo(user types.User) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Again, without breaking the rest of our code base, we are implementing new functionality to our already existing structures. This kind of programming, makes implementing new features very quick and very painless, which is exactly what we are trying to achieve by making our code clean.
|
Again, without breaking the rest of our codebase, we've managed to introduce new functionality. This kind of programming makes implementing new features very quick and painless, which is exactly what we are trying to achieve by writing clean code.
|
||||||
|
|
||||||
Now, I am sorry to break this streak of happiness, because now we return to the smelly forbidden forest of Go. Let's get back to our interfaces and how to show explicitly which interfaces are being implemented by a structure. Instead of embedding a struct, we can embed an interface:
|
Now, I am sorry to break this streak of happiness—it's time we enter the smelly forbidden forest of Go. Let's revisit the original problem of our interfaces: Trying to explicitly show which interfaces are being implemented by a given structure. Instead of embedding a struct, we can embed an interface:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type NullWriter struct {
|
type NullWriter struct {
|
||||||
|
@ -1150,7 +1150,7 @@ func NewNullWriter() io.Writer {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The above code compiles. The first time I saw this, I couldn't believe that this was actually compiling. Technically, we are implementing the interface of `Writer`, because we are embedding the interface and "inheriting" the functions which are associated with this interface. Some see this as a clear way of showing that our `NullWriter` is implementing the `Writer` interface. However, we have to be careful using this technique, as we can no longer rely on the compiler to save us:
|
The above code compiles. The first time I saw this, I couldn't believe that this was actually valid code. Technically, we are implementing the interface of `Writer`, because we are embedding the interface and "inheriting" the functions that are associated with this interface. Some see this as a clear way of showing that our `NullWriter` is implementing the `Writer` interface. However, we have to be careful using this technique, as we can no longer rely on the compiler to save us:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -1160,21 +1160,21 @@ func main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
As mentioned before, the above code will compile. The `NewNullWriter` returns a `Writer` and everything is honky-dori, according to the compiler, because `NullWriter` fulfills the contract of `io.Writer`, via. the embedded interface. However, running the code above will result in the following:
|
As mentioned before, the above code will compile. The `NewNullWriter` returns a `Writer`, and everything is hunky-dory according to the compiler because `NullWriter` fulfills the contract of `io.Writer`, via the embedded interface. However, running the code above will result in the following:
|
||||||
|
|
||||||
> panic: runtime error: invalid memory address or nil pointer dereference
|
> panic: runtime error: invalid memory address or nil pointer dereference
|
||||||
|
|
||||||
The explanation being, that an interface method in Go, is essentially a function pointer. In this case, since we are pointing the function of an interface, rather than an actual method implementation, we are trying to invoke a function, which is in actuality a nil pointer. Oops! Personally, I think that this is a massive oversight in the Go compiler. This code **should not** compile... but while this is being fixed (if it ever will be), let's just promise each other, never to implement code in this way. In an attempt to be more clear with our implementation, we have ended up shooting ourselves in the foot and bypassing compiler checks.
|
What happened? An interface method in Go is essentially a function pointer. In this case, since we are pointing to the function of an interface, rather than an actual method implementation, we are trying to invoke a function that's actually a `nil` pointer. Personally, I think that this is a massive oversight in the Go compiler. This code **should not** compile... but while this is being fixed (assuming it ever will be), let's just promise each other to never write code in this way. In an attempt to be more clear with our implementation, we have ended up shooting ourselves in the foot and bypassing compiler checks.
|
||||||
|
|
||||||
> Some people argue that using embedded interfaces, is a good way of creating a mock structure, for testing a subset of interface methods. Essentially, by using an embedded interface, you won't have to implement all of the methods of an interface, but instead only implement the few methods that you wish to be tested. Within testing / mocking, I can see the argument, but I am still not a fan of this approach.
|
> NOTE: Some people argue that using embedded interfaces is a good way of creating a mock structure for testing a subset of interface methods. Essentially, by using an embedded interface, you won't have to implement all of the methods of the interface; rather, you can choose to implement only the few methods that you'd like to test. Within the context of testing/mocking, I can see this argument, but I am still not a fan of this approach.
|
||||||
|
|
||||||
Let's quickly get back to clean code and quickly get back to using interfaces the proper way in Go. Let's talk about using interfaces as function parameters and return values. The most common proverb for interface usage with functions in Go is:
|
Let's quickly get back to clean code and using interfaces the proper way in Go. It's time to discuss using interfaces as function parameters and return values. The most common proverb for interface usage with functions in Go is the following:
|
||||||
|
|
||||||
> <em>Be conservative in what you do; be liberal in what you accept from others – Jon Postel</em>
|
> <em>Be conservative in what you do; be liberal in what you accept from others – Jon Postel</em>
|
||||||
|
|
||||||
> FUN FACT: This proverb originally has nothing to do with Go, but is actually taken from an early specification of the TCP networking protocol.
|
> FUN FACT: This proverb actually has nothing to do with Go. It's taken from an early specification of the TCP networking protocol.
|
||||||
|
|
||||||
In other words, you should write functions that accept an interface and return a concrete type. This is generally good practice, and becomes super beneficial when doing tests with mocking. As an example, we can create a function which takes a writer interface as input and invokes the `Write` method of that interface.
|
In other words, you should write functions that accept an interface and return a concrete type. This is generally good practice and becomes very beneficial when doing tests with mocking. As an example, we can create a function that takes a writer interface as its input and invokes the `Write` method of that interface:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Pipe struct {
|
type Pipe struct {
|
||||||
|
@ -1196,7 +1196,7 @@ func (pipe *Pipe) Save() error {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Let's assume that we are writing to a file when our application is running, but we don't want to write to a new file for all tests which invokes this function. Therefore, we can implement a new mock type, which will basically do nothing. Essentially, this is just basic dependency injection and mocking, but the point is that it is extremely easy to use in go:
|
Let's assume that we are writing to a file when our application is running, but we don't want to write to a new file for all tests that invoke this function. We can implement a new mock type that will basically do nothing. Essentially, this is just basic dependency injection and mocking, but the point is that it is extremely easy to achieve in Go:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type NullWriter struct {}
|
type NullWriter struct {}
|
||||||
|
@ -1212,9 +1212,9 @@ func TestFn(t *testing.T) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> NOTE: there is actually already a null writer implementation built into the ioutil package named `Discard`
|
> NOTE: There is actually already a null writer implementation built into the `ioutil` package named `Discard`.
|
||||||
|
|
||||||
When constructing our `Pipe` struct with the `NullWriter` (rather than a different writer), when invoking our `Save` function, nothing will happen. The only thing we had to do, was add 4 lines of code. This is why in idiomatic go, it is encouraged to make interface types as small as possible, to make implement a pattern like this as easy as possible. However, this implementation of interfaces, also comes with a *huge* downside.
|
When constructing our `Pipe` struct with `NullWriter` (rather than a different writer), when invoking our `Save` function, nothing will happen. The only thing we had to do was add four lines of code. This is why you're encouraged to make interfaces as small as possible in idiomatic Go—it makes it especially easy to implement patterns like the one we just saw. However, this implementation of interfaces, also comes with a <em>huge</em> downside.
|
||||||
|
|
||||||
### The empty `interface{}`
|
### The empty `interface{}`
|
||||||
Unlike other languages, go does not have an implementation for generics. There have been many implementation proposals, but all have been deemed dissatisfactory by the Go language team. Unfortunately, without generics, developers are trying to find creative ways around this issue, very often using the empty `interface{}`. The next section, will describe why these, often too creative, implementations should be considered bad practice and unclean code. There will also be good examples of usage of the empty `interface{}` and how to avoid some pitfalls of writing code with the empty `interface{}`.
|
Unlike other languages, go does not have an implementation for generics. There have been many implementation proposals, but all have been deemed dissatisfactory by the Go language team. Unfortunately, without generics, developers are trying to find creative ways around this issue, very often using the empty `interface{}`. The next section, will describe why these, often too creative, implementations should be considered bad practice and unclean code. There will also be good examples of usage of the empty `interface{}` and how to avoid some pitfalls of writing code with the empty `interface{}`.
|
||||||
|
|
Loading…
Reference in a new issue