🐿️ Go
- Offical website: https://golang.org/
- Offical documentation: https://golang.org/doc/
Default or Zero values
- 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
- 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 isPoint{0, 0}
.
- 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 forAge
would be 0.
- If you define a type based on another type (e.g.,
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
andcopy
, 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.
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.
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.
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.
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:
- 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.
- 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.
- Java supports both inheritance (with the
- 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.
gofunc 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.
gofunc 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 isnil
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:
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{}
andvar animal Animal
both result inanimal
being of typeAnimal
(not a pointer). The difference is in the declaration and initialization syntax.animal := new(Animal)
andanimal := &Animal{}
both result inanimal
being of typeAnimal
(a pointer to anAnimal
). 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 anint
, you'll get a pointer to the value0
.Example:
goptr := 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.
- For slices:
Example:
goslice := 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:
- Types:
new
can be used with any type, whilemake
is specific to slices, maps, and channels. - 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). - 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
.