|
|
- <p>Golang is incredibly powerful. Its standard library has so much to offer, but I think other languages have encouraged the
- use of external libraries for even the most basic tasks. For example, with JavaScript, most inexperienced developers
- seem to use jQuery to do simple tasks like selecting an element and replacing its contents. When you and I both know
- jQuery is way overkill for such a task.
- <a href="/posts/coming-soon">See my article on Vanilla JS basics</a>.</p>
- <p>I believe that in order to be considered an expert in a language, you must at least be able to demonstrate using the core
- language to achieve your goal. In our current case, HTTP routing. Now to be clear, I don't think you need to write everything
- from scratch all the time, but you should have a firm grasp on what is available by the core language, and when you are
- better suited to use an external library. If you are looking for more advanced HTTP routing, then I would suggest using
- something like
- <a href="https://github.com/gin-gonic/gin">gin</a>.</p>
- <p>Enough ranting, let's get to it.</p>
- <h2 id="assumptions">Assumptions</h2>
- <p>I assume you have basic knowledge of the Go language at this point, so if not, it might be worth searching for some entry
- level basics first. See
- <a href="https://tour.golang.org">A Tour of Go</a>.</p>
- <h2 id="lets-begin">Let's begin</h2>
- <p>The accompanying repo for the code produced in this article is located
- <a href="https://github.com/leothelocust/basic-http-routing-in-golang">on github</a>.</p>
- <h3 id="step-1">Step 1</h3>
- <p>Here is our basic folder structure for this basic http routing example:</p>
- <pre><code> basic-http-routing-in-golang/
- main.go
- </code></pre>
- <p>As a starting point our
- <code>main.go</code> file looks like this:</p>
- <pre><code> package main
-
- import (
- "fmt"
- _ "net/http"
- )
-
- func main() {
- fmt.Println("Hello HTTP")
- }
- </code></pre>
- <h3 id="step-2">Step 2</h3>
- <p>Now starting at a very basic level, we can leverage the
- <a href="https://golang.org/pkg/net/http/#HandleFunc">
- <code>http.HandleFunc</code>
- </a> method.</p>
- <p>It is very simple to use and its signature is easy to understand.</p>
- <pre><code> func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
- </code></pre>
- <p>Which basically means,
- <code>http.HandleFunc("/url", routingFunction)</code> where
- <code>routingFunction</code> looks like this:</p>
- <pre><code> func routingFunction(w http.ResponseWriter, req *http.Request) {
- fmt.Fprint(w, "Hello HTTP")
- }
- </code></pre>
- <p>With
- <code>fmt.Fprint()</code> we can pass an
- <code>http.ResponseWriter</code> and a message to display. Our browser will now look like this when we visit the
- <code>/url</code> endpoint.</p>
- <p>
- <img src="/images/step2-browser-output.png" alt="Browser Output for Step 2 - Hello HTTP">
- </p>
- <p>Here is what
- <code>main.go</code> looks like at this point:</p>
- <pre><code> package main
-
- import (
- "fmt"
- "log"
- "net/http"
- )
-
- func main() {
- http.HandleFunc("/hello", helloHTTP)
- log.Fatal(http.ListenAndServe(":8080", nil))
- }
-
- func helloHTTP(w http.ResponseWriter, req *http.Request) {
- fmt.Fprint(w, "Hello HTTP")
- }
- </code></pre>
- <p>Now we could stop there, as this is a "basic" http routing example, but I think it isn't quite useful as an example
- yet, until we start to see something slightly more practical.</p>
- <h3 id="step-3">Step 3</h3>
- <p>So let's add a
- <code>NotFound</code> page when we don't match a pattern in
- <code>HandleFunc</code>. It's as simple as:</p>
- <pre><code> func notFound(w http.ResponseWriter, req *http.Request) {
- http.NotFound(w, req)
- }
- </code></pre>
- <p>Here is what
- <code>main.go</code> looks like after that:</p>
- <pre><code> package main
-
- import (
- "fmt"
- "log"
- "net/http"
- )
-
- func main() {
- http.HandleFunc("/hello", helloHTTP)
- http.HandleFunc("/", notFound)
- log.Fatal(http.ListenAndServe(":8080", nil))
- }
-
- func helloHTTP(w http.ResponseWriter, req *http.Request) {
- fmt.Fprint(w, "Hello HTTP")
- }
-
- func notFound(w http.ResponseWriter, req *http.Request) {
- http.NotFound(w, req)
- }
- </code></pre>
- <p>This will match
- <code>/hello</code> and use the
- <code>HelloHTTP</code> method to print "Hello HTTP" to the browser. Any other URLs will get caught by the
- <code>/</code> pattern and be given the
- <code>http.NotFound</code> response to the browser.</p>
- <p>So that works, but I think we can go further.</p>
- <h3 id="step-4">Step 4</h3>
- <p>We need to give ourselves something more specific than the simple contrived
- <code>/hello</code> endpoint above. So let's assume we are needing to get a user profile. We will use the url
- <code>/user/:id</code> where
- <code>:id</code> is an identifier used to get the user profile from our persistance layer (i.e. our database).</p>
- <p>We'll start by creating a new method for this GET request called
- <code>userProfile</code>:</p>
- <pre><code> func userProfile(w http.ResponseWriter, req *http.Request) {
- userID := req.URL.Path[len("/user/"):]
- fmt.Fprintf(w, "User Profile: %q", userID)
- }
- </code></pre>
- <p>Notice that we get the URL from the
- <code>req</code> variable and we treat the string returned from
- <code>req.URL.Path</code> as a byte slice to get everything after the
- <code>/user/</code> in the string.
- <strong>Note: this isn't fool proof,
- <code>/user/10ok</code> would get matched here, and we would be assigning
- <code>userID</code> to
- <code>"10ok"</code>.</strong>
- </p>
- <p>Let's add this new route in our
- <code>main</code> function:</p>
- <pre><code> func main() {
- http.HandleFunc("/hello", helloHTTP)
- http.HandleFunc("/user/", userProfile)
- http.HandleFunc("/", notFound)
- log.Fatal(http.ListenAndServe(":8080", nil))
- }
- </code></pre>
- <p>
- <em>Note: that this pattern
- <code>/user/</code> matches the trailing
- <code>/</code> so that a call to
- <code>/user</code> in the browser would return a
- <code>404 Not Found</code>.</em>
- </p>
- <h3 id="step-5">Step 5</h3>
- <p>Ok, so we have introduced some pretty severe holes in the security of our new HTTP router. As mentioned in a note above,
- treating the
- <code>req.URL.Path</code> as a byte slice and just taking the last half is a terrible idea. So let's fix this:</p>
- <pre><code> var validPath = regexp.MustCompile("^/(user)/([0-9]+)$")
-
- func getID(w http.ResponseWriter, req *http.Request) (string, error) {
- m := validPath.FindStringSubmatch(req.URL.Path)
- if m == nil {
- http.NotFound(w, req)
- return "", errors.New("Invalid ID")
- }
- return m[2], nil // The ID is the second subexpression.
- }
- </code></pre>
- <p>Now we can use this method in our code:</p>
- <pre><code> func userProfile(w http.ResponseWriter, req *http.Request) {
- userID, err := getID(w, req)
- if err != nil {
- return
- }
- fmt.Fprintf(w, "User Profile: %q", userID)
- }
- </code></pre>
- <h2 id="conclusion">Conclusion</h2>
- <p>For now, I'm calling this "Basic HTTP Routing in Golang" article finished. But I do plan to add more to it as time
- allows. Additionally, I'd like to create a more advanced article that discusses the ability to respond to not only GET
- requests, but also POST, PUT, and DELETE HTTP methods. Look for an "Advanced HTTP routing in Golang" article
- in the future. Thanks for reading this far. I wish you well in your Go endeavors.</p>
|