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:

❌  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 build
  • go run
  • go test
  • go 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

Discover more from Ramblings in time and measure

Subscribe now to keep reading and get access to the full archive.

Continue reading