My ham website
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

184 lines
8.0 KiB

  1. <h1 id="basic-http-routing-in-golang">Basic HTTP Routing in Golang</h1>
  2. <p>Golang is incredibly powerful. Its standard library has so much to offer, but I think other languages have encouraged the
  3. use of external libraries for even the most basic tasks. For example, with JavaScript, most inexperienced developers
  4. seem to use jQuery to do simple tasks like selecting an element and replacing its contents. When you and I both know
  5. jQuery is way overkill for such a task.
  6. <a href="https://leviolson.com/posts/vanilla-js-basics">See my article on Vanilla JS basics</a>.</p>
  7. <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
  8. language to achieve your goal. In our current case, HTTP routing. Now to be clear, I don't think you need to write everything
  9. from scratch all the time, but you should have a firm grasp on what is available by the core language, and when you are
  10. better suited to use an external library. If you are looking for more advanced HTTP routing, then I would suggest using
  11. something like
  12. <a href="https://github.com/gin-gonic/gin">gin</a>.</p>
  13. <p>Enough ranting, let's get to it.</p>
  14. <h2 id="assumptions">Assumptions</h2>
  15. <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
  16. level basics first. See
  17. <a href="https://tour.golang.org">A Tour of Go</a>.</p>
  18. <h2 id="lets-begin">Let's begin</h2>
  19. <p>The accompanying repo for the code produced in this article is located
  20. <a href="https://github.com/leothelocust/basic-http-routing-in-golang">on github</a>.</p>
  21. <h3 id="step-1">Step 1</h3>
  22. <p>Here is our basic folder structure for this basic http routing example:</p>
  23. <pre><code> basic-http-routing-in-golang/
  24. main.go
  25. </code></pre>
  26. <p>As a starting point our
  27. <code>main.go</code> file looks like this:</p>
  28. <pre><code> package main
  29. import (
  30. &quot;fmt&quot;
  31. _ &quot;net/http&quot;
  32. )
  33. func main() {
  34. fmt.Println(&quot;Hello HTTP&quot;)
  35. }
  36. </code></pre>
  37. <h3 id="step-2">Step 2</h3>
  38. <p>Now starting at a very basic level, we can leverage the
  39. <a href="https://golang.org/pkg/net/http/#HandleFunc">
  40. <code>http.HandleFunc</code>
  41. </a> method.</p>
  42. <p>It is very simple to use and its signature is easy to understand.</p>
  43. <pre><code> func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
  44. </code></pre>
  45. <p>Which basically means,
  46. <code>http.HandleFunc(&quot;/url&quot;, routingFunction)</code> where
  47. <code>routingFunction</code> looks like this:</p>
  48. <pre><code> func routingFunction(w http.ResponseWriter, req *http.Request) {
  49. fmt.Fprint(w, &quot;Hello HTTP&quot;)
  50. }
  51. </code></pre>
  52. <p>With
  53. <code>fmt.Fprint()</code> we can pass an
  54. <code>http.ResponseWriter</code> and a message to display. Our browser will now look like this when we visit the
  55. <code>/url</code> endpoint.</p>
  56. <p>
  57. <img src="https://leviolson.com/images/step2-browser-output.png" alt="Browser Output for Step 2 - Hello HTTP">
  58. </p>
  59. <p>Here is what
  60. <code>main.go</code> looks like at this point:</p>
  61. <pre><code> package main
  62. import (
  63. &quot;fmt&quot;
  64. &quot;log&quot;
  65. &quot;net/http&quot;
  66. )
  67. func main() {
  68. http.HandleFunc(&quot;/hello&quot;, helloHTTP)
  69. log.Fatal(http.ListenAndServe(&quot;:8080&quot;, nil))
  70. }
  71. func helloHTTP(w http.ResponseWriter, req *http.Request) {
  72. fmt.Fprint(w, &quot;Hello HTTP&quot;)
  73. }
  74. </code></pre>
  75. <p>Now we could stop there, as this is a &quot;basic&quot; http routing example, but I think it isn't quite useful as an example
  76. yet, until we start to see something slightly more practical.</p>
  77. <h3 id="step-3">Step 3</h3>
  78. <p>So let's add a
  79. <code>NotFound</code> page when we don't match a pattern in
  80. <code>HandleFunc</code>. It's as simple as:</p>
  81. <pre><code> func notFound(w http.ResponseWriter, req *http.Request) {
  82. http.NotFound(w, req)
  83. }
  84. </code></pre>
  85. <p>Here is what
  86. <code>main.go</code> looks like after that:</p>
  87. <pre><code> package main
  88. import (
  89. &quot;fmt&quot;
  90. &quot;log&quot;
  91. &quot;net/http&quot;
  92. )
  93. func main() {
  94. http.HandleFunc(&quot;/hello&quot;, helloHTTP)
  95. http.HandleFunc(&quot;/&quot;, notFound)
  96. log.Fatal(http.ListenAndServe(&quot;:8080&quot;, nil))
  97. }
  98. func helloHTTP(w http.ResponseWriter, req *http.Request) {
  99. fmt.Fprint(w, &quot;Hello HTTP&quot;)
  100. }
  101. func notFound(w http.ResponseWriter, req *http.Request) {
  102. http.NotFound(w, req)
  103. }
  104. </code></pre>
  105. <p>This will match
  106. <code>/hello</code> and use the
  107. <code>HelloHTTP</code> method to print &quot;Hello HTTP&quot; to the browser. Any other URLs will get caught by the
  108. <code>/</code> pattern and be given the
  109. <code>http.NotFound</code> response to the browser.</p>
  110. <p>So that works, but I think we can go further.</p>
  111. <h3 id="step-4">Step 4</h3>
  112. <p>We need to give ourselves something more specific than the simple contrived
  113. <code>/hello</code> endpoint above. So let's assume we are needing to get a user profile. We will use the url
  114. <code>/user/:id</code> where
  115. <code>:id</code> is an identifier used to get the user profile from our persistance layer (i.e. our database).</p>
  116. <p>We'll start by creating a new method for this GET request called
  117. <code>userProfile</code>:</p>
  118. <pre><code> func userProfile(w http.ResponseWriter, req *http.Request) {
  119. userID := req.URL.Path[len(&quot;/user/&quot;):]
  120. fmt.Fprintf(w, &quot;User Profile: %q&quot;, userID)
  121. }
  122. </code></pre>
  123. <p>Notice that we get the URL from the
  124. <code>req</code> variable and we treat the string returned from
  125. <code>req.URL.Path</code> as a byte slice to get everything after the
  126. <code>/user/</code> in the string.
  127. <strong>Note: this isn't fool proof,
  128. <code>/user/10ok</code> would get matched here, and we would be assigning
  129. <code>userID</code> to
  130. <code>&quot;10ok&quot;</code>.</strong>
  131. </p>
  132. <p>Let's add this new route in our
  133. <code>main</code> function:</p>
  134. <pre><code> func main() {
  135. http.HandleFunc(&quot;/hello&quot;, helloHTTP)
  136. http.HandleFunc(&quot;/user/&quot;, userProfile)
  137. http.HandleFunc(&quot;/&quot;, notFound)
  138. log.Fatal(http.ListenAndServe(&quot;:8080&quot;, nil))
  139. }
  140. </code></pre>
  141. <p>
  142. <em>Note: that this pattern
  143. <code>/user/</code> matches the trailing
  144. <code>/</code> so that a call to
  145. <code>/user</code> in the browser would return a
  146. <code>404 Not Found</code>.</em>
  147. </p>
  148. <h3 id="step-5">Step 5</h3>
  149. <p>Ok, so we have introduced some pretty severe holes in the security of our new HTTP router. As mentioned in a note above,
  150. treating the
  151. <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>
  152. <pre><code> var validPath = regexp.MustCompile(&quot;^/(user)/([0-9]+)$&quot;)
  153. func getID(w http.ResponseWriter, req *http.Request) (string, error) {
  154. m := validPath.FindStringSubmatch(req.URL.Path)
  155. if m == nil {
  156. http.NotFound(w, req)
  157. return &quot;&quot;, errors.New(&quot;Invalid ID&quot;)
  158. }
  159. return m[2], nil // The ID is the second subexpression.
  160. }
  161. </code></pre>
  162. <p>Now we can use this method in our code:</p>
  163. <pre><code> func userProfile(w http.ResponseWriter, req *http.Request) {
  164. userID, err := getID(w, req)
  165. if err != nil {
  166. return
  167. }
  168. fmt.Fprintf(w, &quot;User Profile: %q&quot;, userID)
  169. }
  170. </code></pre>
  171. <h2 id="conclusion">Conclusion</h2>
  172. <p>For now, I'm calling this &quot;Basic HTTP Routing in Golang&quot; article finished. But I do plan to add more to it as time
  173. allows. Additionally, I'd like to create a more advanced article that discusses the ability to respond to not only GET
  174. requests, but also POST, PUT, and DELETE HTTP methods. Look for an &quot;Advanced HTTP routing in Golang&quot; article
  175. in the future. Thanks for reading this far. I wish you well in your Go endeavors.</p>