├── go.mod
├── scripts
└── build.sh
├── web
└── static
│ ├── index.html
│ ├── hello.html
│ └── homepage.html
├── configs
└── server-config.go
├── .gitignore
├── .vscode
└── launch.json
├── cmd
└── web-server
│ └── golang-web-server.go
├── pkg
└── mhttp
│ ├── functions.go
│ └── server.go
├── LICENSE
├── README.md
└── tests
└── golang-mini-web-server.postman_collection.json
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/jlhidalgo/golang-web-server
2 |
3 | go 1.18
4 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | go build -o ./bin/web-server ./cmd/web-server/golang-web-server.go
--------------------------------------------------------------------------------
/web/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Index
4 |
5 |
6 | Index
7 |
8 |
9 |
--------------------------------------------------------------------------------
/web/static/hello.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello World
4 |
5 |
6 | Hello World!
7 |
8 |
9 |
--------------------------------------------------------------------------------
/web/static/homepage.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Home Page
4 |
5 |
6 | Welcome to the home page!
7 |
8 |
9 |
--------------------------------------------------------------------------------
/configs/server-config.go:
--------------------------------------------------------------------------------
1 | package configs
2 |
3 | const (
4 | SERVER_STATIC_DIRECTORY string = "./web/static"
5 | SERVER_URL string = "localhost"
6 | SERVER_PORT string = "8081"
7 | )
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | # Binary built by Go for this code
18 | bin
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Launch Package",
9 | "type": "go",
10 | "request": "launch",
11 | "mode": "auto",
12 | "program": "${fileDirname}",
13 | "args": ["handler_functions"]
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/cmd/web-server/golang-web-server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 |
7 | "github.com/jlhidalgo/golang-web-server/configs"
8 | "github.com/jlhidalgo/golang-web-server/pkg/mhttp"
9 | )
10 |
11 | var useHandlerFunctions bool
12 |
13 | func main() {
14 | flag.BoolVar(&useHandlerFunctions, "use-handler-functions", true, "Runs the Web Browser with handler functions, use false to run it in FileServer mode.")
15 | flag.Parse()
16 |
17 | server := mhttp.NewServer(configs.SERVER_STATIC_DIRECTORY, configs.SERVER_URL, configs.SERVER_PORT)
18 | var err error
19 |
20 | if useHandlerFunctions {
21 | err = server.InitializeHandlerFunctions()
22 | } else {
23 | err = server.InitializeFileServer()
24 | }
25 |
26 | if err != nil {
27 | log.Fatal(err)
28 | }
29 | server.ListenAndServe()
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/pkg/mhttp/functions.go:
--------------------------------------------------------------------------------
1 | package mhttp
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strconv"
7 | "sync"
8 | )
9 |
10 | var counter int
11 | var mutex = &sync.Mutex{}
12 | var staticDirectory string
13 |
14 | // Replies to the request with the contents of the named file or directory
15 | func serveFile(w http.ResponseWriter, r *http.Request) {
16 | http.ServeFile(w, r, staticDirectory)
17 | }
18 |
19 | // Increments a counter every time the '/increment' page is requested
20 | // additionally the counter is replied back to the web browser
21 | func incrementCounter(w http.ResponseWriter, r *http.Request) {
22 | mutex.Lock()
23 | counter++
24 | fmt.Fprint(w, strconv.Itoa(counter))
25 | mutex.Unlock()
26 | }
27 |
28 | // Just replies with the word 'Hi' as the body of the page
29 | func hiString(w http.ResponseWriter, r *http.Request) {
30 | fmt.Fprintf(w, "Hi")
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Juan Luis Hidalgo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pkg/mhttp/server.go:
--------------------------------------------------------------------------------
1 | package mhttp
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "os"
9 | )
10 |
11 | type IServer interface {
12 | InitializeFileServer() error
13 | InitializeHandlerFunctions() error
14 | ListenAndServe()
15 | }
16 |
17 | type ServerConfig struct {
18 | staticDirectory string
19 | url string
20 | port string
21 | }
22 |
23 | func NewServer(staticDir string, url string, port string) ServerConfig {
24 | var serverConfig = ServerConfig{
25 | staticDirectory: staticDir,
26 | url: url,
27 | port: port,
28 | }
29 | return serverConfig
30 | }
31 |
32 | // returns a handler that serves HTTP requests with the contents of the file system rooted at root
33 | func (fs ServerConfig) InitializeFileServer() error {
34 | fmt.Println("Initializing handler with FileServer")
35 |
36 | if err := validateFolder(fs.staticDirectory); err != nil {
37 | return errors.New("couldn't validate static folder, error: " + err.Error())
38 | }
39 |
40 | http.Handle("/", http.FileServer(http.Dir(fs.staticDirectory)))
41 | fmt.Println("Handler has been initialized")
42 | return nil
43 | }
44 |
45 | // starts listenning the port and serves the requests
46 | func (fs ServerConfig) ListenAndServe() {
47 | serverUrl := fs.url + ":" + fs.port
48 | fmt.Printf("Listening on %s...\n", serverUrl)
49 | log.Fatal(http.ListenAndServe(serverUrl, nil))
50 | }
51 |
52 | // Initializes all the handler functions
53 | func (fs ServerConfig) InitializeHandlerFunctions() error {
54 | staticDirectory = fs.staticDirectory
55 | fmt.Println("Initializing the handler functions...")
56 |
57 | if err := validateFolder(fs.staticDirectory); err != nil {
58 | return errors.New("couldn't validate static folder, error: " + err.Error())
59 | }
60 |
61 | http.HandleFunc("/", serveFile)
62 | http.HandleFunc("/increment", incrementCounter)
63 | http.HandleFunc("/hi", hiString)
64 | fmt.Println("Handler functions have been initialized")
65 | return nil
66 | }
67 |
68 | func validateFolder(folderPath string) error {
69 | fileInfo, err := os.Stat(folderPath)
70 |
71 | if err != nil {
72 | return err
73 | }
74 |
75 | if fileInfo != nil && !fileInfo.IsDir() {
76 | return fmt.Errorf("%s : is not a directory", folderPath)
77 | }
78 |
79 | return nil
80 | }
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mini Web Server in Go
2 |
3 | A very simple implementation of a **Web Server** in **Go** programming language.
4 |
5 | ## Description
6 |
7 | This project implements a very basic Web Server using nothing else than the [Go Standard Library](https://pkg.go.dev/std). This was created for educational purposes in order to explore the basic functions that are provided by the standard library for *networking*, specifically the [net/http](https://pkg.go.dev/net/http@go1.19.5#pkg-overview) package. The server is capable of performing these operations:
8 | * listen to a specific port for incoming HTTP requests, and
9 | * process the requests and then reply with the corresponding HTTP responses.
10 |
11 | The application can serve the requests by two different ways:
12 | * handler functions: the application registers individual functions to process the requests depending on specific patterns.
13 | * FileServer handler: Serves HTTP requests with the contents of the file system rooted at root.
14 |
15 | This repository also includes a [Postman collection](/tests/golang-mini-web-server.postman_collection.json) that can be used for testing the functionality of the Web server.
16 |
17 | ## Getting started
18 |
19 | ### Dependencies
20 |
21 | This is required for building and compiling the application:
22 | * Go version 1.18.x or higher
23 |
24 | Optionally you will need Postman if you want to run the tests provided [here](/tests/golang-mini-web-server.postman_collection.json).
25 | * Postman
26 |
27 | ### Installing
28 |
29 | 1. Clone this repository
30 | 2. Make the repository folder your working directory
31 | 3. Initialize the module for the application by running these commands from the terminal (give a proper name to your module)
32 | ```bash
33 | go mod init module_name
34 | go mod vendor
35 | ```
36 | 4. Update the package reference in the *import* section inside the `cmd/web-server/golang-web-server.go` file with the *module_name* that you used in the previous step:
37 | ```golang
38 | "module_name/pkg/handler"
39 | ```
40 | 5. Compile and build the application to ensure everything works fine.
41 |
42 | In Unix-like systems just execute this script:
43 | ```bash
44 | ./scripts/build.sh
45 | ```
46 |
47 | Or you can just execute this command as well:
48 | ```bash
49 | go build -o ./bin/web-server cmd/web-server/golang-web-server.go
50 | ```
51 | This should generate a binary file named `web-server` in the `bin` directory.
52 |
53 | ### Executing the program
54 |
55 | * The simplest way is to run the `web-server` binary file directly:
56 | ```bash
57 | ./bin/web-server
58 | ```
59 | Expected outcome is:
60 | ```bash
61 | Initializing the handler functions...
62 | Handler functions have been initialized
63 | Listening on localhost:8081...
64 | ```
65 | **Note**: By default, the application will be executed using handler functions and will listen to port 8081.
66 |
67 | * Alternatively you can also run the application with FileServer handler:
68 | ```bash
69 | ./bin/web-server -use-handler-functions=false
70 | ```
71 | Expected outcome is:
72 | ```bash
73 | Initializing handler with FileServer
74 | Handler has been initialized
75 | Listening on localhost:8081...
76 | ```
77 |
78 | ### Testing
79 |
80 | There are two approaches for interacting and testing the application, this can be done through a Web browser or Postman.
81 |
82 | #### Web browser
83 |
84 | Open a Web browser and go to http://localhost:8081. The `index` page should be loaded if everything is working as expected. Examine the source code for the requests that can be processed by the Web server, these vary depending on how the application is serving the requests (handler functions or FileServer handler).
85 |
86 | #### Postman
87 |
88 | Import the [collection](./tests/golang-mini-web-server.postman_collection.json) into Postman and then open the `Golang Mini Web Server` collection. The requests of this collection are grouped into two different folders, one is for handler functions implementation and the other one for FileServer handler implementation. You can run the requests within the folder that matches the current mode of the Web server.
89 |
90 | ## License
91 | This project is licensed under the MIT License - see the LICENSE file for details
92 |
93 | ## Acknowledgments
94 | Inspiration, code snippets, etc.
95 |
96 | * [Creating a simple Web server with Golang](https://tutorialedge.net/golang/creating-simple-web-server-with-golang/)
97 | * [A simple README.md template](https://gist.github.com/DomPizzie/7a5ff55ffa9081f2de27c315f5018afc)
98 | * [Standard Go project layout](https://github.com/golang-standards/project-layout)
--------------------------------------------------------------------------------
/tests/golang-mini-web-server.postman_collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "_postman_id": "9a24d3a1-6241-424e-8843-8731058e95cd",
4 | "name": "Golang Mini Web Server",
5 | "description": "# Overview\n\nThis collection contains the allowed requests for the Golang Web Server project which is hosted at: [https://github.com/jlhidalgo/golang-web-server](https://github.com/jlhidalgo/golang-web-server)\n\nThese requests will be typically used for:\n1. Running verification over the Golang Web Server in order to confirm that the application is working as expected, and\n2. Providing a guidance on how the Web Server works and what is capable of doing",
6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
7 | "_exporter_id": "6679410"
8 | },
9 | "item": [
10 | {
11 | "name": "Handler Functions",
12 | "item": [
13 | {
14 | "name": "Get root",
15 | "event": [
16 | {
17 | "listen": "test",
18 | "script": {
19 | "exec": [
20 | "pm.test(\"Status code is 200\", function () {",
21 | " pm.response.to.have.status(200);",
22 | "});",
23 | "",
24 | "pm.test(\"Index page is obtained\", function () {",
25 | " pm.expect(pm.response.text()).to.include(\"Index\");",
26 | "});"
27 | ],
28 | "type": "text/javascript"
29 | }
30 | }
31 | ],
32 | "request": {
33 | "method": "GET",
34 | "header": [],
35 | "url": {
36 | "raw": "http://localhost:8081/",
37 | "protocol": "http",
38 | "host": [
39 | "localhost"
40 | ],
41 | "port": "8081",
42 | "path": [
43 | ""
44 | ]
45 | }
46 | },
47 | "response": []
48 | },
49 | {
50 | "name": "Get hi",
51 | "event": [
52 | {
53 | "listen": "test",
54 | "script": {
55 | "exec": [
56 | "pm.test(\"Status code is 200\", function () {",
57 | " pm.response.to.have.status(200);",
58 | "});",
59 | "",
60 | "pm.test(\"Hi page is obtained\", function () {",
61 | " pm.expect(pm.response.text()).to.include(\"Hi\");",
62 | "});"
63 | ],
64 | "type": "text/javascript"
65 | }
66 | }
67 | ],
68 | "request": {
69 | "method": "GET",
70 | "header": [],
71 | "url": {
72 | "raw": "http://localhost:8081/hi",
73 | "protocol": "http",
74 | "host": [
75 | "localhost"
76 | ],
77 | "port": "8081",
78 | "path": [
79 | "hi"
80 | ]
81 | }
82 | },
83 | "response": []
84 | },
85 | {
86 | "name": "Get counter incremented",
87 | "event": [
88 | {
89 | "listen": "test",
90 | "script": {
91 | "exec": [
92 | "var localCounter = pm.collectionVariables.get(\"counter\");",
93 | "",
94 | "pm.test(\"Status code is 200\", function () {",
95 | " pm.response.to.have.status(200);",
96 | "});",
97 | "",
98 | "pm.test(\"Counter is provided in body\", function () {",
99 | " var isNumber = !isNaN(pm.response.text());",
100 | " pm.expect(isNumber).to.be.true;",
101 | "});",
102 | "",
103 | "pm.test(\"Counter has been incremented\", function () {",
104 | " var newCounter = Number.parseInt(pm.response.text());",
105 | " pm.expect(newCounter).to.be.greaterThan(Number.parseInt(localCounter));",
106 | " pm.collectionVariables.set(\"counter\", newCounter);",
107 | "});",
108 | ""
109 | ],
110 | "type": "text/javascript"
111 | }
112 | }
113 | ],
114 | "request": {
115 | "method": "GET",
116 | "header": [],
117 | "url": {
118 | "raw": "http://localhost:8081/increment",
119 | "protocol": "http",
120 | "host": [
121 | "localhost"
122 | ],
123 | "port": "8081",
124 | "path": [
125 | "increment"
126 | ]
127 | }
128 | },
129 | "response": []
130 | }
131 | ],
132 | "description": "# Description\n\nThis folder contains the requests that are served when the Golang Web Server is launched with Handler Functions. See more about this in the Readme file of the project: [https://github.com/jlhidalgo/golang-web-server/blob/main/README.md](https://github.com/jlhidalgo/golang-web-server/blob/main/README.md)"
133 | },
134 | {
135 | "name": "FileServer Handler",
136 | "item": [
137 | {
138 | "name": "Get root",
139 | "event": [
140 | {
141 | "listen": "test",
142 | "script": {
143 | "exec": [
144 | "pm.test(\"Status code is 200\", function () {",
145 | " pm.response.to.have.status(200);",
146 | "});",
147 | "",
148 | "pm.test(\"Index page is obtained\", function () {",
149 | " pm.expect(pm.response.text()).to.include(\"Index\");",
150 | "});"
151 | ],
152 | "type": "text/javascript"
153 | }
154 | }
155 | ],
156 | "request": {
157 | "method": "GET",
158 | "header": [],
159 | "url": {
160 | "raw": "http://localhost:8081/",
161 | "protocol": "http",
162 | "host": [
163 | "localhost"
164 | ],
165 | "port": "8081",
166 | "path": [
167 | ""
168 | ]
169 | }
170 | },
171 | "response": []
172 | },
173 | {
174 | "name": "Get index",
175 | "event": [
176 | {
177 | "listen": "test",
178 | "script": {
179 | "exec": [
180 | "pm.test(\"Status code is 200\", function () {",
181 | " pm.response.to.have.status(200);",
182 | "});",
183 | "",
184 | "pm.test(\"Index page is obtained\", function () {",
185 | " pm.expect(pm.response.text()).to.include(\"Index\");",
186 | "});"
187 | ],
188 | "type": "text/javascript"
189 | }
190 | }
191 | ],
192 | "request": {
193 | "method": "GET",
194 | "header": [],
195 | "url": {
196 | "raw": "http://localhost:8081/index.html",
197 | "protocol": "http",
198 | "host": [
199 | "localhost"
200 | ],
201 | "port": "8081",
202 | "path": [
203 | "index.html"
204 | ]
205 | }
206 | },
207 | "response": []
208 | },
209 | {
210 | "name": "Get index - extension not provided",
211 | "event": [
212 | {
213 | "listen": "test",
214 | "script": {
215 | "exec": [
216 | "pm.test(\"Status code is 404\", function () {",
217 | " pm.response.to.have.status(404);",
218 | "});",
219 | ""
220 | ],
221 | "type": "text/javascript"
222 | }
223 | }
224 | ],
225 | "request": {
226 | "method": "GET",
227 | "header": [],
228 | "url": {
229 | "raw": "http://localhost:8081/index",
230 | "protocol": "http",
231 | "host": [
232 | "localhost"
233 | ],
234 | "port": "8081",
235 | "path": [
236 | "index"
237 | ]
238 | }
239 | },
240 | "response": []
241 | },
242 | {
243 | "name": "Get home page",
244 | "event": [
245 | {
246 | "listen": "test",
247 | "script": {
248 | "exec": [
249 | "pm.test(\"Status code is 200\", function () {",
250 | " pm.response.to.have.status(200);",
251 | "});",
252 | "",
253 | "pm.test(\"Home page is obtained\", function () {",
254 | " pm.expect(pm.response.text()).to.include(\"home page\");",
255 | "});"
256 | ],
257 | "type": "text/javascript"
258 | }
259 | }
260 | ],
261 | "request": {
262 | "method": "GET",
263 | "header": [],
264 | "url": {
265 | "raw": "http://localhost:8081/homepage.html",
266 | "protocol": "http",
267 | "host": [
268 | "localhost"
269 | ],
270 | "port": "8081",
271 | "path": [
272 | "homepage.html"
273 | ]
274 | }
275 | },
276 | "response": []
277 | },
278 | {
279 | "name": "Get hi - page not served",
280 | "event": [
281 | {
282 | "listen": "test",
283 | "script": {
284 | "exec": [
285 | "pm.test(\"Status code is 404\", function () {",
286 | " pm.response.to.have.status(404);",
287 | "});",
288 | ""
289 | ],
290 | "type": "text/javascript"
291 | }
292 | }
293 | ],
294 | "request": {
295 | "method": "GET",
296 | "header": [],
297 | "url": {
298 | "raw": "http://localhost:8081/hi",
299 | "protocol": "http",
300 | "host": [
301 | "localhost"
302 | ],
303 | "port": "8081",
304 | "path": [
305 | "hi"
306 | ]
307 | }
308 | },
309 | "response": []
310 | }
311 | ],
312 | "description": "# Description\n\nThis folder contains the requests that are served when the Golang Web Server is launched with the FileServer handler option. See more about this in the Readme file of the project: [https://github.com/jlhidalgo/golang-web-server/blob/main/README.md](https://github.com/jlhidalgo/golang-web-server/blob/main/README.md)"
313 | }
314 | ],
315 | "event": [
316 | {
317 | "listen": "prerequest",
318 | "script": {
319 | "type": "text/javascript",
320 | "exec": [
321 | ""
322 | ]
323 | }
324 | },
325 | {
326 | "listen": "test",
327 | "script": {
328 | "type": "text/javascript",
329 | "exec": [
330 | ""
331 | ]
332 | }
333 | }
334 | ],
335 | "variable": [
336 | {
337 | "key": "counter",
338 | "value": "0",
339 | "type": "string"
340 | }
341 | ]
342 | }
--------------------------------------------------------------------------------