Unlock Your Python Backend Career: Build 30 Projects in 30 Days. Join now for just $54

Go Essentials

Go Essentials

Go Essentials

Go Custom Types, Methods, and Interfaces

Go Custom Types, Methods, and Interfaces

In Go, custom types, methods, and interfaces are crucial in designing well-organized and reusable code. They allow you to define abstractions and behaviors that fit your application requirements. Let's explore these concepts while handling user authentication to demonstrate custom types, methods, and interfaces in Go.

Custom Types

We may use custom types to provide more context and safety when working with user-related data in user authentication.

package main

import "fmt"

type UserID int64 // 
type UserName string

type User struct {
    ID   UserID
    Name UserName
}

func main() {
    user := User{
        ID:   123456,
        Name: "john_doe",
    }

    fmt.Println("User ID:", user.ID)
    fmt.Println("Username:", user.Name)
}

Methods

Go allows you to define methods on types, including custom types. Methods are functions that have a receiver argument, which specifies the type the method operates on. This enables you to define behaviors that are closely associated with the type.

For ease of explanation, let's define a method for calculating the area of a circle:

package main

import "fmt"

type Circle struct {
    radius float64
}

func (c Circle) calculateArea() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    circle := Circle{radius: 5}

    fmt.Printf("Circle Area with radius %.2f: %.2f\n", circle.radius, circle.calculateArea())
}

In simple terms, we made the area function to be a property of a type of object called a Circle with a radius. The (c Circle) declaration before the function name is called the receiver, which shows us that the Circle type supports the function calculateArea.

Methods can be useful for implementing user-related behaviors, such as hashing passwords for secure storage. Let's see a more practical example:

package main

import (
	"fmt"
	"golang.org/x/crypto/bcrypt"
)

type User struct {
	ID       int64
	Username string
	Password string
}

func (u *User) HashPassword() error {
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	u.Password = string(hashedPassword)
	return nil
}

func main() {
	user := User{
		ID:       1,
		Username: "john_doe",
		Password: "secretpassword",
	}

	err := user.HashPassword()
	if err != nil {
		fmt.Println("Error hashing password:", err)
		return
	}

	fmt.Println("Hashed Password:", user.Password)
}

Here, we made HashPassword a function attributed to a User. Notice that in the shapes code, an asterisk is absent before the function name, but an asterisk is present in the password hashing code. Go uses parameters of pointer type to indicate that parameter might be modified by the function. The same rules apply to method receivers, too. They can be pointer receivers (the type is a pointer) or value receivers (the type is a value type). The following rules help you determine when to use each kind of receiver:

  • If your method modifies the receiver, you must use a pointer receiver.

  • If your method needs to handle nil instances, it must use a pointer receiver.

  • You can use a value receiver if your method doesn't modify the receiver.

Whether or not you use a value receiver for a method that doesn't modify the receiver depends on the other methods declared on the type. When a type has any pointer receiver methods, a common practice is to be consistent and use pointer receivers for all methods, even the ones that don't modify the receiver.

Interfaces

Interfaces in Go provide a way to define a set of methods that a type must implement to be considered as implementing that interface. This enables polymorphism and allows you to write more generic and flexible code. Let's extend our calculateArea code to use an interface:

package main

import "fmt"

// Shape interface stores the functions/operations possible on Shapes.
type Shape interface {
    calculateArea() float64
}

// Circle is a shape with a radius.
// It inherits Shape properties, meaning it's calculateArea can be performed on it.
type Circle struct {
    radius float64
}

// calculateArea extends all properties of Shape interface on an instance of a Circle.
// It calculates the area of the instance.
func (c Circle) calculateArea() float64 {
    return 3.14 * c.radius * c.radius
}

// Square is another shape with side.
// Likewise, it inherits Shape properties.
type Square struct {
    side float64
}

// calculateArea extends all properties of Shape interface on an instance of a Square.
// It calculates the area of the instance.
func (s Square) calculateArea() float64 {
    return s.side * s.side
}

// printArea is a function that accepts any shape, doesn't modify it, but prints out it's area.
func printArea(shape Shape) {
    fmt.Printf("Area: %.2f\n", shape.calculateArea())
}

func main() {
    circle := Circle{radius: 5}
    square := Square{side: 4}

    printArea(circle) // Output: Area: 78.50
    printArea(square) // Output: Area: 16.00
}

Interfaces can be used to create a common authentication service for different authentication providers.

package main

import "fmt"

type AuthProvider interface {
	Authenticate(username, password string) (bool, error)
}

type LocalAuthProvider struct{}

func (a *LocalAuthProvider) Authenticate(username, password string) (bool, error) {
	// Logic for authenticating against a local user database
	return true, nil
}

type ExternalAuthProvider struct{}

func (a *ExternalAuthProvider) Authenticate(username, password string) (bool, error) {
	// Logic for authenticating against an external authentication service
	return true, nil
}

func AuthenticateUser(authProvider AuthProvider, username, password string) (bool, error) {
	return authProvider.Authenticate(username, password)
}

func main() {
	localAuthProvider := &LocalAuthProvider{}
	externalAuthProvider := &ExternalAuthProvider{}

	username, password := "john_doe", "secretpassword"

	localAuthResult, _ := AuthenticateUser(localAuthProvider, username, password)
	fmt.Println("Local Authentication Result:", localAuthResult)

	externalAuthResult, _ := AuthenticateUser(externalAuthProvider, username, password)
	fmt.Println("External Authentication Result:", externalAuthResult)
}

In this example, we create two authentication providers, LocalAuthProvider and ExternalAuthProvider, that implement the AuthProvider interface. Using the same function call, we can use the AuthenticateUser function to authenticate users against different providers. Running this code on the Go Playground will print out a hashed form of the password.

In general, remember that your code should accept interfaces and return structs.

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board