this may seem off-topic but it's relevant: I'm trying to write benchmarks in go to compare performance to nea (our roc webserver). I don't know any go, so I have no idea what I'm doing, and could use some help here
the server below should just serve the string "hello world!". But curl does not actually find that
> curl -i 127.0.0.1:8080
HTTP/1.1 200 OK
Date: Wed, 17 Jan 2024 10:20:01 GMT
Content-Length: 0
Ultimately I want to convert these benchmarks to use go. It is important that there is just one thread doing the listening on a socket, and then work gets distributed to N worker threads that process the request and send a response.
https://github.com/tweedegolf/nea/tree/main/benchmarks/roc-nea
Help would be most welcome!
package main
import (
"fmt"
"net/http"
)
import _ "embed"
var workerCount = 2
var workerPool chan struct{}
func main() {
// Create a buffered channel to limit the number of concurrent worker threads
workerPool = make(chan struct{}, workerCount)
// Handle requests using a single IO thread
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Acquire a worker thread from the pool
workerPool <- struct{}{}
// Handle the request in a goroutine
go func() {
// Ensure the worker is released back to the pool when done
defer func() {
<-workerPool
}()
// Handle the actual request (in this case, just returning "Hello, World!")
handleRequest(w, r)
}()
})
// Start the web server on port 8080
fmt.Println("Server listening on :8080")
http.ListenAndServe(":8080", nil)
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
// Simulate some work being done by the worker
fmt.Println("Processing request...")
// Set the Content-Type header to plain text
w.Header().Set("Content-Type", "text/plain")
// Get the response string
response := "Hello, World!"
// Set the Content-Length header
w.Header().Set("Content-Length", fmt.Sprint(len(response)))
// Write the response body
fmt.Fprint(w, response)
}
Not a solution, will need to play with the code later cause that looks like it should work. Anyway, general questions:
Do you want to limit go workers in that way? Goroutines are meant to be exceptionally cheap and should essentially be treated like async contexts. So they generally shouldn't be limited like regular threads.
Go will automatically limit the number of actual threads
There is a bug in the code. The go call in the callback function does not sync. So the callback returns before the inner go function runs. This means that the function handleRequest gets called after the request handler is finished. In this case, the variables r and w are not valid anymore. You cannot write "hello world" to the client, after the connection is closed.
Go starts a separate gorotine for any request anyway. So there is no need to start another one. To fix your code, you can just remove the line "go func()"
I also would not use a Worker Pool.
Cool, I guessed something along those lines but haven't used go in a long while so didn't remember for sure.
I assume for this work, probably just want to limit the number of system threads go uses with GOMAXPROCS
I need a fair comparison. So in rust/tokio we have one "thread" that listens for requests on the socket, and N others that process requests. I just need to replicate that
you can have many concurrent OS threads all listening to the same socket, and that would be faster but also not a fair comparison
That said it just limits live threads running go code. It doesn't limit the number of threads waiting on system calls
Is that a system thread limit or an async process limit?
Looking at the go source. The server is single threaded dispatch that spawns a goroutine after accepting a request.
If you want to restrict the number of goroutines, I guess you can just use the worker pool without a second go func call.
Though go will end up generating a metric crap ton of waiting go routines and that will probably have really bad characteristics. Cause it will still accept every connection and spawn a go thread for it.
finally coming back to this, thanks for the help earlier!
a quick thing I want to check, does this actually allocate the memory?
// Allocate an array of N bytes using make
byteArray := make([]byte, capacity)
Even if so I'd guess it doesn't initialize it? I want something like let v = vec![0xAA; capacity];
but in go
fingers crossed this is turned into a memset?
// Allocate an array of N bytes using make
byteArray := make([]byte, capacity)
for i := range byteArray {
byteArray[i] = 0xAA
}
As far as I understand, byteArray := make([]byte, capacity)
on its own does initialise the values of the underlying array (in addition to allocating memory) as well, as per this Go playground session.
Further background reading:
Update: I see now what your intention was in your code. Yes, as far as I'm aware, the only way to initialise to non-default values is the way you specified here with the for
loop.
cool! there is special hardware support for initializing to zero so that's why I want a different value. But it turns out to not matter too much anyway
in that zero vs 0xAA
does not make a big performance difference
probably the final question: does this look reasonable
func handleRequest(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
pathString, err := generateSVGPath(string(body))
if err != nil {
http.Error(w, "Error processing request body", http.StatusBadRequest)
return
}
response := fmt.Sprintf(`<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<path d="%s" stroke="black" fill="transparent"/>
</svg>
`, pathString)
_, _ = w.Write([]byte(response))
}
func generateSVGPath(requestBody string) (string, error) {
lines := strings.Split(requestBody, "\n")
var pathString strings.Builder
pathString.WriteString("M 0 0 L")
for _, line := range lines {
parts := strings.SplitN(line, ", ", 2)
if len(parts) != 2 {
continue
}
x, errX := strconv.Atoi(parts[0])
y, errY := strconv.Atoi(parts[1])
if errX != nil || errY != nil {
return "", fmt.Errorf("invalid input format")
}
_, _ = fmt.Fprintf(&pathString, " %d %d", x, y)
}
return pathString.String(), nil
}
in particular from a performance perspective?
Last updated: Jul 06 2025 at 12:14 UTC