This commit is contained in:
Lasse Martin Jakobsen 2019-03-04 01:03:17 +01:00 committed by GitHub
parent f8f147df1d
commit f423cdf1ae

View file

@ -14,11 +14,17 @@ The document will start with a simple and short introduction of the fundamentals
* [Introduction to Clean Code](#Introduction-to-Clean-Code)
* Test Driven Development
* Cleaning Functions
* Variable Scope
* [Clean Golang](#Refactoring-Code)
* Let's talk about `interface{}`
* Spotting Code Smell
* Refactor examples
* [Clean Golang](#Clean-Golang)
* Returning Defined Errors
* Returning Dynamic Errors
* Interfaces in Go
* The empty `interface{}`
* [Go Code Generation](#Go-Code-Generation)
* [Balancing Performance with Cleanliness](#Balancing-Performance-with-Cleanliness)
* Immutability - Variable Scope Continue
* [Index](#Index)
### Introduction to Clean Code
Clean Code, is the pragmatic concept of ensuring readable and maintanable code. Clean Code establishes trust in the codebase and will steer developers away from introducing bugs. Clean Code will also establish much more stability in development speed, which typically will take a nose dive in the later stages of projects, due to higher risk of increasing bugs when introducing changes, as the codebase expands.
@ -251,8 +257,6 @@ func main() {
}
```
#### Variable Scope - continued
// TODO: write some more stuff here
### Clean Golang
This section will describe some less generic aspects of writing clean golang code, but rather be discussing aspects that are very go specific. Like the previous section, there will still be a mix of generic and specific concepts being discussed, however, this section marks the start of the document, where the document changes from a generic description of clean code with golang examples, to golang specific descriptions, based on clean code principles.
@ -421,7 +425,6 @@ func GetItemHandler(w http.ReponseWriter, r http.Request) {
}
```
#### 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 / fullfills the `Error` interface and can be returned as an error. This has it's advantages, as it makes golang feel more fast-paced and dynamic, as implementing an interface is extremely easy. The general proverb for writing golang functions is:
@ -569,7 +572,7 @@ func SomeFunction_String(value string) string { ... }
Unfortunately, there is no way in golang to directly translate this. However, we can actually get very very close to producing something similar with go code generation.
#### Go Code Generation
### Go Code Generation
Let me start out by saying, that go code generation is nothing fancy. It really, really isn't. At it's most basic, it's simply just templatling (much like HandleBars.js in JavaScript and Jinja in Python). However, it works really well, especially when it comes to writing generic code, that we don't want to write over and over again. Keep in mind, that it doesn't solve all of our problems and there are definitely situations in which actual generic lanauge support would be preferred. However, it is still greatly preferred to using empty `interface{}`
The following shows some basic go code generation, which will be able to create very simple, but thread-safe, HashMap for primitive types. This is an example of how to do this, and should give enough inspiration as how to expand on the example and create more elaborate code generation templates.
@ -622,6 +625,116 @@ Of course, this doesn't come without certain disadvantages. We add a comment a t
This line, will tell certain IDE's to show a warning, that the file is not to be edited. However, nothing is actually preventing users from editing these files. Typically though, code generation is used along with a build script tool (such as a Makefile), so that all generated files are re-generated on build. This will, hopefully, prevent any users from making unwanted changes to the generated code. Of course, this also means that there is no room for tweaking our generated code type by type, but I feel this to be more of an advantage, than anything.
### Balancing Performance with Cleanliness
> TODO: I'm still not quite sure where to place this in the document
Everything comes with a price. Much like you *can't please all of the people all of the time*, you can't always have all your come code be 100% clean. In certain scenarios where you're main goal is to ensure the absolute most performant setup possible, it won't be possible to write the cleanest code. With everything else in life, comprimises are unavoidable and sometimes going against common best-practice can be a necessity. This is also absolutely fine, as long as we understand why the compromise was made.
Of course, if your main goal is to write top-performant applications. You're best bet, probably isn't golang. Golang is definitely a good choice within performance, but we shouldn't kid ourselves into believing that it can compete with the likes of C/C++ (or even Rust). That being said, it doesn't mean that we should all thoughts on creating performant code out of the window. This section will look at some concepts of ensuring clean code, while still ensuring a minimum loss of performance.
#### Immutability - (Variable Scope Continued)
Previously, we spoke about variable scope and how we should try to keep our variables immutable, as best possible. This is to avoid managing state, which is an immensely difficult part of programming and also to avoid confusion, when reading code. If state is changed in the flow of code, it can become extremely difficult to read and remember the current value of that state, making the code harder to debug. This becomes an even bigger problem when we start introducing concurrency into the picture. Functional programming languages pride themselves on being 100% concurrency safe, and rightly so! In functional programming all variables are immutable (at least to a certain extent). With this in mind, we can now guarantee that our code will never suffer from a race condition, as we will never mutate memory. That's neat!
Now, while rather controversial to mention in the golang language community. Golang, performance-wise, really benefits from borrowing a functional style. The performance bottleneck for golang, is the garbage collector. In most cases, this is why C/C++ and Rust outperform golang. Solving certain problems, however, where the computation is very reliant on fast memory allocation, golang performs ***horribly***. As an example of this (from: [The Computer Language Benchamrks Game](https://benchmarksgame-team.pages.debian.net/benchmarksgame/performance/binarytrees.html)), Rust is almost 8 times faster than go at creating binary trees. Even more insulting, there are Nodejs implementations that are faster than the fastest golang implementation.
It's really weird. But let's not think about it too much right now, because honestly, there is not much we *can* do about it. Instead, let's just agree that it's extremely unlikely that you will need to create binary trees performant in your day-to-day, so everything is probably going to be fine. The more important lesson we can learn from this, is that heap allocations should be avoided in golang code, if we want to keep it performant... and wow, what a coincidence... because this leads me to immutability and performance.
Let's take a look at a simple "FizzBuzz" implementation:
```go
func devnull(v ...interface{}) {
// don't print
}
func BenchmarkMutable(b *testing.B) {
buf := &bytes.Buffer{}
for i := 1; i < b.N; i++ {
buf.Reset()
if i%3 == 0 {
buf.WriteString("Fizz")
}
if i%5 == 0 {
buf.WriteString("Buzz")
}
if buf.String() == "" {
devnull(i)
} else {
devnull(buf.String())
}
}
}
```
Hey look it's a buffer implementation, and it get's worse, becasue the buf is a "global" variable, oh no! If we put the buf inside the for loop though, performance is absolutely terrible. Oh no ! What should we do? The allocation is tearing me apart, Lisa.
```go
func BenchmarkImmutable(b *testing.B) {
for i := 1; i < b.N; i++ {
devnull(
NewFizzBuzz(i).
AddIfDivisbleBy("Fizz", 3).
AddIfDivisbleBy("Buzz", 5).
Result(),
)
}
}
type FizzBuzz struct {
result string
number int
}
func NewFizzBuzz(n int) FizzBuzz {
return FizzBuzz{
number: n,
}
}
func (fb FizzBuzz) AddIfDivisbleBy(text string, divisor int) FizzBuzz {
if fb.number%divisor == 0 {
return FizzBuzz{result: fb.result + text, number: fb.number}
}
return fb
}
func (fb FizzBuzz) Result() interface{} {
if fb.result == "" {
return fb.number
}
return fb.result
}
```
Holy moly dude, that's the most complicated implementation of FizzBuzz I have ever seen in my life, that is some weird shit dude. But wow, 0 allocations.
```go
func BenchmarkThatsSoMutable(b *testing.B) {
for i := 1; i < b.N; i++ {
result := ""
if i%3 == 0 {
result += "Fizz"
}
if i%5 == 0 {
result += "Buzz"
}
if result != "" {
devnull(i)
} else {
devnull(result)
}
}
}
```
> NOTE: FizzBuzz implementation taken from article by Grayson Koonce, Engineering Manager at Uber: https://graysonkoonce.com/fizzbuzz-in-golang/
This solution is by no means awful and actually does quite well in our benchmark... However, there simply must be something wrong with these benchmarks.... I mean ffs... `result := ""` must surely be an allocation? Or is this all fucked, because it is put on the stack??? Wtf is going on here?
`TODO: don't have the time right now to finish this section. So I will just copy paste the code here, without any description`
## Index
### Generated Code