← O-Reilly Learning Go

Types, Methods and Interfaces

GolangTypesMethodsInterfaces

Types

An abstract type is one that defines what type should do not how it is done.
Abstract type defines contract, it tell callers what they can expect and what they must provide.
A concrete type specify what and how.
Golang achieve abstract type through interface.
For concrete type, golang achieve it by attaching methods/functions to a struct through receiver.
(we can attach function to any named types through receiver not just for structs)

Receivers

The receiver name is a short abbreviation of the type's name, usually the first letter.
Difference between methods and functions
Methods can be defined within package block level.
While functions can be defined inside any block.

Note
When you use a pointer receiver with a local variable(a value type), Go automatically takes the address of the local variable when calling the method.
If we call value receiver on a pointer variable, Go will automatically dereference the pointer when calling the method.

type counter struct{}
func (c `*`counter) Increment(){
smth
}

func (c counter) Print(){
smth
}

c1 := counter{}
c1.Increment()-> automatically convert value variable to pointer when calling method

c2 := &counter{}
c2.Print() -> automatically dereference the receiver when calling method

Don't write getter and setter methods, unless you need to meet the interface.
Go courages to access directly to a field. Reserves methods for business logics.

The exceptions are when you need to update multiple fields as a single operation or when the update isn’t a straightforward assignment of a new value.

Type Declarations are not inheritance

In language with inheritance, a child instance can be used anywhere the parent instance is used.
In Go, you can't assign an instance type of child to a variable of parent type. You can't assign without a proper type conversion.

Interfaces

Interfaces are implemented implicitly.
If the method set of concrete type contains all the method set of the interface type, the concrete type implements the interface.

Accept Interface Returns Struct

The primary reason your functions should return concrete types is they make it easier to gradually update a function’s return values in new versions of your code. When a concrete type is returned by a function, new methods and fields can be added without breaking existing code that calls the function, because the new fields and methods are ignored. The same is not true for an interface. Adding a new method to an interface means that all existing implementations of that interface must be updated, or your code breaks.

This is for flexibility.
If a function accepts a interface, we can pass any struct that satisfy the interface.
But not in function that accepts struct, we strictly rule the function input to be the same exact type as we provided in the function parameter.
So that the function won't be tied to a single struct.
Think of it like a struct using a database driver as a dependency.
Whose function accepts interface DB with Insert() and Delete() and return a struct.
First that function is using in a code with SQL driver.
Due to project requirements, we need to change to PSQL driver.
The current struct also using only Insert() and Delete() from SQL driver.
PSQL driver has one more function like Update().
But the PSQL driver match the interface DB requirements.
We can simply pass it to the function without needing changes.
That is the advantage of this concept.

Returning a struct also frees the caller.
The caller get all the informations and methods.
The returning struct can be also passed into any parameter that satisfies the interface.

Drawback
Reducing heap allocations improves performance by reducing the amount of work for the garbage collector. Returning a struct avoids a heap allocation, which is good. However, when invoking a function with parameters of interface types, a heap allocation occurs for each interface parameter.

Interfaces and Nil

In the Go runtime, interfaces are implemented as a struct with two pointer fields, one for the value and one for the type of the value. As long as the type field is non-nil, the interface is non-nil.

Example

type TestInterface interface{}
type TestStruct struct{}

Interface Variable

var AcceptsInterface TestInterface
AcceptsInterface = (type: nil, value: nil)  → nil ✅

Struct Pointer Variable

var SampleStruct *TestStruct
SampleStruct = nil  → nil ✅

After Assignment

AcceptsInterface = SampleStruct
AcceptsInterface = (type: *TestStruct, value: nil)  → not nil ❌

Type Assertions and Type Switches

Type assertion and type conversions are different.
Conversion change the value to new type.
Assertion reveal the type of the value stored in the interface.

Function Types are bridges to interfaces

This means that we have a plain function, but the target interface expects a type with a specific method — not a bare function. To bridge that gap, we define a custom named type whose underlying type matches our function's signature. We then attach a method to that type, one that satisfies the interface. Inside that method, we simply call the function itself. Finally, we convert our plain function into this custom type, which gives it the method and therefore makes it satisfy the interface.

//plain function
func myHandler(w http.ResponseWriter, r *http.Request) { 
fmt.Fprintln(w, "Hello!") 
}

// custom named type
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
f(w, r) // call the function that f holds 
}

//using it
func myHandler(w http.ResponseWriter, r *http.Request) { 
fmt.Fprintln(w, "Hello!") 
} 
// convert the function to HandlerFunc, which satisfies Handler 
http.Handle("/", http.HandlerFunc(myHandler))

Idiomatic Go and Generics

Changing a function that has an interface parameter to a function with a generic type parameter would make the function call 30% slower.

type Ager interface {
	age() int
}

func doubleAge(a Ager) int {
	return a.age() * 2
}

//into:

func doubleAgeGeneric[T Ager](a T) int {
	return a.age() * 2
}