Skip to content

🐿️ Go

Default or Zero values

  1. Basic Types:
    • int, int8, int16, int32, int64: 0
    • uint, uint8, uint16, uint32, uint64, uintptr: 0
    • float32, float64: 0.0
    • complex64, complex128: 0 + 0i
    • bool: false
    • string: "" (empty string)
    • byte (alias for uint8): 0
    • rune (alias for int32): 0
  2. Composite Types:
    • Pointer (e.g., *T): nil
    • Array (e.g., [5]int): The zero value of the array's element type, repeated for each element. For example, [5]int has a zero value of [0, 0, 0, 0, 0].
    • Slice: nil
    • Map: nil
    • Channel: nil
    • Function: nil
    • Interface: nil
    • Struct: Each field of the struct has its zero value. For example, for a struct type Point struct { X, Y int }, the zero value is Point{0, 0}.
  3. Named Types:
    • If you define a type based on another type (e.g., type Age int), the zero value is the same as the underlying type. In this example, the zero value for Age would be 0.

Slice vs Array

  • Array:
    • Fixed Size Collections: When you know the number of elements in advance and it won't change.
    • Performance: Arrays can be more cache-friendly due to their fixed size.
    • Value Semantics: Arrays are value types, so they're copied in their entirety when passed to functions or assigned to another variable.
    • As Underlying Storage: Sometimes used as the base storage for implementing other data structures or for backing slices.
  • Slice:
    • Dynamic Collections: When you need a collection that can grow or shrink.
    • Function Arguments: It's more idiomatic to pass slices to functions rather than arrays because they're more flexible and can represent a subset of an array.
    • Common Operations: Slices have built-in functions like append and copy, making many operations more convenient.
    • Reference Semantics: Slices are reference types, so they point to an underlying array. Multiple slices can share the same underlying data.
    • Subsetting: Easily represent a subset of an array or another slice.

Interface implementations and inheritance

1. Java Interfaces vs. Go Interfaces:

Java Interface: Java interfaces define a contract (a set of method signatures) that implementing classes must adhere to.

java

interface Animal {
    void speak();
}

class Dog implements Animal {
    @Override
    public void speak() {
        System.out.println("Woof!");
    }
}

interface Animal {
    void speak();
}

class Dog implements Animal {
    @Override
    public void speak() {
        System.out.println("Woof!");
    }
}

Go Interface: Go interfaces are implicitly satisfied. If a type has methods that match the interface's definition, it satisfies the interface without an explicit declaration.

go

type Animal interface {
    Speak() string
}

type Dog struct{}

func (d Dog) Speak() string {
    return "Woof!"
}

type Animal interface {
    Speak() string
}

type Dog struct{}

func (d Dog) Speak() string {
    return "Woof!"
}

2. Java Inheritance vs. Go Embedding:

Java Inheritance: Java uses class-based inheritance. A subclass inherits fields and methods from a superclass.

java

class Animal {
    void eat() {
        System.out.println("Eating...");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Woof!");
    }
}

Dog dog = new Dog();
dog.eat();  // Inherited from Animal
dog.bark();

class Animal {
    void eat() {
        System.out.println("Eating...");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Woof!");
    }
}

Dog dog = new Dog();
dog.eat();  // Inherited from Animal
dog.bark();

Go Embedding: Go doesn't support class-based inheritance. Instead, it offers embedding, where one struct can be embedded inside another, allowing the outer struct to access the methods and fields of the embedded struct.

go

type Animal struct{}

func (a Animal) Eat() {
    fmt.Println("Eating...")
}

type Dog struct {
    Animal  // Embedding Animal into Dog
}

func (d Dog) Bark() {
    fmt.Println("Woof!")
}

var dog Dog
dog.Eat()  // Accessed via the embedded Animal
dog.Bark()

type Animal struct{}

func (a Animal) Eat() {
    fmt.Println("Eating...")
}

type Dog struct {
    Animal  // Embedding Animal into Dog
}

func (d Dog) Bark() {
    fmt.Println("Woof!")
}

