Skip to content
ExponaBLOG
GO PROGRAMMING & PRODUCTION APIS · DAY 098 / 100

Feature flags in Go services: what developers should know

Ship safely with flags.

Expona Admin6 min read

Feature flags in Go services: what developers should know

Deploying new features directly to production carries risk. Feature flags in Go services give you the ability to decouple deployment from release, letting you control who sees what and when. We'll walk through the practical patterns and tools that production teams use to ship safely, from simple configuration-based approaches to more sophisticated providers like OpenFeature.

Start with config-based flags

The simplest feature flag implementation uses environment variables or configuration files. You define a flag, check it at runtime, and route traffic accordingly. This approach works well for small teams and straightforward use cases.

Consider a Go service that calculates shipping costs. You want to test a new algorithm before rolling it out to all users. A config-based flag lets you enable it for a subset of requests:

go
package shipping

import "os"

type ShippingService struct {
	useNewAlgorithm bool
}

func NewShippingService() *ShippingService {
	return &ShippingService{
		useNewAlgorithm: os.Getenv("USE_NEW_SHIPPING_ALGO") == "true",
	}
}

func (s *ShippingService) CalculateCost(origin, destination string, weight float64) float64 {
	if s.useNewAlgorithm {
		return s.calculateCostV2(origin, destination, weight)
	}
	return s.calculateCostV1(origin, destination, weight)
}

func (s *ShippingService) calculateCostV1(origin, destination string, weight float64) float64 {
	// Legacy calculation
	return weight * 2.5
}

func (s *ShippingService) calculateCostV2(origin, destination string, weight float64) float64 {
	// New calculation with distance optimization
	return weight * 2.0
}

This pattern is predictable and requires no external dependencies. The downside is that changes require redeployment. For a canary release where you need to gradually expose the flag to 5% of users, then 25%, then 100%, you'd need multiple deployments.

Use OpenFeature for providers

OpenFeature is a vendor-neutral specification for feature flag management. It abstracts flag evaluation behind a common interface, letting you swap providers—Unleash, LaunchDarkly, Split.io—without changing your application code.

Install the Go SDK and a provider. We'll use the file-based provider for simplicity, though production teams typically use a service provider:

go
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/open-feature/go-sdk/openfeature"
	"github.com/open-feature/go-sdk-contrib/providers/file-provider/pkg"
)

func main() {
	provider, err := fileprovider.NewFileProvider("/etc/flags.json")
	if err != nil {
		log.Fatalf("Failed to create provider: %v", err)
	}

	openfeature.SetProvider(provider)
	client := openfeature.NewClient("shipping")

	ctx := context.Background()

	// Evaluate a boolean flag
	enabled, err := client.BooleanValue(ctx, "new_shipping_algo", false)
	if err != nil {
		log.Fatalf("Failed to evaluate flag: %v", err)
	}

	if enabled {
		fmt.Println("Using new algorithm")
	}
}

Your flags configuration file looks like:

go
{
  "flags": {
    "new_shipping_algo": {
      "state": "ENABLED",
      "variants": {
        "on": true,
        "off": false
      },
      "defaultVariant": "on"
    }
  }
}

OpenFeature separates flag definition from evaluation logic. You can change flags without touching code, and providers handle distribution across your infrastructure. Many providers also support real-time updates—your services get flag changes without restarting.

Evaluate flags with context

Context-aware evaluation is where feature flags become powerful. Instead of a simple on/off, you check properties like user ID, region, plan tier, or request metadata to decide whether a flag applies.

Imagine you're rolling out a premium shipping feature. You want it enabled for users on the Enterprise plan first, then Standard, then everyone:

go
package main

import (
	"context"

	"github.com/open-feature/go-sdk/openfeature"
)

type EvaluationContext struct {
	UserID      string
	PlanTier    string
	Region      string
	BetaTester  bool
}

func evaluateShippingFlag(ctx context.Context, client *openfeature.Client, user EvaluationContext) bool {
	evalCtx := openfeature.NewEvaluationContext(user.UserID, map[string]interface{}{
		"plan_tier":   user.PlanTier,
		"region":      user.Region,
		"beta_tester": user.BetaTester,
	})

	enabled, err := client.BooleanValue(ctx, "premium_shipping", false, openfeature.WithEvalContext(evalCtx))
	if err != nil {
		// Handle error, fall back to false
		return false
	}

	return enabled
}

func main() {
	// In your handler
	user := EvaluationContext{
		UserID:   "user_456",
		PlanTier: "Enterprise",
		Region:   "US-East",
	}

	ctx := context.Background()
	client := openfeature.NewClient("shipping")

	if evaluateShippingFlag(ctx, client, user) {
		// Show premium shipping options
	}
}

Your provider stores rules that interpret this context. A rule might say: "Enable premium_shipping for Enterprise tier users in US-East." Another might say: "Enable for 10% of Standard tier users." Context makes your rollout strategy dynamic and testable.

This approach also enables targeting: you can enable a flag for specific users (beta testers), gradual rollouts (percentage-based), or geographic phased releases without code changes.

Remove flags after rollout

Feature flags are technical debt. After a flag completes its lifecycle—you've shipped the feature, validated it, and rolled it out to 100% of users—delete it. Leaving dead flags clutters your codebase and creates mental overhead during debugging.

Set a removal date when you create a flag. Document which code paths are controlled by the flag. After the flag is fully rolled out and stable, delete the flag check and the legacy code path:

go
// Before: with flag
func (s *ShippingService) CalculateCost(origin, destination string, weight float64) float64 {
	if s.useNewAlgorithm {
		return s.calculateCostV2(origin, destination, weight)
	}
	return s.calculateCostV1(origin, destination, weight)
}

// After: flag removed, new logic is default
func (s *ShippingService) CalculateCost(origin, destination string, weight float64) float64 {
	return s.calculateCostV2(origin, destination, weight)
}

// Delete calculateCostV1 entirely

Track flags in your team's backlog. Create a cleanup ticket when a flag reaches 100% adoption, ideally within 1–2 weeks. This discipline prevents flags from becoming permanent and keeps your production code clean.

Common mistakes

  • Forgetting to remove old code paths. Dead code accumulates and makes branches harder to reason about. Flag removal should be as routine as flag creation.
  • Using flags for permissions or security decisions. Flags control feature visibility, not access control. Use proper authentication and authorization for sensitive operations.
  • Not testing flag combinations. If you have three flags, you have eight possible states. Test the combinations that matter: all off, all on, and critical mixed states.
  • Ignoring performance in evaluation. Every request evaluates flags. If your provider is slow or unreachable, your API slows or fails. Use caching and fallback mechanisms.
  • Conflating flags with configuration. Flags control features; configuration controls behavior (timeout values, database URLs). Keep them separate.

Key takeaways

  • Start simple with config-based flags for small teams; graduate to OpenFeature when you need flexibility and multi-provider support.
  • Use evaluation context to enable sophisticated targeting: gradual rollouts, user segments, regional phasing.
  • Treat flag removal as a first-class task. Set expiration dates, document dependencies, and clean up after a flag completes its lifecycle.
  • Cache flag evaluations and implement fallback behavior to avoid performance degradation when your flag provider is slow or down.
  • Test your flag combinations, especially mixed states, to catch logic errors early.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles