I’m over 50 years old, and as such I might be a little “stuck in my ways”.
However, previously I have talked about “an old dog learning new tricks”, when I had to switch from OOP to Functional Programming.
Recently I was once again faced with something new. Go. And I was sort of “against” it. Again, from the standpoint that I am “too old” to learn something new, after spending years building with C#/.NET, and now TypeScript/Node.
Well, I “hunkered down” and “bit that bullet”, and whatever other cliche sayings I could come up with to signify that I took this as a challenge. I am starting to get a little more comfortable with it – and somewhat enjoy it. It’s different. It’s certainly simpler. And it forces you to rethink your assumptions.
But what I wish I could have found earlier is something that was simple and straight forward which clearly showed differences between the two paradigms, and how to transfer knowledge of one to the other. “Not just, hey, here is how you write a program”
So, I am not going to go through and give you a “Hello World” type of tutorial here – there are plenty of those around. Instead I want to focus on some of the main “gotcha” points that I ran into, and super simple ways to address them.
🚀 The Big Shifts
1. Composition, Not Inheritance
Go has structs, not classes. You won’t find base classes or deep inheritance trees here—just small, composable components. Embed a struct, and if it has the methods, it’s done.
abstract class Animal
{
public virtual void Speak()
{
Console.WriteLine("...");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof");
}
}
type Animal struct{}
func (a Animal) Speak() {
fmt.Println("...")
}
type Dog struct{
Animal
}
func (d Dog) Speak() {
fmt.Println("Woof")
}
📚 Learn More
✅Interfaces are Implicit
Unlike C#, Go interfaces are satisfied implicitly. If a type implements the required methods, it automatically implements the interface.
public interface IStringer
{
string ToString();
}
public class Dog : Animal, IStringer
{
public string ToString()
{
return "Woof";
}
}
type Stringer interface {
ToString() string
}
type Dog struct {
Name string
}
func (d Dog) ToString() string {
return d.Name
}
📚 Learn More
🧩Manual DI = Clearer Architecture
In .NET you can use the built-in DI containers, and auto wire everywhere. In Go, dependencies are passed in constructively. It feels verbose at first, but it keeps things transparent.
services.AddScoped<IUserService, UserService>();
func NewHandler(svc UserService) *Handler {
return &Handler{Service: svc}
}
You can use:
- Google’s Wire – compile-time DI
- Uber’s Dig – reflection-based
- NVIDIA’s gontainer – container-based
❌ Errors as Go Values
No exceptions. No try/catch. Return an error, check it, repeat. Yes, it’s verbose, but it forces you to handle every failure point.
try
{
string data = File.ReadAllText("data.txt");
}
catch (Exception ex)
{
Console.WriteLine($"read error: {ex.Message}");
}
data, err := os.ReadFile("data.txt")
if err != nil {
log.Printf("read error: %v", err)
return
}
📚 Learn More
⚡Goroutines & Channels > async/await
Async/await in .NET is great. Go’s answer? go func() and channels. Super lightweight. Super fast. Just learn to coordinate and avoid race conditions with context and sync.
static async Task Main()
{
try
{
string data = await File.ReadAllTextAsync("data.txt");
Console.WriteLine($"Read {data.Length} characters.");
}
catch (Exception ex)
{
Console.WriteLine($"read error: {ex.Message}");
}
}
func readFile(ctx context.Context, ch chan<- string, errCh chan<- error) {
data, err := os.ReadFile("data.txt")
if err != nil {
errCh <- err
return
}
ch <- string(data)
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
dataCh := make(chan string)
errCh := make(chan error)
go readFile(ctx, dataCh, errCh)
select {
case data := <-dataCh:
fmt.Printf("Read %d characters.
", len(data))
case err := <-errCh:
fmt.Printf("read error: %v
", err)
case <-ctx.Done():
fmt.Println("read timed out")
}
}
📚 Learn More
🛠 6. Minimal Tooling
Go keeps things simple:
go buildgo rungo testgo fmt
No project files or XML configs.
📚 Learn More
⚙️ Common Patterns Reimagined
| Concept | .NET (C#) | Go |
|---|---|---|
| Class / Struct | class Person {} |
type Person struct {} |
| Async | await Task.Run(...) |
go DoSomething() |
| Interfaces | : IInterface |
Satisfy method = implements |
| DI | AddScoped<IUserService>() |
Constructor function |
| Error Handling | try/catch |
Return error, check every time |
| Testing | xUnit / NUnit | go test + testing package |
💡 Tips for a Smooth Transition
- Keep things tiny and composable. Small programs learn Go’s idioms fastest.
- Error handling can be annoying, but it is also explicit documentation.
- Read Go’s standard library: it’s clean, concise, idiomatic code.
- Translation ≠ Transplantation. Go’s simplicity is its power.
🎯 Final Takeaway
.NET and Go each shine in different spaces. Your .NET background gives you robust architecture tools. Go gives you speed, clarity, and concurrency, especially for cloud-native systems. Switching isn’t abandoning—it’s broadening your toolkit.
If you’re on the fence about learning Go, especially as a .NET dev, dive in. It’s certainly an interesting experience.
Leave a Reply