var dog Dog
dog.Eat()  // Accessed via the embedded Animal
dog.Bark()

Key Takeaways:

  1. Implicit vs. Explicit:
    • In Java, interfaces and inheritance are explicit. You declare that a class implements an interface or extends another class.
    • In Go, interface satisfaction is implicit. If a type has the necessary methods, it satisfies the interface. Embedding is explicit, but the fact that the outer type "inherits" methods and fields from the embedded type is implicit.
  2. Composition over Inheritance:
    • Java supports both inheritance (with the extends keyword) and composition (by having instance variables that are references to other objects).
    • Go promotes composition over inheritance. Instead of inheriting from a base class, you embed types within other types.
  3. Flexibility:
    • Embedding in Go is more flexible than classical inheritance in Java. In Go, you can embed multiple structs into another struct, effectively achieving what would be "multiple inheritance" in traditional OOP languages. Java doesn't support multiple inheritance for classes.

In essence, while Java and Go approach object-oriented design differently, they both offer mechanisms to define contracts (interfaces) and reuse code (inheritance/embedding). However, Go's approach is more flexible and promotes composition, making it easier to build modular and maintainable systems.


Returning pointer or struct in functions

1. Memory and Performance:

  • Returning a Struct: When a function returns a struct, it returns a copy of that struct. This means that the memory used by the original struct inside the function and the copy received by the caller are separate. For large structs, copying can be relatively expensive in terms of both memory and performance.
  • Returning a Pointer: When a function returns a pointer to a struct, it returns the memory address of that struct. No copying of the struct's data occurs. This can be more efficient, especially for larger structs.

2. Mutability:

  • Returning a Struct: Since the caller receives a copy of the struct, any modifications made to the struct by the caller won't affect the original struct inside the function.

    go
    
    func createStruct() MyStruct {
        return MyStruct{Value: 1}
    }
    
    obj := createStruct()
    obj.Value = 2  // This doesn't affect the original struct inside createStruct
    
    func createStruct() MyStruct {
        return MyStruct{Value: 1}
    }
    
    obj := createStruct()
    obj.Value = 2  // This doesn't affect the original struct inside createStruct
  • Returning a Pointer: The caller receives a reference to the original struct. Any modifications made by the caller will affect the original struct.

    go
    
    func createPointer() *MyStruct {
        return &MyStruct{Value: 1}
    }
    
    objPtr := createPointer()
    objPtr.Value = 2  // This modifies the original struct inside createPointer
    
    func createPointer() *MyStruct {
        return &MyStruct{Value: 1}
    }
    
    objPtr := createPointer()
    objPtr.Value = 2  // This modifies the original struct inside createPointer

3. Nil Value:

  • Returning a Struct: A struct cannot be nil. The caller will always receive a valid struct value, even if it's the zero value of the struct.
  • Returning a Pointer: The pointer can be nil, which can represent the absence of a value or an error condition. The caller needs to check if the returned pointer is nil before dereferencing it to avoid a runtime panic.

4. Consistency and Idiomatic Go:

  • In many Go libraries and frameworks, it's common to return pointers to structs, especially when the functions create new instances of objects or fetch data from external sources like databases.

5. Garbage Collection:

  • Returning a Struct: Since the caller gets a copy, the original struct inside the function can be garbage collected (if no other references to it exist) once the function execution is complete.
  • Returning a Pointer: The memory for the struct is kept alive as long as there are references to it. If the caller retains the pointer, the garbage collector won't reclaim the memory for the struct.

In the Context of the Caller:

  • When receiving a struct, the caller can be sure that modifications won't affect any other parts of the program that might reference the original struct.
  • When receiving a pointer, the caller should be aware that modifications will affect the original data. The caller should also check for nil before accessing the struct's fields.
  • If the caller doesn't intend to modify the struct and just wants to read its fields, there's little practical difference between receiving a struct or a pointer. However, performance considerations might still apply, especially for large structs.

Creating new objects

1. animal := Animal{}

