Library / SDK
dghubble/sling avatar
dghubble/sling

Sling's New() exists because the alternative is a concatenated URL

A Go HTTP client library for creating and sending API requests

1,719 stars122 forksGoMIT

At a glance

What is it?
Sling is a Go HTTP client library that accumulates request properties and then produces a standard http.Request. Its one genuinely surprising rule is that extending a Sling requires an explicit New() call, because a child shares the parent's path otherwise.
Who is it for?
Sling earns a place in a Go codebase that wraps several third-party APIs with url tagged parameter structs, because QueryStruct, BodyJSON and ReceiveSuccess remove three repetitive blocks without hiding the standard library request. It is the wrong choice for a service with one simple call site, and you should check its release history first, since the newest tag is v1.4.2 from 2024-02-26 even though the repository received a push on 2026-09-08.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 45 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A Sling is a builder that ends in Request()

Sling stores HTTP request properties and turns them into a standard http.Request when you call Request(). Nothing about the result is Sling specific, so it composes with any *http.Client, including one with your own timeout, transport and redirect policy.

The smallest example sets a path, encodes a struct into the query and produces a request:

go
type Params struct {
    Count int `url:"count,omitempty"`
}
params := &Params{Count: 5}

req, err := sling.New().Get("https://example.com").QueryStruct(params).Request()
client.Do(req)

The feature list in the README is short enough to be a design statement: method setters for Get, Post, Put, Patch, Delete and Head, Add or Set for headers, Base and Path for endpoints, struct encoding into query parameters, form or JSON bodies, and decoding of JSON success or failure responses.

Installation is a single line, with no subpackage to import:

code
go get github.com/dghubble/sling

go.mod shows why the struct tags work at all: there is exactly one dependency, github.com/google/go-querystring at v1.2.0, and a go directive of 1.19.

Path extends a base URL, and the method setters are Path plus a verb

Path sets or extends the URL, and extension means the new path is resolved relative to the existing one:

go
// creates a GET request to https://example.com/foo/bar
req, err := sling.New().Base("https://example.com/").Path("foo/").Path("bar").Request()

Each method setter behaves identically to Path except that it also sets the HTTP method. The README lists Get, Post, Put, Patch, Delete, Head, Options, Trace and Connect, so the verb list is broader than the feature list at the top, which names only six.

Headers use Add or Set, following the standard library distinction where Set replaces and Add appends. The common pattern is to configure a base once and reuse it:

go
s := sling.New().Base(baseUrl).Set("User-Agent", "Gophergram API Client")
req, err := s.New().Get("gophergram/list").Request()

Setting a User-Agent once on the base and deriving per-request Sling objects is the shape most client code settles into.

The base.New() rule, and the bug it prevents

This is the part worth memorising, because the failure it prevents is silent and produces a valid but wrong URL.

Each Sling instance has a New() method that creates an independent copy, so properties set on the child do not mutate the parent. The intended usage looks like this:

go
const twitterApi = "https://api.twitter.com/1.1/"
base := sling.New().Base(twitterApi).Client(authClient)

// statuses/show.json Sling
tweetShowSling := base.New().Get("statuses/show.json").QueryStruct(params)
req, err := tweetShowSling.Request()

Without those base.New() calls, the README explains, tweetShowSling and tweetPostSling would reference the base Sling, and the result would be a POST to "https://api.twitter.com/1.1/statuses/show.json/statuses/update.json".

Both paths are valid strings, the request builds without error, and the failure surfaces as a 404 from the API rather than as a panic. If your client package starts returning 404s on endpoints that exist, this is the first thing to check.

The rule in one line: to extend a Sling, create a child copy with New().

Three body setters, three Content-Type headers

Body handling is split by encoding. BodyJSON and BodyForm set matching Content-Type headers, while plain Body leaves that header to the caller.

BodyJSON takes a struct with json tags and the request will include an application/json Content-Type header. The struct is the one you would pass to encoding/json anyway:

go
body := &IssueRequest{
    Title: "Test title",
    Body:  "Some issue",
}
req, err := githubBase.New().Post(path).BodyJSON(body).Request()

