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.