2 ms·
Go doesn't have direct sum types, but we can use interfaces and type switches: package main import "fmt" // Define types for our "sum type" t
by greenavocado 2y ago
Go doesn't have direct sum types, but we can use interfaces and type switches:
package main
import "fmt"
// Define types for our "sum type"
type Success struct {
Value string
}
type Error struct {
Message string
}
// Interface for our sum type
type Result interface {
isResult()
}
// Implement the interface
func (s Success) isResult() {}
func (e Error) isResult() {}
// Pattern matching using type switch
func handleResult(r Result) string {
switch v := r.(type) {
case Success:
return fmt.Sprintf("Success: %s", v.Value)
case Error:
return fmt.Sprintf("Error: %s", v.Message)
default:
// Go requires a default case, which can help catch new types
panic("Unhandled result type")
}
}
func main() {
result := Success{Value: "Operation completed"}
fmt.Println(handleResult(result))
}
Here is a practical example of it:
package main
import (
"fmt"
"time"
)
// Common interface for our "sum type"
type Notification interface {
Send() string
isNotification() // marker method
}
// Email notification
type EmailNotification struct {
To string
Subject string
Body string
}
func (n EmailNotification) Send() string {
return fmt.Sprintf("Email sent to %s with subject '%s'", n.To, n.Subject)
}
func (EmailNotification) isNotification() {}
// SMS notification
type SMSNotification struct {
PhoneNumber string
Message string
}
func (n SMSNotification) Send() string {
return fmt.Sprintf("SMS sent to %s", n.PhoneNumber)
}
func (SMSNotification) isNotification() {}
// Push notification
type PushNotification struct {
DeviceToken string
Title string
Message string
ExpiresAt time.Time
}
func (n PushNotification) Send() string {
return fmt.Sprintf("Push notification sent to device %s", n.DeviceToken)
}
func (PushNotification) isNotification() {}
// Function that handles different notification types
func ProcessNotification(notification Notification) {
// Type switch for pattern matching
switch n := notification.(type) {
case EmailNotification:
fmt.Printf("Processing Email: %s\n", n.Send())
fmt.Printf("Email details - To: %s, Subject: %s\n", n.To, n.Subject)
case SMSNotification:
fmt.Printf("Processing SMS: %s\n", n.Send())
fmt.Printf("SMS length: %d characters\n", len(n.Message))
case PushNotification:
fmt.Printf("Processing Push: %s\n", n.Send())
timeToExpiry := time.Until(n.ExpiresAt)
fmt.Printf("Push expires in: %v\n", timeToExpiry)
default:
// This catches any future notification types that we haven't handled
fmt.Println("Unknown notification type")
}
}
// Function to record notifications in different ways based on type
func LogNotification(notification Notification) string {
timestamp := time.Now().Format(time.RFC3339)
switch n := notification.(type) {
case EmailNotification:
return fmt.Sprintf("[%s] EMAIL: To=%s Subject=%s",
timestamp, n.To, n.Subject)
case SMSNotification:
return fmt.Sprintf("[%s] SMS: To=%s",
timestamp, n.PhoneNumber)
case PushNotification:
return fmt.Sprintf("[%s] PUSH: Device=%s Title=%s ExpiresAt=%s",
timestamp, n.DeviceToken, n.Title, n.ExpiresAt.Format(time.RFC3339))
default:
return fmt.Sprintf("[%s] UNKNOWN notification type", timestamp)
}
}
func main() {
// Create different notification types
email := EmailNotification{
To: "user@example.com",
Subject: "Important Update",
Body: "Hello, this is an important update about your account.",
}
sms := SMSNotification{
PhoneNumber: "+1234567890",
Message: "Your verification code is 123456",
}
push := PushNotification{
DeviceToken: "device-token-abc123",
Title: "New Message",
Message: "You have a new message from a friend",
ExpiresAt: time.Now().Add(24 * time.Hour),
}
// Process notifications
fmt.Println("=== Processing Notifications ===")
ProcessNotification(email)
fmt.Println()
ProcessNotification(sms)
fmt.Println()
ProcessNotification(push)
// Log notifications
fmt.Println("\n=== Logging Notifications ===")
fmt.Println(LogNotification(email))
fmt.Println(LogNotification(sms))
fmt.Println(LogNotification(push))
// We can also store different notification types in a slice
notifications := []Notification{email, sms, push}
fmt.Println("\n=== Processing Notification Queue ===")
for i, notification := range notifications {
fmt.Printf("Item %d: %s\n", i+1, LogNotification(notification))
}
}
The marker method pattern isNotification() prevents other types that happen to have a Send() method from being considered notifications. T
- jimbokun 2y agoA bit cumbersome, but a nice workaround all the same!
- flakes 2y agoThe problem I have here, is that by being an interface you’ve suddenly made the type nilable. This leads to nasty bugs and segfaults, especially where nil is the default construct for all interfaces. Ideally sum types would be concrete/non-nilable somehow.
- greenavocado 2y agoThe workaround is even more cancerous but you could wrap it in a struct: type NotificationWrapper struct { // This field holds the actual notification data Value interface{} } And use it like: func NewPushNotification(token, title, message string, expires time.Time) NotificationWrapper { return NotificationWrapper{ Value: PushNotification{ DeviceToken: token, Title: title, Message: message, ExpiresAt: expires, }, } } And destructure it with something like func ProcessNotification(notification NotificationWrapper) string { switch n := notification.Value.(type) { Roll it all up in a nice syntax with a preprocessor haha
- flakes 2y agoThat still has the same issues depending on the usage. The default construct here is `NotificationWrapper{nil}`. If you want to actually guard it, you also need to make the Value field private as that nil becomes part of your public api otherwise. type NotificationWrapper struct { // This field holds the actual notification data value interface{} } Within `ProcessNotification` you'd also want to always assert the struct is initialized. You have to handle that error case via `err` or `panic`. With a true sum type, you could be assured that the value is always concrete, removing the need for error handling, which otherwise complicates the business logic of your application. Errors that should be safeguarded against by the compiler become run time checks, eating up cycles on the CPU, or paniced upon, potentially leading to segfaults which can only be caught by tests (not asserted valid via the act of compiling).