From 306e2d7118704bb0a3345b247113f15cd421af5e Mon Sep 17 00:00:00 2001 From: Lasse Martin Jakobsen Date: Tue, 23 Jul 2019 17:05:50 +0200 Subject: [PATCH 01/10] less aggressive language and more descriptive expalanation of the downsides of using embedded interfaces --- README.md | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b16c8a0..0077af6 100644 --- a/README.md +++ b/README.md @@ -1162,7 +1162,7 @@ func (metadata *Metadata) AddUpdateInfo(user types.User) { 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—it's time that 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: +Going back to our interfaces contract fulfillment using embedded interfaces, the code below shows an example, which compiles without any issues: ```go type NullWriter struct { @@ -1174,7 +1174,7 @@ func NewNullWriter() io.Writer { } ``` -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: +The above code compiles. Technically, we are implementing the interface of `Writer` on our `NullWriter`, as `NullWriter` will inherit all 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, when using this technique we must be extra precautious. ```go func main() { @@ -1188,11 +1188,45 @@ As mentioned before, the above code will compile. The `NewNullWriter` returns a > panic: runtime error: invalid memory address or nil pointer dereference -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. +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. To avoid this from happening, we would have to provide the `NulllWriter` with a struct which fulfills the interface contract, with actual implemented methods. -> 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. +```go +func main() { + w := NullWriter{ + Writer: &bytes.Buffer{}, + } -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: + w.Write([]byte{1, 2, 3}) +} +``` + +> NOTE: In the above examble, `Writer` is referring to the embedded `io.Writer` interface. It is also possible to invoke the `Write` method, by accessing this property with: `w.Writer.Write()` + +We are no longer receiving a panic and can now use the `NullWriter` as a `Writer`. This initialisation process is not much different from having properties which are initialised as `nil`, as discussed previously. Therefore, logically, we should try and handle them in a similar way. However, this is where embedded interfaces become a little difficult to work with. In a previous chapter, it was explained that the best way to handle potential `nil` values, was to make the property in question private and create a public *getter* method. This way, we could ensure that our property is, in fact, not `nil`. Unfortunately, this is simply not possible with embedded interfaces, as they are by nature, always public. + +Another concern raised by using embedded interfaces, is the potential confusion caused by partially overwritten interface methods: + +```go +type MyReadCloser struct { + io.ReadCloser +} + +func (closer *ReadCloser) Read(data []byte) { ... } + +func main() { + closer := MyReadCloser{} + + closer.Read([]byte{1, 2, 3}) // works fine + closer.Close() // causes panic + closer.ReadCloser.Closer() // no panic +} +``` + +Even though, this might look like overriding methods, which are common in languages such as C# and Java. It isn't. Go doesn't have inheritance nor super classes. We can imitate the behaviour, but it is not an in-built part of the language. By using methods such as interface embedding without caution, we are creating confusing and possibly buggy code, just to save a few more lines of code. + +> NOTE: Some 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 proper usage of interfaces. 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: > Be conservative in what you do; be liberal in what you accept from others – Jon Postel From 052a6003ea4c0dc7f1fbbdf3c4ed1dc2447744e3 Mon Sep 17 00:00:00 2001 From: Lasse Martin Jakobsen Date: Tue, 23 Jul 2019 17:09:08 +0200 Subject: [PATCH 02/10] oopsie --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0077af6..ab7b5d4 100644 --- a/README.md +++ b/README.md @@ -1162,7 +1162,7 @@ func (metadata *Metadata) AddUpdateInfo(user types.User) { 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. -Going back to our interfaces contract fulfillment using embedded interfaces, the code below shows an example, which compiles without any issues: +Let's return to the topic of interface contract fulfillment using embedded interfaces, the code below shows an example, which compiles without any issues: ```go type NullWriter struct { From 2acf6d8020e1651c8a747538b87fff11af001089 Mon Sep 17 00:00:00 2001 From: Lasse Martin Jakobsen Date: Sun, 28 Jul 2019 09:15:22 +0200 Subject: [PATCH 03/10] Update README.md Co-Authored-By: Aleksandr Hovhannisyan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab7b5d4..c532578 100644 --- a/README.md +++ b/README.md @@ -1162,7 +1162,7 @@ func (metadata *Metadata) AddUpdateInfo(user types.User) { 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. -Let's return to the topic of interface contract fulfillment using embedded interfaces, the code below shows an example, which compiles without any issues: +Let's return to the topic of interface contract fulfillment using embedded interfaces. Consider the following code as an example: ```go type NullWriter struct { From 6bcff141fdd97d4e3437d078bc45316056fc4758 Mon Sep 17 00:00:00 2001 From: Lasse Martin Jakobsen Date: Sun, 28 Jul 2019 09:15:41 +0200 Subject: [PATCH 04/10] Update README.md Co-Authored-By: Aleksandr Hovhannisyan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c532578..f8e59e7 100644 --- a/README.md +++ b/README.md @@ -1174,7 +1174,7 @@ func NewNullWriter() io.Writer { } ``` -The above code compiles. Technically, we are implementing the interface of `Writer` on our `NullWriter`, as `NullWriter` will inherit all 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, when using this technique we must be extra precautious. +The above code compiles. Technically, we are implementing the interface of `Writer` in our `NullWriter`, as `NullWriter` will inherit all 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 must be careful when using this technique. ```go func main() { From eab8c21f28b5cc518abd80421eea738ba794de9a Mon Sep 17 00:00:00 2001 From: Lasse Martin Jakobsen Date: Sun, 28 Jul 2019 09:15:58 +0200 Subject: [PATCH 05/10] Update README.md Co-Authored-By: Aleksandr Hovhannisyan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8e59e7..811c562 100644 --- a/README.md +++ b/README.md @@ -1188,7 +1188,7 @@ As mentioned before, the above code will compile. The `NewNullWriter` returns a > panic: runtime error: invalid memory address or nil pointer dereference -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. To avoid this from happening, we would have to provide the `NulllWriter` with a struct which fulfills the interface contract, with actual implemented methods. +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. To prevent this from happening, we would have to provide the `NulllWriter` with a struct that fulfills the interface contract, with actual implemented methods. ```go func main() { From 384eaa6f30d05c2de0186fa4d8e1897eeb5143a2 Mon Sep 17 00:00:00 2001 From: Lasse Martin Jakobsen Date: Sun, 28 Jul 2019 09:16:11 +0200 Subject: [PATCH 06/10] Update README.md Co-Authored-By: Aleksandr Hovhannisyan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 811c562..ad9a9f7 100644 --- a/README.md +++ b/README.md @@ -1200,7 +1200,7 @@ func main() { } ``` -> NOTE: In the above examble, `Writer` is referring to the embedded `io.Writer` interface. It is also possible to invoke the `Write` method, by accessing this property with: `w.Writer.Write()` +> NOTE: In the above example, `Writer` is referring to the embedded `io.Writer` interface. It is also possible to invoke the `Write` method by accessing this property with `w.Writer.Write()`. We are no longer receiving a panic and can now use the `NullWriter` as a `Writer`. This initialisation process is not much different from having properties which are initialised as `nil`, as discussed previously. Therefore, logically, we should try and handle them in a similar way. However, this is where embedded interfaces become a little difficult to work with. In a previous chapter, it was explained that the best way to handle potential `nil` values, was to make the property in question private and create a public *getter* method. This way, we could ensure that our property is, in fact, not `nil`. Unfortunately, this is simply not possible with embedded interfaces, as they are by nature, always public. From 6920b78bc0c00c6141b7ca052884220256095b39 Mon Sep 17 00:00:00 2001 From: Lasse Martin Jakobsen Date: Sun, 28 Jul 2019 09:16:24 +0200 Subject: [PATCH 07/10] Update README.md Co-Authored-By: Aleksandr Hovhannisyan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad9a9f7..3176dd1 100644 --- a/README.md +++ b/README.md @@ -1202,7 +1202,7 @@ func main() { > NOTE: In the above example, `Writer` is referring to the embedded `io.Writer` interface. It is also possible to invoke the `Write` method by accessing this property with `w.Writer.Write()`. -We are no longer receiving a panic and can now use the `NullWriter` as a `Writer`. This initialisation process is not much different from having properties which are initialised as `nil`, as discussed previously. Therefore, logically, we should try and handle them in a similar way. However, this is where embedded interfaces become a little difficult to work with. In a previous chapter, it was explained that the best way to handle potential `nil` values, was to make the property in question private and create a public *getter* method. This way, we could ensure that our property is, in fact, not `nil`. Unfortunately, this is simply not possible with embedded interfaces, as they are by nature, always public. +We are no longer triggering a panic and can now use the `NullWriter` as a `Writer`. This initialisation process is not much different from having properties that are initialised as `nil`, as discussed previously. Therefore, logically, we should try to handle them in a similar way. However, this is where embedded interfaces become a little difficult to work with. In a previous section, it was explained that the best way to handle potential `nil` values is to make the property in question private and create a public *getter* method. This way, we could ensure that our property is, in fact, not `nil`. Unfortunately, this is simply not possible with embedded interfaces, as they are by nature always public. Another concern raised by using embedded interfaces, is the potential confusion caused by partially overwritten interface methods: From 0f5b9126e8d60164d57d82d51368e35953de649e Mon Sep 17 00:00:00 2001 From: Lasse Martin Jakobsen Date: Sun, 28 Jul 2019 09:16:32 +0200 Subject: [PATCH 08/10] Update README.md Co-Authored-By: Aleksandr Hovhannisyan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3176dd1..2d51aba 100644 --- a/README.md +++ b/README.md @@ -1222,7 +1222,7 @@ func main() { } ``` -Even though, this might look like overriding methods, which are common in languages such as C# and Java. It isn't. Go doesn't have inheritance nor super classes. We can imitate the behaviour, but it is not an in-built part of the language. By using methods such as interface embedding without caution, we are creating confusing and possibly buggy code, just to save a few more lines of code. +Even though this might look like we're overriding methods, which is common in languages such as C# and Java, we actually aren't. Go doesn't support inheritance (and thus has no notion of a superclass). We can imitate the behaviour, but it is not a built-in part of the language. By using methods such as interface embedding without caution, we can create confusing and potentially buggy code, just to save a few more lines. > NOTE: Some 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. From 3605215928ef5b6409c3aa7b6bfde3c59e2cf545 Mon Sep 17 00:00:00 2001 From: Lasse Martin Jakobsen Date: Sun, 28 Jul 2019 09:16:42 +0200 Subject: [PATCH 09/10] Update README.md Co-Authored-By: Aleksandr Hovhannisyan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d51aba..49eeb40 100644 --- a/README.md +++ b/README.md @@ -1204,7 +1204,7 @@ func main() { We are no longer triggering a panic and can now use the `NullWriter` as a `Writer`. This initialisation process is not much different from having properties that are initialised as `nil`, as discussed previously. Therefore, logically, we should try to handle them in a similar way. However, this is where embedded interfaces become a little difficult to work with. In a previous section, it was explained that the best way to handle potential `nil` values is to make the property in question private and create a public *getter* method. This way, we could ensure that our property is, in fact, not `nil`. Unfortunately, this is simply not possible with embedded interfaces, as they are by nature always public. -Another concern raised by using embedded interfaces, is the potential confusion caused by partially overwritten interface methods: +Another concern raised by using embedded interfaces is the potential confusion caused by partially overwritten interface methods: ```go type MyReadCloser struct { From d2307c8f361aee1e11a4e540d88165cb1e13353b Mon Sep 17 00:00:00 2001 From: Lasse Martin Jakobsen Date: Sun, 28 Jul 2019 09:16:52 +0200 Subject: [PATCH 10/10] Update README.md Co-Authored-By: Aleksandr Hovhannisyan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 49eeb40..b0084fe 100644 --- a/README.md +++ b/README.md @@ -1217,7 +1217,7 @@ func main() { closer := MyReadCloser{} closer.Read([]byte{1, 2, 3}) // works fine - closer.Close() // causes panic + closer.Close() // causes panic closer.ReadCloser.Closer() // no panic } ```