Edit

Share via


Tutorial: Use variant feature flags in a Go Gin web application

In this tutorial, you use a variant feature flag to manage experiences for different user segments in an example application, Quote of the Day. You utilize the variant feature flag created in Use variant feature flags. Before proceeding, ensure you create the variant feature flag named Greeting in your App Configuration store.

Prerequisites

Set up a Go Gin web application

  1. Create a new directory for your Go project and navigate into it:

    mkdir quote-of-the-day
    cd quote-of-the-day
    
  2. Initialize a new Go module:

    go mod init quote-of-the-day
    
  3. Install the required Go packages:

    go get github.com/gin-gonic/gin
    go get github.com/gin-contrib/sessions
    go get github.com/gin-contrib/sessions/cookie
    go get github.com/microsoft/Featuremanagement-Go/featuremanagement
    go get github.com/microsoft/Featuremanagement-Go/featuremanagement/providers/azappconfig
    
  4. Create a templates directory for your HTML templates and add the required HTML files:

    mkdir templates
    

    Add the following HTML template files from the GitHub repo and place them in the templates directory:

  5. Create a file named appconfig.go with the following content. You can connect to your App Configuration store using Microsoft Entra ID (recommended) or a connection string.

    package main
    
    import (
        "context"
        "log"
        "os"
    
        "github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration"
        "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
    )
    
    func loadAzureAppConfiguration(ctx context.Context) (*azureappconfiguration.AzureAppConfiguration, error) {
        // Get the endpoint from environment variable
        endpoint := os.Getenv("AZURE_APPCONFIG_ENDPOINT")
        if endpoint == "" {
            log.Fatal("AZURE_APPCONFIG_ENDPOINT environment variable is not set")
        }
    
        // Create a credential using DefaultAzureCredential
        credential, err := azidentity.NewDefaultAzureCredential(nil)
        if err != nil {
            log.Fatalf("Failed to create credential: %v", err)
        }
    
        // Set up authentication options with endpoint and credential
        authOptions := azureappconfiguration.AuthenticationOptions{
            Endpoint:   endpoint,
            Credential: credential,
        }
    
        // Set up options to enable feature flags
        options := &azureappconfiguration.Options{
            FeatureFlagOptions: azureappconfiguration.FeatureFlagOptions{
                Enabled: true,
                RefreshOptions: azureappconfiguration.RefreshOptions{
                    Enabled: true,
                },
            },
        }
    
        // Load configuration from Azure App Configuration
        appConfig, err := azureappconfiguration.Load(ctx, authOptions, options)
        if err != nil {
            log.Fatalf("Failed to load configuration: %v", err)
        }
    
        return appConfig, nil
    }
    