This creates a new instance of the Animal struct with all its fields set to their zero values. The variable animal is of type Animal (not a pointer).

2. animal := new(Animal)

This also creates a new instance of the Animal struct with all its fields set to their zero values, but it returns a pointer to the struct. The variable animal is of type *Animal.

The new function is a built-in function in Go that allocates memory for a given type, initializes it (i.e., sets all its fields to their zero values), and returns a pointer to it.

3. animal := &Animal{}

This is similar to the previous method. It creates a new instance of the Animal struct with all its fields set to their zero values and returns a pointer to the struct. The variable animal is of type *Animal.

The difference between this method and the new(Animal) method is mostly syntactical. They both achieve the same result, but the &Animal{} syntax can be more flexible if you want to immediately initialize some fields of the struct.

For example:

go
animal := &Animal{Name: "Dog"}
animal := &Animal{Name: "Dog"}

4. var animal Animal

This declares a variable animal of type Animal. The struct is initialized with zero values for all its fields. This method doesn't explicitly create a new instance; instead, it declares a variable of the struct type, which is automatically initialized.

In Summary:

  • animal := Animal{} and var animal Animal both result in animal being of type Animal (not a pointer). The difference is in the declaration and initialization syntax.
  • animal := new(Animal) and animal := &Animal{} both result in animal being of type Animal (a pointer to an Animal). The difference is in the instantiation method: new is a built-in function, while &Animal{} is a combination of the address-of operator and struct literal syntax.

When choosing between these methods, consider whether you need a pointer or a value, and whether you want to initialize any fields immediately. The choice often depends on the use case, performance considerations, and personal or team coding style preferences.


make vs new

Both make and new are built-in functions in Go used for memory allocation, but they serve different purposes and are used with different types. Here's a breakdown of their differences:

1. new:

  • Purpose: new is used to allocate memory for a given type and returns a pointer to a zero value of that type.

  • Return Value: It always returns a pointer.

  • Usage: It can be used with any type, including basic types (like int, float64, string), structs, and arrays.

  • Initialization: The allocated memory is initialized to the zero value of the type. For instance, if you use new with an int, you'll get a pointer to the value 0.

  • Example:

    go
    ptr := new(int)  // ptr is a pointer to an int, *ptr == 0
    ptr := new(int)  // ptr is a pointer to an int, *ptr == 0

2. make:

  • Purpose: make is used to initialize and allocate memory for slices, maps, and channels. It not only allocates memory but also initializes the underlying data structures for these types, making them ready to use.

  • Return Value: It returns an initialized (non-zero) value of the type, not a pointer. However, note that slices, maps, and channels are reference types in Go, so they internally contain pointers to the underlying data.

  • Usage: It's only used with slices, maps, and channels.

  • Initialization: The allocated memory is initialized based on the type:

    • For slices: make allocates the underlying array and returns a slice reference to it. You can specify length and capacity.
    • For maps: make initializes the hash table and returns a map ready for key-value assignments.
    • For channels: make initializes a new channel with the specified buffer capacity.
  • Example:

    go
    slice := make([]int, 5)      // Slice of int with length 5
    myMap := make(map[string]int)// Empty map with string keys and int values
    ch := make(chan int, 10)     // Channel of int with buffer capacity of 10
    slice := make([]int, 5)      // Slice of int with length 5
    myMap := make(map[string]int)// Empty map with string keys and int values
    ch := make(chan int, 10)     // Channel of int with buffer capacity of 10

Key Differences:

  1. Types: new can be used with any type, while make is specific to slices, maps, and channels.
  2. Return Value: new always returns a pointer to the zero value of the type. make returns an initialized value of the type (which is a reference type for slices, maps, and channels).
  3. Initialization: new only allocates memory and returns a pointer to the zero value. make allocates and initializes the memory, making it ready for use.

In summary, when working with slices, maps, or channels in Go, you'll typically use make to ensure they're properly initialized. For other types where you just want a pointer to a zero value, you'd use new.


Giovanny Massuia 👨‍💻