Search code examples
gogo-http

Go http client setup for multiple endpoints?


I reuse the http client connection to make external calls to a single endpoint. An excerpt of the program is shown below:

var AppCon MyApp

func New(user, pass string, platformURL *url.URL, restContext string) (*MyApp, error) {
  if AppCon == (MyApp{}) {
      AppCon = MyApp{
          user:        user,
          password:    pass,
          URL:         platformURL,
          Client:      &http.Client{Timeout: 30 * time.Second},
          RESTContext: restContext,
      }

      cj, err := cookiejar.New(nil)
      if err != nil {
          return &AppCon, err
      }

      AppCon.cookie = cj
  }

    return &AppCon, nil
}

// This is an example only. There are many more functions which accept *MyApp as a pointer.
func(ma *MyApp) GetUser(name string) (string, error){
   // Return user
}

func main(){   
    for {
      // Get messages from a queue 
      // The message returned from the queue provide info on which methods to call
      // 'm' is a struct with message metadata

      c, err := New(m.un, m.pass, m.url)

      go func(){
        // Do something i.e c.GetUser("123456")
      }()
    }
}

I now have the requirement to set up a client connections with different endpoints/credentials received via queue messages.

The problem I foresee is I can't just simply modify AppCon with the new endpoint details since a pointer to MyApp is returned, resulting in resetting c. This can impact a goroutine making a HTTP call to an unintended endpoint. To make matters non trivial, the program is not meant to have awareness of the endpoints (I was considering using a switch statement) but rather receive what it needs via queue messages.

Given the issues I've called out are correct, are there any recommendations on how to solve it?

EDIT 1

Based on the feedback provided, I am inclined to believe this will solve my problem:

  1. Remove the use of a Singleton of MyApp
  2. Decouple the http client from MyApp which will enable it for reuse
var httpClient *http.Client

func New(user, pass string, platformURL *url.URL, restContext string) (*MyApp, error) {

    AppCon = MyApp{
          user:        user,
          password:    pass,
          URL:         platformURL,
          Client:      func() *http.Client {
      if httpClient == nil {
        httpClient = &http.Client{Timeout: 30 * time.Second}
      }
        return httpClient
    }()
          RESTContext: restContext,
      }

    return &AppCon, nil
}

// This is an example only. There are many more functions which accept *MyApp as a pointer.
func(ma *MyApp) GetUser(name string) (string, error){
   // Return user
}

func main(){   
    for {
      // Get messages from a queue 
      // The message returned from the queue provide info on which methods to call
      // 'm' is a struct with message metadata

      c, err := New(m.un, m.pass, m.url)

      // Must pass a reference
      go func(c *MyApp){
        // Do something i.e c.GetUser("123456")
      }(c)
    }
}

Solution

  • Disclaimer: this is not a direct answer to your question but rather an attempt to direct you to a proper way of solving your problem.

    • Try to avoid a singleton pattern for you MyApp. In addition, New is misleading, it doesn't actually create a new object every time. Instead you could be creating a new instance every time, while preserving the http client connection.
    • Don't use constructions like this: AppCon == (MyApp{}), one day you will shoot in your leg doing this. Use instead a pointer and compare it to nil.
    • Avoid race conditions. In your code you start a goroutine and immediately proceed to the new iteration of the for loop. Considering you re-use the whole MyApp instance, you essentially introduce a race condition.
    • Using cookies, you make your connection kinda stateful, but your task seems to require stateless connections. There might be something wrong in such an approach.