Lessons Learned after 1 year of programming in Go as a C# developer

Before pointing out my lessons learned, I am going to describe my background and the way I was learning and using Go to give you some context and better understanding.

I was writing in C# from 2009 to 2018. I was developing different sort of applications such as desktop apps, web apps, web services. Some of these were really big (even modular) monoliths. I was doing quite a lot of TDD since 2015. I think that I have a quite deep understanding and experience in OOAD, Design Patterns, SOLID, DDD, CQRS. I have also tried functional programming on my own in F# and started to apply functional programming in C#.

In 2018, I started to feel that C# is getting to complicated and loaded with too many features. Moreover, it did not look anything is going to be simplified in the future, rather the contrary. For example for concurrent programming you could use threading, TPL DataFlow, async/await, Reactive Extensions. Right now in C# 8.0 if you want to return more than one result from a method you can use an out parameter, use ValueTuple or Tuple or create your own type… or throw an exception if it is an error. I decided that I want to check out something else, even if I would decide to go back to C# some day. I just started to feel unhappy with C#.

My journey with Go started in the second half of December of 2018. This is when I decided to move to a project, where it was planned to develop new microservices in Go. I started learning by completing the Tour of Go. Then I have read Effective Go and How to Write Go Code. Then I have started to learn in multiple ways in parallel to get the most of my brain. I have been:

  1. Reading Go Programming Language
  2. Watching Learn How To Code: Google’s Go (golang) Programming Language
  3. Completing Exercism: Go Track
  4. Attending GoCracow meetup
  5. Analyzing Go source code
  6. Writing my own toy app https://github.com/pellared/milionerzy

in May of 2019, I finally started to code in Go at work. Colleagues from other teams have made for our team a beautiful project seed with basic CRUD logic together with dockerization and Continuous Integration. Moreover, I have been mentored by probably one of our best Gopher, who has been doing a lot of initial code reviews (thank you a lot Trevor!). During 8 months, I have been developing a REST API microservice, a queue worker microservice and 2 CLI apps.

After this lengthy preface, I am going to try the list what I have learned in my last 14 months of development in Go.

  1. Start developing everything in one package/assembly. If project grows then first decide if the application should be broken into two (or more) applications. Alternatively, structure your project based on functionality (not layers! it breaks CCP and CRP principles). Usually it results in having a “core/domain” package, functionality packages, utility packages and “main” package which bootstraps the application.
  2. Avoid adding new dependencies (libraries, tools). A little copying is better than a little dependency. It also often steepens the learning curve and decreases maintainability. There are a lot of problems in software dependencies.
  3. Clear code is better than clever code. It is even worth to make the code more verbose if it makes the code more clear and readable.
  4. Use dependency injection. However, do not create an interface, if there is only one implementation and you do not need to substitute the dependency in tests.
  5. Do not use patterns, nor do not apply SOLID, DDD, CQRS unless it really helps. If not needed, it often just adds boilerplate code, which can be even harder to understand. For example, regarding SRP or OCP, it may be better to extract some code a to method/function instead of introducing a new type. I claim that Kent Beck’s Design Rules are the most important software design rules. Use patterns to solve problems. Use principles as guidelines, not as rules.
  6. Resist to create your own generics, if you use something only in one scenario. It just adds additional complexity.
  7. Avoid using reflection. It hurts not only performance, but also static type checking. Favor Pure DI over using a DI Container.
  8. Fluent API often adds additional complexity and is harder to debug. Use it when it really helps. Same goes for intensive use of closures.
  9. If you know SQL, then try not use any ORMs. At most some tiny library which maps database query result sets to objects like sqlx or Dapper. For database migrations you can consider to use migrate.
  10. Maintainable automated tests are the key for whole software maintainability. If you have a good test suite then you can always refine the implementation. Test the behavior, not implementation. Without it would be hard to change anything. Tests should show what the system does, instead of how it does it. Avoid mocking whenever possibile. There is nothing wrong in using real database or file system in your automated tests unless the tests are not taking too much time. Integration tests give more trust, because they work the same way as code in production. Break something in the system to test error handling. For example change the name of table in the database. Mocking often leads to testing implementation details instead of behavior. Consider writing fakes instead of mocking.
  11. Write unit tests as you develop, but remove redundant tests after you are done. If you have an some test that already covers 10 of other tests, then just remove them. The need of refactoring applies also to test code. Less code means less code to read and maintain. Avoid writing tests for a single type.
  12. There is nothing bad in multiple asserts in a single test as long as it verifies one scenario.

The list is probably not complete, as my memory is not really good. There are also some stuff that I generally like in Go, but I think that they are not really applicable for other languages. For example, I favor returning explicit error objects instead of throwing exception. However, I would not do it in C#, because it is not idiomatic. Unfortunately, error handling in most of the languages is done using exceptions. I love how fast Go builds the code, its testing framework and the simplicity of the language.

Right now, I truly miss only one thing in Go, which is great in C# ecosystem. A great free IDE like Visual Studio. I use Visual Studio Code with Go extension. It has almost no refactorings. Even simple renaming often fails. Additionally, support for go modules does not work very well.

The other stuff pro C# is that it possible to write almost all kind of applications in C#: network, desktop, mobile, ML, IoT. It gives you possibility to use the same tools and libraries in most of the above applications. For me C# is like a Swiss Army knife. You can do everything with it, but it would never be the most efficient way.

I know that I could learn all of this stuff while still being a C# developer. However, changing the language and ecosystem made it easier to analyze my coding style and change my habits. Additionally, I think that a lot Gophers have similar experience to mine that most software in other ecosystems are a total legacy mess or over-engineered modular, CQRS, DDD, Event Driven, everything systems. Go community resits to use patterns when not needed.


One thought on “Lessons Learned after 1 year of programming in Go as a C# developer

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s