HTTP stands for Hypertext Transfer Protocol and at its core, it is the set of rules that defines how data is exchanged between a web browser (the client) and a web server whenever you type a URL into your address bar or click a link, your browser sends an HTTP request to a server somewhere and this server will sent you what you need HTTP operates on a simple cycle:

  1. The Request: The client sends a message to the server, this message includes a Method like GET to fetch data or POST to send data, a path to the resource, and headers providing metadata.
  2. The Response: The server processes the request and sends back a message, this includes a Status Code like 200 OK if everything went well, or 404 Not Found if the page is missing and the actual content of the page.

this requests and responses look like this Pasted image 20260504101949.png

the red rectangle is the request method

  • HTTP has more than method and these methods inform the server what are we trying to accomplish using this request
  • so if the method is GET we are telling the server we are just getting this page
    • sometimes it is used to send data to the server but it isn't a safe way to do it
  • if it is a POST so we are sending data to the server
  • and there is some other methods like PUT, DELETE, PATCH, TRACE, HEAD

and the arrow is the path of the resource

  • and this tells the server which resource we are applying that action on
  • so if we are trying to delete a profile it will look like this /profile/id for example

GET Requests

lets start with the simple one GET Requests there is two ways to do a GET request

  1. simple one if we don't need custom headers or behavior
  2. custom headers and behavior way lets start with the easy one
go
http.Get(url)

lets say we need to fetch a page

go
http.Get("http://google.com")

what does this return ? it returns an http response (pointer) and an error this http response isn't just the response text but every thing about it, the status, the status code, the actual body, and the headers the status and status code and headers are easy to read but the Body has a trick you should know about

the resp.Body not a string, it is an io.ReadCloser which an interface representing a stream of bytes coming over the network and we can read it and close it the close part is easy just defer respo.Body.Close() to avoid any memory leaks but the challenge is the Read() we don't know what is the response size so we can't just create a buffer with a fixed size and we shouldn't be bothered by checking the size that's why using io.ReadAll()to drain the entire stream into a []byte slice just so you know this io.ReadAll() accepts anything that implements the io.Reader interface like a file or a buffer or Stdin

with all that being said, if we need to make a request to fetch google.com/robots.txt it would look like that

this was the easy way lets see what we can do if we need to modify the behavior like adding a timeout or proxy or having custom headers for example now the http.Get uses the default Client so it doesn't provide all that control and we have to create a client and a request this client will customize the client behavior like timeouts and proxies the request will customize the req like having custom headers

and we create a client using http.Client{} and then add the custom behavior in those brackets and for the request we create it using this http.NewRequest(method, url, body) and because this is a GET request the body will be just nil

so lets do the same request this way

go
package main

import (
    "net/http"
    "strings"
    "time"
)

func main() {
    client := &http.Client{Timeout: 10 * time.Second} // this tells the server if the response takes more than 10 seconds to return, return an error instead

    req, _ := http.NewRequest("GET", "http://localhost:8080/", nil)
    // note: body is nil for GET
    req.Header.Set("Authorization", "token123")
    req.Header.Set("Accept", "application/json")

    resp, _ := client.Do(req)
    defer resp.Body.Close()
}

and just to prove a point that you can put a body in GET request but the server will silently ignore it

go
package main

import (
    "net/http"
    "strings"
    "time"
)

func main() {
    client := &http.Client{Timeout: 10 * time.Second}

    body := strings.NewReader("hi")
    req, _ := http.NewRequest("GET", "http://localhost:8080/", body)
    // note: body is nil for GET
    req.Header.Set("Authorization", "Bearer token123")
    req.Header.Add("Authorization", "Bearer token456")
    req.Header.Set("Accept", "application/json")

    resp, _ := client.Do(req)
    defer resp.Body.Close()
}

and in some strict servers it might reject the entire request

if you need to add a new headers you will use either one of those

go
req.Header.Set("Content-Type", "application/json") //set a new pair, if the key already exists replace it with the new one
req.Header.Add("Authorization", "Bearer TOKEN") //append a new pair, it can lead to duplicate headers 

so what happened when we used http.Get() it just used the default client like this

go
package main

import (
    "io"
    "net/http"
    "os"
)

func main() {
    client := http.DefaultClient
    req, _ := http.NewRequest("GET", "http://127.0.0.1:8080", nil)
    resp1, _ := client.Do(req)
    io.Copy(os.Stdout, resp1.Body)

    resp2, _ := http.Get("http://127.0.0.1:8080")
    io.Copy(os.Stdout, resp2.Body)

}

and those two are the exact same thing

Query Parameters using GET Request

we can just append them to the URL if it is a fixed value we can do

go
http.Get("http://something.com/?q=123")

if it is some kind of input we can format it as a string first

go
    value1 := "123"
    url := fmt.Sprintf("http://127.0.0.1:8080/?q=%s", value1)
    http.Get(url)  

but the proper way is through the url.Values for the values to get encoded if there is any special characters

go
func main() {
    client := http.DefaultClient
    req, _ := http.NewRequest("GET", "http://127.0.0.1:8080", nil)
    q := url.Values{} // creates an empty map
    q.Add("q", "123") // adds first param
    q.Add("a", "drama") // adds second param
    req.URL.RawQuery = q.Encode() // convert it to string and encode it then attach it to the url
    client.Do(req)
}

the issue with this way that if there is any parameters in the URL it'll be overwritten completely but we can preserve the existing ones by using this

go
    client := http.DefaultClient
    req, _ := http.NewRequest("GET", "http://127.0.0.1:8080", nil)
    q := req.URL.Query() // query the existing ones first and adds them to the map
    q.Add("q", "123")
    q.Add("a", "drama")
    req.URL.RawQuery = q.Encode()
    client.Do(req)

and we're done with GET Requests see you in the next one