No results for ""
  • Home
  • API docs


Go SDK 4.x to 5.0 migration guide

Read time: 16 minutes
Last edited: May 01, 2024


This topic explains how to adapt code that currently uses a 4.x version of the Go server-side SDK to use version 5.0 or later.

Before you migrate to 5.0, update to the latest 4.x version. Some of the changes that are mandatory in 5.0 were originally added in a 4.x version and made optional.

If you update to the latest 4.x version, deprecation warnings appear in areas of your code that need to be changed for 5.0. You can update them at your own pace while still using 4.x, rather than migrating everything simultaneously.

To learn more about updating to the latest 4.x version, visit the SDK's GitHub repository.

Identifying supported Go versions for the 5.0 SDK

The minimum Go version for LaunchDarkly SDK 5.0 is 1.14.

LaunchDarkly no longer supports lower Go versions, as stated in the End of Life policy.

Using Go modules

Modules are supported in all modern versions of Go, and are now the standard way to write Go code. However, not all Go users use Go modules.

To support as many users as possible, we designed the 5.x SDK for use with or without modules. It has a go.mod file so it is recognized as a module, but if you use an older dependency management system like dep or govendor, the 5.x SDK will still work.

This is why we have continued to use the redirection service for the SDK's import paths. Go modules with major versions greater than 1 must normally have an import path suffix like /v5, which would not work with dep and govendor. Paths that use are exempt from that rule.

Plan to migrate to Go modules

Starting with the next major version, 6.0.0, you will only be able to use the Go SDK from module-based code.

As described below, the SDK is now made up of multiple repositories. If you do not use modules and you have pinned your dependency versions, when you update the SDK to a new version you should also update any other or dependencies that were previously pinned. This is not required if you use modules, because the SDK can specify its own preferred dependency versions.

Using updated package names

The package structure of the SDK has changed. Some packages have been added in the main repository, and other packages are now in related repositories.

There were two reasons for this. First, the main package go-server-sdk.v4 was cluttered with many things that are not necessary for normal usage of the SDK. Moving things out of this package produced a clearer basic API.

Second, there are some internal parts of the Go SDK code that are also used by other LaunchDarkly components, such as the Relay Proxy. The fact that those implementation details were visible within the main SDK package meant that they could not be changed in any backward-incompatible way without releasing a new major version of the SDK, even though application code was unlikely to be using them.

This was potentially confusing for developers, and required LaunchDarkly to maintain support for many obsolete types and methods.

The full package schema is now as follows:

  •, these packages are not used directly by application code.

The redis, ldconsul, and lddynamodb packages are now in separate repositories. To learn more, read Understanding changes to the data store.

Understanding changes to SDK configuration

Previously, the Config struct had fields for all of the SDK's configuration options. An empty Config{} struct was not a valid configuration. Applications had to first copy from ld.DefaultConfig and then make any desired changes.

Now, most of the properties are grouped into areas of functionality, each of which has its own builder class that is only available if you are using that functionality. The Config struct has a much smaller number of fields, because most of its options are contained within one of the builders.

For example, if you use streaming mode, there are options you can set to control streaming mode, and if you use polling mode, you use a different builder that does not have the streaming options. Similarly, if you have disabled analytics events, the event-related options are not accessible.

ld.DefaultConfig no longer exists. An empty Config{} is now a valid configuration, with the same default behavior that was previously provided by ld.DefaultConfig.

The basic areas of functionality are "data source" (polling, streaming, or file data), "data store" (memory or database), events, logging, and networking.

Understanding changes to data source methods

The data source is the mechanism that the SDK uses to receive feature flag updates. Typically this is a streaming connection, but you can also configure the SDK to use polling or other sources.

For each data source type, there is a factory method whose name ends in DataSource. These methods give you a builder object with methods for whatever options are appropriate. Store that object in Config.DataSource.

Here is an example:

import (
ld ""
config := ld.DefaultConfig
// 4.x model: setting custom options for streaming mode
config.Stream = true
config.StreamInitialReconnectDelay = 500*time.Millisecond
// 4.x model: specifying polling mode and setting custom polling options
config.Stream = false
config.PollInterval = 60*time.Second

Streaming mode is the default. Unlike the earlier model, it is no longer possible to construct a meaningless configuration such as "use streaming mode, but set the polling interval to 1 minute" or "disable events, but set the flush interval to 10 seconds."

Another data source option is the file-based data source. To learn more, read the API documentation for ldfiledata.DataSource().

Understanding changes to the data store

As before, the default data store is a simple in-memory cache.

If you want to use a persistent datastore database integration, you must call a DataStore factory method for that integration. This gives you a builder object with whatever options are appropriate for that database. Next, pass the object to ldcomponents.PersistentDataStore(), a wrapper that provides caching options that are not specific to any one database but are built into the SDK. Put this into Config.DataStore.

In the 4.x API, the constructors for these integrations took a variable number of arguments representing the various options you could use. For example, packagename.NewFeatureStore(packagename.OptionA(value), packagename.OptionB(value)).

The 5.0 API uses a more concise builder pattern instead, with one builder method for each option. For example, packagename.DataStore().OptionA(value).OptionB(value).

Also, the integrations for Redis, DynamoDB, and Consul are no longer bundled in the main SDK project. To learn more, read Understanding changes to add-on integration packages.

To learn more, read Persistent data stores.

The following examples use the Redis integration:

import (
ld ""
ldredis ""
config := ld.DefaultConfig
// 4.x model: use Redis, set custom Redis URI and key prefix, set cache TTL to 45 seconds
config.FeatureStoreFactory = redis.NewRedisFeatureStoreWithDefaults(

Understanding changes to events

Analytics events are enabled by default. To customize their behavior, call ldcomponents.SendEvents() to get a builder object with event-related options, and then put that object in Config.Events.

To completely disable events, set Config.Events to ldcomponents.NoEvents().

Here's how:

import (
ld ""
config := ld.DefaultConfig
// 4.x model: disabling events
config.SendEvents = false
// 4.x model: customizing event behavior
config.Capacity = 20000
config.FlushInterval = 10*time.Second
config.PrivateAttributeNames = []string{

You can no longer construct a meaningless configuration like "disable events, but set the flush interval to 10 seconds."

Understanding changes to logging configuration

The SDK still uses the ldlog logging API, although that package is now in rather than

However, for basic logging configuration, such as specifying the minimum log level or disabling logging, you no longer need to interact directly with the ldlog.Loggers type. Instead, there is a new Logging property in Config which you can set to either ldcomponents.NoLogging() or to a configuration builder that you get from ldcomponents.Logging().

Here is an example:

import (
ld ""
config := ld.DefaultConfig
// 4.x model: disabling logging
config.Loggers = ldlog.NewDisabledLoggers()
// 4.x model: setting log level to Warn
// 4.x model: specifying that evaluation errors should be logged
config.LogEvaluationErrors = true

Understanding changes to networking configuration

Options in this category affect how the SDK communicates with LaunchDarkly over HTTP/HTTPS, including connection timeout, proxy servers, and more. If you need to customize these, call ldcomponents.HTTPConfiguration() to get a builder object, configure this builder, and then put that object in Config.HTTP.

Here's how:

import (
ld ""
config := ld.DefaultConfig
// 4.x model: setting socket connection timeout
config.Timeout = 3*time.Second
// 4.x model: specifying a secure HTTPS proxy with a custom CA certificate
config.HTTPClientFactory = ld.NewHTTPClientFactory(

Using the user builder

Before Go SDK 4.16, the way to define user properties was to set the fields of the User struct directly. Because the fields had pointer types, this was somewhat inconvenient. Also, the mutability of the User struct and the Custom map inside it could cause concurrency problems.

Version 4.16 added the UserBuilder type as a safer and more convenient way to define user properties. As of version 5.0, this is the only way, if you need to use the full set of properties. The NewUser and NewAnonymousUser constructors still exist for simple users. The User struct no longer has any exported fields and cannot be modified by application code after it is built. It now has getter methods like GetName() if you need to inspect existing property values.

The 5.0 User and UserBuilder types, and the NewUser(), NewAnonymousUser(), and NewUserBuilder() functions, are now in instead of

Here is an example:

import (
ld ""
// before 4.16: create a simple user with just a key
user := ld.NewUser("key")
// before 4.16: set email and country
user := ld.NewUser("key")
email := ""
country := "us"
user.Email = &email
user.Country = &country
// before 4.16: set custom attribute "team" to "admin"
user := ld.NewUser("key")
custom := make(map[string]interface{})
custom["team"] = "admin"

Using the Value type for JSON values

Before Go SDK 4.16, if you wanted to describe a value that could be of any valid JSON type, you would use Go's universal value type interface{}.

For example, LDClient.JsonVariation() returned that type, and also took a default value parameter of that type.

However, there were two problems with interface{}:

  1. It could represent any type at all, even ones that are not valid in a LaunchDarkly feature flag, and
  2. JSON arrays and objects were represented as slices and maps, which are mutable and can cause concurrency problems.

Version 4.16 added the immutable class ldvalue.Value as a replacement for interface{}, along with a LDClient.JSONVariation() method that uses LDValue, and UserBuilder methods for setting user attributes of any JSON type.

As of version 5.0, these are the only options. interface{} is no longer used in the public API.

The Value type for the 5.0 SDK is now in instead of

Here is an example:

import (
ld ""
// before 4.16: get a JSON flag variation whose default value is "default",
// and check if the result is a string or a number
result, _ := client.JsonVariation(flagKey, user, "default")
if s, ok := result.(string); ok {
} else if n, ok := result.(float64); ok {
// before 4.16: set a user's custom attribute "teams" to an
// array of ["admin", "foosball"]
user := ld.NewUser("key")
custom := make(map[string]interface{})
custom["team"] = []interface{}{"admin", "foosball"}

Understanding changes to EvaluationDetail and EvaluationReason

The EvaluationDetail and EvaluationReason types were formerly in and are now in

We have modified them in two ways:

  1. EvaluationReason is now a struct rather than an interface. Instead of casting it to another type such as EvaluationReasonRuleMatch to get specific properties, it provides getter methods for those properties.
  2. The VariationIndex property of EvaluationDetail was previously a pointer of type *int, which would be nil if evaluation failed. This was unsafe due to mutability and the potential for nil pointer panics. It now uses the immutable type ldvalue.OptionalInt instead, which allows an "undefined" value to be represented without using pointers.

Here is an example:

import (
ld ""
value, detail, _ := client.BoolVariationDetail(flagKey, user, false)
// 4.x model: check if the variation index is 0
if detail.VariationIndex != nil && *detail.VariationIndex == 0 {
log.Printf("it is variation zero")
// 4.x model: check if the reason is "prerequisite failed"
if pf, ok := detail.Reason.(ld.EvaluationReasonPrerequisiteFailed); ok {
log.Printf("the prerequisite %s failed", pf.PrerequisiteKey)

Understanding changes to AllFlagsState

The AllFlagsState method of LDClient, which returns a snapshot of flag values and metadata, has been changed as follows:

  • The type that it returns was previously called FeatureFlagsState and was in the main SDK package. It is now called AllFlags and is in the new package go-server-sdk.v5/interfaces/flagstate. Moving it out of the main package helps to streamline the API, and also allows it to be referenced from the interfaces in the interfaces package without causing an import cycle.
  • The getter methods of AllFlags are slightly different than the methods of the earlier FeatureFlagsState, although most applications will not need to use these. There is also a builder for constructing instances of it in test code.
  • The optional parameters that can be used to change the behavior of AllFlagsState are now in the flagstate package, and have been renamed to OptionClientSideOnly, OptionDetailsOnlyForTrackedFlags, and OptionWithReasons.

Understanding changes to add-on integration packages

The Consul, DynamoDB, and Redis integrations are no longer bundled in the main SDK project. Instead, they are maintained in separate repositories:,, and

The new import paths for those packages are the same as the repository locations:, etc.

Also, the configuration syntax has changed. To learn more about configuration details, read Understanding changes to the data store.

Using the Relay Proxy

There are two ways you can configure the SDK to use the Relay Proxy.

You can use it in:

  • proxy mode, connecting to it via HTTP/HTTPS just as if it were the LaunchDarkly service.
  • daemon mode, receiving flag data only through a database.

The syntax for configuring these has changed in version 5.x.

import (
ld ""
ldredis ""
config := ld.DefaultConfig
relayURI := "http://my-relay-host:8000"
// 4.x model: proxy mode
config.BaseURI = relayURI
config.StreamURI = relayURI
config.EventsURI = relayURI // if you want to proxy events
// 4.x model: daemon mode with a Redis database
config.UseLdd = true
config.FeatureStoreFactory = ldredis.NewRedisFeatureStoreWithDefaults(

Implementing custom components

Most applications use either the default in-memory storage or one of the database integrations provided by LaunchDarkly (Consul, DynamoDB, Redis), but the data store interface (formerly called "feature store") has always been public so developers can write their own integrations.

Starting in Go SDK 5.0, this model has been changed to make it easier to implement database integrations. The basic concepts are the same. The SDK defines its own "data kinds," such as feature flags and user segments. The data store must provide a way to add and update items of any of these kinds without knowing anything about their properties except the key and version.

The main changes are:

  • Caching is now handled by the SDK, not by the store implementation. Now you only need to implement the lower-level operations of adding or updating data.
  • The SDK serializes and deserializes the data, so the store operates only on strings.
  • Data structures have been simplified to avoid the use of maps.

The interface for "data source" components that receive feature flag data, either from LaunchDarkly or from some other source, such as a file, has also changed slightly to use the new data store model.

To learn more, read the API documentation for the interfaces.DataStore, interfaces.PersistentDataStore, and interfaces.DataSource interfaces. You may also want to review the SDK's source code for one of the LaunchDarkly database integrations, such as the ldredis package, to learn how it changed from the previous major version.

Understanding what was deprecated

All types and methods that were marked as deprecated in the last 4.x release have been removed from the 5.0 release.

If you were using these with a recent version previously, you should already have received deprecation warnings at compile time, with suggestions about how to replace them.

Understanding new functionality

The 5.0 release also includes new features as described in the release notes.

These include:

To learn more, read the API documentation for these classes and methods.