Use the variant feature flag

  1. Create a file named main.go with the following content:

    package main
    
    import (
        "context"
        "fmt"
        "log"
        "net/http"
        "strings"
    
        "github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration"
        "github.com/gin-contrib/sessions"
        "github.com/gin-contrib/sessions/cookie"
        "github.com/gin-gonic/gin"
        "github.com/microsoft/Featuremanagement-Go/featuremanagement"
        "github.com/microsoft/Featuremanagement-Go/featuremanagement/providers/azappconfig"
    )
    
    type Quote struct {
        Message string `json:"message"`
        Author  string `json:"author"`
    }
    
    type WebApp struct {
        featureManager *featuremanagement.FeatureManager
        appConfig      *azureappconfiguration.AzureAppConfiguration
        quotes         []Quote
    }
    
    func main() {
        // Load Azure App Configuration
        appConfig, err := loadAzureAppConfiguration(context.Background())
        if err != nil {
            log.Fatalf("Error loading Azure App Configuration: %v", err)
        }
    
        // Create feature flag provider
        featureFlagProvider, err := azappconfig.NewFeatureFlagProvider(appConfig)
        if err != nil {
            log.Fatalf("Error creating feature flag provider: %v", err)
        }
    
        // Create feature manager
        featureManager, err := featuremanagement.NewFeatureManager(featureFlagProvider, nil)
        if err != nil {
            log.Fatalf("Error creating feature manager: %v", err)
        }
    
        // Initialize quotes
        quotes := []Quote{
            {
                Message: "You cannot change what you are, only what you do.",
                Author:  "Philip Pullman",
            },
        }
    
        // Create web app
        app := &WebApp{
            featureManager: featureManager,
            appConfig:      appConfig,
            quotes:         quotes,
        }
    
        // Setup Gin with default middleware (Logger and Recovery)
        r := gin.Default()
    
        // Start server
        if err := r.Run(":8080"); err != nil {
            log.Fatalf("Failed to start server: %v", err)
        }
    
        fmt.Println("Starting Quote of the Day server on http://localhost:8080")
        fmt.Println("Open http://localhost:8080 in your browser")
        fmt.Println()
    }
    
  2. Enable configuration and feature flag refresh from Azure App Configuration with the middleware.

    // Existing code
    // ... ...
    
    func (app *WebApp) refreshMiddleware() gin.HandlerFunc {
        return func(c *gin.Context) {
            go func() {
                if err := app.appConfig.Refresh(context.Background()); err != nil {
                    log.Printf("Error refreshing configuration: %v", err)
                }
            }()
            c.Next()
        }
    }
    // The rest of existing code
    //... ...
    
  3. Set up the routes with the following content:

    // Existing code
    // ... ...
    
    func (app *WebApp) setupRoutes(r *gin.Engine) {
        // Setup sessions
        store := cookie.NewStore([]byte("secret-key-change-in-production"))
        store.Options(sessions.Options{
            MaxAge:   3600, // 1 hour
            HttpOnly: true,
            Secure:   false, // Set to true in production with HTTPS
        })
        r.Use(sessions.Sessions("session", store))
    
        r.Use(app.refreshMiddleware())
    
        // Load HTML templates
        r.LoadHTMLGlob("templates/*.html")
        // Routes
        r.GET("/", app.homeHandler)
        r.GET("/login", app.loginPageHandler)
        r.POST("/login", app.loginHandler)
        r.GET("/logout", app.logoutHandler)
    }
    
    // Home page handler
    func (app *WebApp) homeHandler(c *gin.Context) {
        session := sessions.Default(c)
        username := session.Get("username")
        quote := app.quotes[0]
    
        var greetingMessage string
        var targetingContext featuremanagement.TargetingContext
        if username != nil {
            // Create targeting context for the user
            targetingContext = createTargetingContext(username.(string))
    
            // Get the Greeting variant for the current user
            if variant, err := app.featureManager.GetVariant("Greeting", targetingContext); err != nil {
                log.Printf("Error getting Greeting variant: %v", err)
            } else if variant != nil && variant.ConfigurationValue != nil {
                // Extract the greeting message from the variant configuration
                if configValue, ok := variant.ConfigurationValue.(string); ok {
                    greetingMessage = configValue
                }
            }
        }
    
        c.HTML(http.StatusOK, "index.html", gin.H{
            "title":           "Quote of the Day",
            "user":            username,
            "greetingMessage": greetingMessage,
            "quote":           quote,
        })
    }
    
    func (app *WebApp) loginPageHandler(c *gin.Context) {
        c.HTML(http.StatusOK, "login.html", gin.H{
            "title": "Login - Quote of the Day",
        })
    }
    
    func (app *WebApp) loginHandler(c *gin.Context) {
        email := strings.TrimSpace(c.PostForm("email"))
    
        // Basic validation
        if email == "" {
            c.HTML(http.StatusOK, "login.html", gin.H{
                "title": "Login - Quote of the Day",
                "error": "Email cannot be empty",
            })
            return
        }
    
        if !strings.Contains(email, "@") {
            c.HTML(http.StatusOK, "login.html", gin.H{
                "title": "Login - Quote of the Day",
                "error": "Please enter a valid email address",
            })
            return
        }
    
        // Store email in session
        session := sessions.Default(c)
        session.Set("username", email)
        if err := session.Save(); err != nil {
            log.Printf("Error saving session: %v", err)
        }
    
        c.Redirect(http.StatusFound, "/")
    }
    
    func (app *WebApp) logoutHandler(c *gin.Context) {
        session := sessions.Default(c)
        session.Clear()
        if err := session.Save(); err != nil {
            log.Printf("Error saving session: %v", err)
        }
        c.Redirect(http.StatusFound, "/")
    }
    
    // Helper function to create TargetingContext
    func createTargetingContext(userID string) featuremanagement.TargetingContext {
        targetingContext := featuremanagement.TargetingContext{
            UserID: userID,
            Groups: []string{},
        }
    
        if strings.Contains(userID, "@") {
            parts := strings.Split(userID, "@")
            if len(parts) == 2 {
                domain := parts[1]
                targetingContext.Groups = append(targetingContext.Groups, domain) // Add domain as group
            }
        }
    
        return targetingContext
    }
    // The rest of existing code
    //... ...
    
  4. Update the main.go with the following content:

    // Existing code
    // ... ...
     r := gin.Default()
    
     // Setup routes
     app.setupRoutes(r)
    
     // Start server
     if err := r.Run(":8080"); err != nil {
        log.Fatalf("Failed to start server: %v", err)
    }
    // The rest of existing code
    // ... ...
    

Build and run the app

  1. Set the environment variable for authentication and run the application:

    go mod tidy
    go run .
    
  2. Open your browser and navigate to http://localhost:8080. Select Login at the top right to sign in as usera@contoso.com.

    Screenshot of Gin web app before user login.

  3. Once logged in, you see a long greeting message for usera@contoso.com.

    Screenshot of Gin web app, showing a long message for the user.

  4. Click Logout and login as userb@contoso.com, you see the simple greeting message.

    Screenshot of Gin web app, showing a simple message for the user.

    Note

    It's important for the purpose of this tutorial to use these names exactly. As long as the feature has been configured as expected, the two users should see different variants.

Next steps

To learn more about feature management in Go, continue to the following documents: