Go SDK 4.x to 5.0 migration guide
Read time: 16 minutes
Last edited: May 01, 2024
Overview
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 gopkg.in
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 gopkg.in
are exempt from that rule.
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 github.com/launchdarkly
or gopkg.in/launchdarkly
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:
-
gopkg.in/launchdarkly/go-sdk-server.v5
- Main package: types that are specific to server-side SDK usage, such as
Config
,LDClient
, andFeatureFlagsState
. /interfaces
: types likeDataStore
that are only used if you are writing a custom component, as well as interfaces for advanced features likeFlagTracker
./ldcomponents
: configuration builders for specific areas of SDK functionality, such as configuring streaming options or logging options. To learn more, read Understanding changes to SDK configuration.
- Main package: types that are specific to server-side SDK usage, such as
-
gopkg.in/launchdarkly/go-sdk-common.v2
/ldlog
: the SDK's logging abstraction (formerly ingo-server-sdk.v4/ldlog
)./ldreason
: theEvaluationDetail
andEvaluationReason
types (formerly ingo-server-sdk.v4
)./lduser
:User
,UserBuilder
, and related methods (formerly ingo-server-sdk.v4
)./ldvalue
: the universal JSON value typeValue
, and related types likeOptionalString
.
-
gopkg.in/launchdarkly/go-sdk-events.v1
,gopkg.in/launchdarkly/go-server-sdk-evaluation.v1
: 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 "gopkg.in/launchdarkly/go-server-sdk.v4")config := ld.DefaultConfig// 4.x model: setting custom options for streaming modeconfig.Stream = trueconfig.StreamInitialReconnectDelay = 500*time.Millisecond// 4.x model: specifying polling mode and setting custom polling optionsconfig.Stream = falseconfig.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 "gopkg.in/launchdarkly/go-server-sdk.v4"ldredis "gopkg.in/launchdarkly/go-server-sdk.v4/redis")config := ld.DefaultConfig// 4.x model: use Redis, set custom Redis URI and key prefix, set cache TTL to 45 secondsconfig.FeatureStoreFactory = redis.NewRedisFeatureStoreWithDefaults(ldredis.URI("redis://my-redis-host"),ldredis.Prefix("my-key-prefix"),ldredis.CacheTTL(45*time.Second))
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 "gopkg.in/launchdarkly/go-server-sdk.v4")config := ld.DefaultConfig// 4.x model: disabling eventsconfig.SendEvents = false// 4.x model: customizing event behaviorconfig.Capacity = 20000config.FlushInterval = 10*time.Secondconfig.PrivateAttributeNames = []string{"email","name","myCustomAttribute",}
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 gopkg.in/launchdarkly/go-sdk-common.v2
rather than gopkg.in/launchdarkly/go-server-sdk.v4
.
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 "gopkg.in/launchdarkly/go-server-sdk.v4""gopkg.in/launchdarkly/go-server-sdk.v4/ldlog")config := ld.DefaultConfig// 4.x model: disabling loggingconfig.Loggers = ldlog.NewDisabledLoggers()// 4.x model: setting log level to Warnconfig.Loggers.SetMinLevel(ldlog.Warn)// 4.x model: specifying that evaluation errors should be loggedconfig.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 "gopkg.in/launchdarkly/go-server-sdk.v4""gopkg.in/launchdarkly/go-server-sdk.v4/ldhttp")config := ld.DefaultConfig// 4.x model: setting socket connection timeoutconfig.Timeout = 3*time.Second// 4.x model: specifying a secure HTTPS proxy with a custom CA certificateconfig.HTTPClientFactory = ld.NewHTTPClientFactory(ldhttp.ProxyOption(proxyURL),ldhttp.CACertOption("mycert.crt"),)
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 gopkg.in/launchdarkly/go-sdk-common.v2/lduser
instead of gopkg.in/launchdarkly/go-server-sdk.v4
.
Here is an example:
import (ld "gopkg.in/launchdarkly/go-server-sdk.v4")// before 4.16: create a simple user with just a keyuser := ld.NewUser("key")// before 4.16: set email and countryuser := ld.NewUser("key")email := "sandy@example.com"country := "us"user.Email = &emailuser.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{}
:
- It could represent any type at all, even ones that are not valid in a LaunchDarkly feature flag, and
- 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 gopkg.in/launchdarkly/go-sdk-common.v2/ldvalue
instead of gopkg.in/launchdarkly/go-sdk-common.v1/ldvalue
.
Here is an example:
import (ld "gopkg.in/launchdarkly/go-server-sdk.v4")// before 4.16: get a JSON flag variation whose default value is "default",// and check if the result is a string or a numberresult, _ := client.JsonVariation(flagKey, user, "default")if s, ok := result.(string); ok {DoSomethingWithString(s)} else if n, ok := result.(float64); ok {DoSomethingWithNumberAsInt(int(n))}// 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 gopkg.in/launchdarkly/go-server-sdk.v4
and are now in gopkg.in/launchdarkly/go-sdk-common.v2/ldreason
.
We have modified them in two ways:
EvaluationReason
is now a struct rather than an interface. Instead of casting it to another type such asEvaluationReasonRuleMatch
to get specific properties, it provides getter methods for those properties.- The
VariationIndex
property ofEvaluationDetail
was previously a pointer of type*int
, which would benil
if evaluation failed. This was unsafe due to mutability and the potential for nil pointer panics. It now uses the immutable typeldvalue.OptionalInt
instead, which allows an "undefined" value to be represented without using pointers.
Here is an example:
import (ld "gopkg.in/launchdarkly/go-server-sdk.v4")value, detail, _ := client.BoolVariationDetail(flagKey, user, false)// 4.x model: check if the variation index is 0if 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 calledAllFlags
and is in the new packagego-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 theinterfaces
package without causing an import cycle. - The getter methods of
AllFlags
are slightly different than the methods of the earlierFeatureFlagsState
, 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 theflagstate
package, and have been renamed toOptionClientSideOnly
,OptionDetailsOnlyForTrackedFlags
, andOptionWithReasons
.
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: github.com/launchdarkly/go-server-sdk-redis-redigo
, github.com/launchdarkly/go-server-sdk-dynamodb
, and github.com/launchdarkly/ldconsul
.
The new import paths for those packages are the same as the repository locations: github.com/launchdarkly/go-server-sdk-redis-redigo
, 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 "gopkg.in/launchdarkly/go-server-sdk.v4"ldredis "gopkg.in/launchdarkly/go-server-sdk.v4/redis")config := ld.DefaultConfigrelayURI := "http://my-relay-host:8000"// 4.x model: proxy modeconfig.BaseURI = relayURIconfig.StreamURI = relayURIconfig.EventsURI = relayURI // if you want to proxy events// 4.x model: daemon mode with a Redis databaseconfig.UseLdd = trueconfig.FeatureStoreFactory = ldredis.NewRedisFeatureStoreWithDefaults(ldredis.URI("redis://my-redis-host"),ldredis.Prefix("my-key-prefix"))
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:
- You can tell the SDK to notify you whenever a feature flag's configuration has changed, using
LDClient.GetFlagTracker()
. - You can monitor the status of the SDK's connection to LaunchDarkly services with
LDClient.GetDataSourceStatusProvider()
. - You can monitor the status of a persistent data store (for instance, to get caching statistics, or to be notified if the store's availability changes due to a database outage) with
LDClient.GetDataStoreStatusProvider()
. - You can disable analytics events temporarily for specific flag evaluation calls with
LDClient.WithEventsDisabled()
. - You can configure the SDK's use of different logging levels depending on the duration of a service outage with
LDConfig.Logging
andldcomponents.LoggingConfigurationBuilder
. - For test code, you can use the new
ldtestdata
package to set flag values or rules programmatically, as an alternative to usingldfiledata
.
To learn more, read the API documentation for these classes and methods.