Tariq Brown

Tariq Brown

Software Engineer

Learning Go as a TypeScript Developer

avatar
Tariq

·

Aug 25, 2025

·

5 min read

Cover image

Coming from a TypeScript background, I thought that learning Go(lang) would be fairly straight-forward, especially since coding patterns like classes and objects are fairly consistent across most programming languages. Surprisingly, this was not the case with Go. Unlike other languages, Go has a very different philosophy. For instance, while TypeScript feels like a flexible superset of JavaScript, Go is minimal, opinionated, and strict in ways that force you to think differently.

In this post, I’ll share some of the biggest differences I noticed while learning Go as a TypeScript user.


Declaring Variables

In TypeScript, declaring variables is straightforward: you just use let or const. There is also var, but let’s be honest; no one uses that anymore. Unlike JavaScript, TypeScript also allows you to declare the type:

let count = 0; // mutable
const name: string = "Tariq"; // immutable with string type declaration

With Go, there are two main ways to declare variables:

// Using var (explicit type or inferred)
var count int = 0
var message = "hello"
 
// Using := (short hand, only inside functions)

Using := is essentially a shorthand for declaring and initializing a variable when the type can be inferred. You will see this being used quite often. If you need package-level variables, or want to be more explicit, you use var.

Structs VS Interfaces & Classes

In TypeScript, we’re used to defining object shapes using interfaces or modelling behaviour using classes, like typical OOP languages:

interface User {
    id: string;
    name: string;
}
 
class Person {
    constructor(public name: string) {}
 
    greet(): string {
        return `Hello, I'm, ${this.name}`;
    }
}

Go doesn’t have classes. Instead, you define structs to group related data:

type User struct {
    ID string
    Name string
}

If you want to add behaviour, you attach methods to the struct. But be warned, declaring methods is completely different to how you would normally do it in other languages:

type Person struct {
    Name string
}
 
func (p Person) Greet() string {
    return "Hello, I'm " + p.Name
}
 
func main() {
    me := Person{Name: "Tariq"}
    me.Greet()
}

Notice how the func keyword declares a method with (p Person) acting like the “receiver” (similar to this in TypeScript). At first, this felt awkward compared to class syntax, but I quickly began to appreciate its simplicity. It is worth noting that you instantiate the class (or create an object) with curly braces, not parenthesis.

Implicit Interfaces

Here’s a big shift: in TypeScript, when a class claims to implement an interface, you explicitly say so with the implements key word:

interface Greeter {
  greet(): string;
}
 
class Person implements Greeter {
  constructor(public name: string) {}
  greet(): string {
    return `Hello, I'm ${this.name}`;
  }
}

In Go, interfaces are implicit. If a type provides all the methods an interface requires, then it automatically satisfies that interface; no explicit declaration needed.

type Greeter interface {
    Greet() string
}
 
type Person struct {
    Name string
}
 
// Person has a Greet method, so it satisfies Greeter automatically
func (p Person) Greet() string {
    return "Hello, I'm " + p.Name
}

That means Person now implements Greeter without saying so anywhere in the code. This may feel strange at first, but it makes Go code more flexible. Libraries can define interfaces, and your types can “just work” with them as long as they implement the right methods.

This is one of those “Go is opinionated but pragmatic” features: it avoids boilerplate while still enforcing strong contracts.

No While Loops

In TypeScript, loops come in many flavors: for, while, do…while, for…of, and so on.

Go takes the opposite approach: there’s only one looping keyword: for.

// TypeScript
let i = 0;
while (i < 5) {
    console.log(i);
    i++;
}

In Go, you’d write the same thing as:

// Go
i := 0
for i < 5 {
    fmt.Println(i)
    i++
}

This seems far more efficient than the typical while loops that we see in many languages and is one of the aspects that I love about Go.

Multiple Return Values

In many languages, a function can only return a single value. If you need more, you usually return an object or tuple:

function splitName(fullName: string): [string, string] {
  const [first, last] = fullName.split(" ");
  return [first, last];
}

Go allows functions to return multiple values natively, without wreapping them in an array or an object:

func SplitName(fullName string) (string, string) {
    parts := strings.Split(fullName, " ")
    return parts[0], parts[1]
}

You can capture them directly:

first, last := SplitName("Tariq Brown")

This feature is core to Go’s design; it’s the reason why error handling usually works by returning a value and an error separately, which I will get into in the following section.

Error Handling: No Exceptions

Typescript has try…catch for exceptionjs, but Go doesn’t use exceptions for flow control. Instead, functions typically return two values: the result and an error. This links back to the section of returning multiple values.

// TypeScript
try {
  const data = JSON.parse("invalid");
} catch (err) {
  console.error("Something went wrong", err);
}
// Go
data, err := json.Unmarshal([]byte("invalid"), &result)
if err != nil {
    fmt.Println("Something went wrong:", err)
}

This explicit error handling might feel verbose at first, but it forces you to deal with errors immediately and makes the control flow very clear.

Concurrency: Goroutines VS Async/Await

TypeScript gives us async/await and Promises for handling asynchronous tasks

async function fetchData() {
    const res = await fetch("/api")
    return await res.json();
}

Go doesn’t have Promises - it has goroutines, lightweight threads managed by the Go runtime:

go fetchData() // Runs in the background

This model feels much lower-level than async/await, but it’s incredibly powerful when combined with Go’s channels for communication.


Final Thoughts

Learning Go as a TypeScript user has been refreshing. At Ffirst, I missed the flexibility of TypeScript, the freedom to model data however I wanted, the many loop constructs, and the familiar try…catch block. But Go’s simplicity grew on me.

By limiting features, Go forces you to think more about your data structures and error handling. And once you experience the speed of GO’s compiler and the elegance of goroutines, you start to understand why so many backend developers love it.

If you’re a TypeScript developer considering Go, my advice is: lean into the differences. Don’t try to write Go like it’s TypeScript. Instead, embrace its philosophy of simplicity and explicitness; you might be surprised by how much you learn.