Go Programming Language Tutorial (Part 3)
This tutorial focuses on interfaces, testing techniques, error handling best practices, concurrency
patterns, and package development.
1. Interfaces
What is an Interface?
An interface in Go defines a set of method signatures. A type implements an interface if it defines those
methods.
Defining and Using an Interface
go
Copy code
package main
import "fmt"
// Define an interface
type Shape interface {
Area() float64
Perimeter() float64
}
// Circle type
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * 3.14 * c.Radius
}
// Rectangle type
type Rectangle struct {
Length, Width float64
}
func (r Rectangle) Area() float64 {
return r.Length * r.Width
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Length + r.Width)
}
func main() {
// Polymorphism with interface
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Length: 10, Width: 4},
}
for _, shape := range shapes {
fmt.Println("Area:", shape.Area(), "Perimeter:", shape.Perimeter())
}
}
2. Advanced Error Handling
Custom Error Types
Create custom error types to provide more context.
go
Copy code
package main
import (
"fmt"
)
// Custom error type
type DivideError struct {
Dividend, Divisor int
}
func (e DivideError) Error() string {
return fmt.Sprintf("cannot divide %d by %d", e.Dividend, e.Divisor)
}
func divide(a, b int) (int, error) {
if b == 0 {
return 0, DivideError{Dividend: a, Divisor: b}
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
3. Advanced Testing
Table-Driven Tests
Table-driven testing is an efficient way to test multiple cases.
go
Copy code
package main
import "testing"
// Function to test
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"Add positive numbers", 1, 2, 3},
{"Add negatives", -1, -2, -3},
{"Add mixed", 10, -5, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Expected %d, got %d", tt.expected, result)
}
})
}
}
Mocking with Interfaces
Use interfaces to create mock implementations for testing.
go
Copy code
package main
import (
"fmt"
"testing"
)
// Service interface
type Service interface {
GetData() string
}
// Real implementation
type RealService struct{}
func (r RealService) GetData() string {
return "Real Data"
}
// Mock implementation for testing
type MockService struct{}
func (m MockService) GetData() string {
return "Mock Data"
}
// Function using the service
func Process(service Service) string {
return service.GetData()
}
func TestProcess(t *testing.T) {
mock := MockService{}
result := Process(mock)
if result != "Mock Data" {
t.Errorf("Expected 'Mock Data', got '%s'", result)
}
}
4. Concurrency Patterns
Worker Pools
Worker pools distribute tasks among multiple Goroutines.
go
Copy code
package main
import (
"fmt"
"sync"
)
// Worker function
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
var wg sync.WaitGroup
// Start workers
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, jobs, results, &wg)
}
// Send jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Wait for workers to finish
wg.Wait()
close(results)
// Collect results
for result := range results {
fmt.Println("Result:", result)
}
}
Select Statement
Use select to wait on multiple channels.
go
Copy code
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch1 <- "Channel 1"
}()
go func() {
time.Sleep(1 * time.Second)
ch2 <- "Channel 2"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
}
}
}
5. Package Development
Creating a Custom Package
1. Create the following structure:
go
Copy code
mypackage/
├── main.go
├── math/
│ └── math.go
2. Define a package in math/math.go:
go
Copy code
package math
// Function in the package
func Add(a, b int) int {
return a + b
}
3. Use the package in main.go:
go
Copy code
package main
import (
"fmt"
"mypackage/math"
)
func main() {
fmt.Println("Sum:", math.Add(2, 3))
}
4. Build and run:
bash
Copy code
go run main.go
6. Building HTTP APIs
Basic HTTP API
go
Copy code
package main
import (
"encoding/json"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func getUser(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "John Doe"}
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("/user", getUser)
http.ListenAndServe(":8080", nil)
}
7. Managing Dependencies
Using go mod
1. Initialize a module:
bash
Copy code
go mod init example.com/myproject
2. Add dependencies:
bash
Copy code
go get github.com/gin-gonic/gin
3. Example with the gin package:
go
Copy code
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
r.Run(":8080")
}
This tutorial explores key intermediate and advanced Go features. It prepares you for real-world Go
development, including creating reusable packages, managing dependencies, and building scalable
applications. Dive deeper into advanced concurrency, reflection, and Go's standard library for further
learning!