BodyForm is the older style, taking a struct with url tags and producing application/x-www-form-urlencoded. Slice fields are supported with an explicit comma option, which is a go-querystring feature rather than a Sling one:

go
type StatusUpdateParams struct {
    Status             string   `url:"status,omitempty"`
    InReplyToStatusId  int64    `url:"in_reply_to_status_id,omitempty"`
    MediaIds           []int64  `url:"media_ids,omitempty,comma"`
}

Body takes a plain io.Reader, in which case you set the content type yourself with Set("Content-Type", "text/plain"). That third path is the escape hatch for streaming uploads, and it is the only one where the header is not automatic.

QueryStruct is the same encoder applied to the query string, which is why the url tag appears on both sides: the same struct can define an outbound filter and an inbound form body.

Receive and ReceiveSuccess split the 2XX boundary for you

The response side follows the same pattern of narrow helpers. ReceiveSuccess sends the request and decodes the body into the value you pass only when the response succeeds:

go
issues := new([]Issue)
resp, err := githubBase.New().Get(path).QueryStruct(params).ReceiveSuccess(issues)
fmt.Println(issues, resp, err)

Receive takes two values. It decodes a 2XX response into the first and a non-2XX response into the second, which is what most APIs need because failures carry a JSON error body:

go
type GithubError struct {
    Message string `json:"message"`
    Errors  []struct {
        Resource string `json:"resource"`
        Field    string `json:"field"`
        Code     string `json:"code"`
    } `json:"errors"`

Note that ReceiveSuccess hides the response body on failure, so a client that needs the error details must use Receive. Both return the *http.Response alongside the error, which is how you reach status codes and headers for cases the decoder does not model.

The repository keeps the working clients in examples/, with github.go as a separate module alongside its own go.mod, so the README examples are runnable rather than illustrative.

Small repository, small dependency list, quiet release history

The source is four Go files plus a test: body.go, response.go, sling.go and sling_test.go, with the repository root holding no directories beyond .github and examples. The Makefile is three targets, and the default target runs all of them:

code
all: test vet fmt

where test runs go test with coverage, vet runs go vet with all analyzers, and fmt fails the build if go fmt would change anything. That is a deliberately narrow CI surface for a library this small.

The release history is the thing to weigh. The most recent tags are v1.4.2 from 2024-02-26, v1.4.1 from 2022-12-07 and v1.4.0 from 2021-10-02, while the last push was on 2026-09-08. So the code is being touched but the version line has not moved in roughly two and a half years.

For a library whose entire surface is request building, that is not alarming. There is little to break, and the one dependency is stable. It does mean that if you need a behaviour that arrived upstream in a newer HTTP standard library, you are waiting on a maintainer to cut a release, not on the repository to merge a change.

Editorial conclusion

Sling earns a place in a Go codebase that wraps several third-party APIs with url tagged parameter structs, because QueryStruct, BodyJSON and ReceiveSuccess remove three repetitive blocks without hiding the standard library request. It is the wrong choice for a service with one simple call site, and you should check its release history first, since the newest tag is v1.4.2 from 2024-02-26 even though the repository received a push on 2026-09-08. One dependency, go-querystring at v1.2.0, and a go directive of 1.19 keep the supply chain surface small.

Frequently asked questions

How do I install Sling?

Run `go get github.com/dghubble/sling` and import the root package. There is no subpackage, and go.mod declares a single dependency, github.com/google/go-querystring at v1.2.0.

Why do I need to call New() on a Sling?

Without base.New(), a derived Sling references the base and its path, and a request built from it can end up pointing at a concatenated URL such as "https://api.twitter.com/1.1/statuses/show.json/statuses/update.json". New() creates an independent copy so the parent is not mutated.

How do I decode an error response body with Sling?

Use Receive with two values, a success struct and a failure struct. It decodes a 2XX response into the first and a non-2XX response into the second. ReceiveSuccess only decodes 2XX bodies.

Which Content-Type does BodyForm set?

BodyForm url encodes a url tagged struct and sets application/x-www-form-urlencoded. BodyJSON sets application/json, and Body takes a plain io.Reader where you set the content type yourself.

Official sources

  1. dghubble/sling on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dghubble-sling.svg)](https://hysenlabs.com/projects/dghubble-sling)

Was this useful?