如何创建一个能够根据配置文件中描述的 URL 提供服务的服务器?

huangapple go评论94阅读模式
英文:

Go: How to create a server which can serve urls described in config file

问题

有人可以帮我吗?因为我对golang还不熟悉。我有一个yaml文件,内容如下:

port: 5000
handlers:
  - name: test1
    uri: /api/test1
    response:
      status: 200
      body: test1
  - name: test2
    uri: /api/test2
    response:
      status: 500
      body: test2

基于这个文件,我想创建一个服务器。目前我尝试这样做,但似乎不像预期那样工作。我做错了什么?有更好的方法来实现我所需的吗?

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"

	"gopkg.in/yaml.v2"
)

func main() {
	config := parseYaml("conf.yaml")
	configHandlers := config.Handlers
	mux := http.NewServeMux()
	for _, handler := range *configHandlers {
		mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(*handler.Response.Status)
			fmt.Fprintf(w, *handler.Response.Body)
		})
	}
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", *config.Port), mux))
}

type YamlConfig struct {
	Port     *string          `yaml:"port"`
	Handlers *[]HandlerConfig `yaml:"handlers"`
}

type HandlerConfig struct {
	Uri      *string   `yaml:"uri"`
	Name     *string   `yaml:"name"`
	Response *Response `yaml:"response"`
}

type Response struct {
	Status *int    `yaml:"status"`
	Body   *string `yaml:"body"`
}

func (c *YamlConfig) parseYaml(data []byte) error {
	return yaml.Unmarshal(data, c)
}

func parseYaml(path string) YamlConfig {
	data, err := ioutil.ReadFile(path)
	if err != nil {
		log.Fatal(err)
	}
	var config YamlConfig
	if err := config.parseYaml(data); err != nil {
		log.Fatal(err)
	}
	return config
}

更新:
如果我运行这个服务器,无论我访问哪个端点,它都会始终返回500test2作为响应体。

英文:

could anyone help me here please as I'm new to golang? I have a yaml file which looks like this:

port: 5000
handlers:
  - name: test1
    uri: /api/test1
    response:
      status: 200
      body: test1
  - name: test2
    uri: /api/test2
    response:
      status: 500
      body: test2

based on this file I want to create a server. Currently I'm trying to do it this way, but looks like it doesn't work as expected.
What am I doing wrong and what is the better way to achieve what I need?

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"

	"gopkg.in/yaml.v2"
)

func main() {
	config := parseYaml("conf.yaml")
	configHandlers := config.Handlers
	mux := http.NewServeMux()
	for _, handler := range *configHandlers {
		mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(*handler.Response.Status)
			fmt.Fprintf(w, *handler.Response.Body)
		})
	}
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", *config.Port), mux))
}

type YamlConfig struct {
	Port     *string          `yaml:"port"`
	Handlers *[]HandlerConfig `yaml:"handlers"`
}

type HandlerConfig struct {
	Uri      *string   `yaml:"uri"`
	Name     *string   `yaml:"name"`
	Response *Response `yaml:"response"`
}

type Response struct {
	Status *int    `yaml:"status"`
	Body   *string `yaml:"body"`
}

func (c *YamlConfig) parseYaml(data []byte) error {
	return yaml.Unmarshal(data, c)
}

func parseYaml(path string) YamlConfig {
	data, err := ioutil.ReadFile(path)
	if err != nil {
		log.Fatal(err)
	}
	var config YamlConfig
	if err := config.parseYaml(data); err != nil {
		log.Fatal(err)
	}
	return config
}

Update:
If I run this server then regardless of which endpoint I hit, it will always return me 500 and test2 in body

答案1

得分: 1

你所看到的似乎是人们常见的一个陷阱:

configHandlers := config.Handlers
mux := http.NewServeMux()
for _, handler := range *configHandlers {
    mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(*handler.Response.Status)
        fmt.Fprintf(w, *handler.Response.Body)
    })
}

在每次迭代中,for 循环会重新分配 handler 变量。在循环体中,你创建了一个新的函数并将其传递给 mux.HandlerFun。这些函数体在某种程度上继承了外部作用域,并访问了 handler 变量。该变量在函数外部被重新赋值,因此每个处理程序函数可以访问的值也随之改变。为了解决这个问题,你可以掩盖循环使用的 handler 变量,并创建一个对每个处理程序唯一的作用域。在像 JavaScript 这样的语言中(在我写一些 JavaScript 时是一个常见问题),经典的解决方法是将代码包装在 IIFE(立即调用的函数表达式)中:

for _, handler := range *configHandlers {
    func (handler *HandlerConfig) { // handler 现在是传递给此函数的参数
        mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(*handler.Response.Status)
            fmt.Fprintf(w, *handler.Response.Body)
        })
    }(handler) // 使用当前的 handler 值调用函数
}

这有点混乱,因为 Go 语言具有适当的块作用域,你可以直接这样做:

for _, handler := range *configHandlers {
    h := handler // 在内部作用域中创建一个变量
    mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
        // 现在 h 将引用每次迭代中的唯一副本
        w.WriteHeader(*h.Response.Status)
        fmt.Fprintf(w, *h.Response.Body)
    })
}

这应该可以解决问题。不过,我注意到你在问题中添加的类型中使用了指针的一些奇怪之处... Port 字段的类型为 *string?为什么不直接使用 stringResponse 类型中的 BodyStatus 字段也是如此。将它们更改为普通的 string 字段,你就不需要在处理程序函数中解引用它们了。代码看起来会更清晰。

更大的问题是这个字段:

Handlers *[]HandlerConfig `yaml:"handlers"`

我不确定你是否真正知道这个字段的类型,但它几乎没有意义。Handlers 现在是指向 HandlerConfig 值切片的指针。我猜你想要的是:

// Handlers 是 HandlerConfig 值的切片:
Handlers []HandlerConfig `yaml:"handlers"`
// 或者 Handlers 是指向 HandlerConfig 值的指针切片
Handlers []*HandlerConfig `yaml:"handlers"`

一般来说,在配置类型中使用指向切片的指针,尤其是在配置类型中,是不好的代码风格。

英文:

What you're seeing is seemingly a common pitfall for people:

configHandlers := config.Handlers
mux := http.NewServeMux()
for _, handler := range *configHandlers {
mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(*handler.Response.Status)
fmt.Fprintf(w, *handler.Response.Body)
})
}

The for loop, on each iteration, reassigns the handler variable. In the loop body, you create a new function and pass it to mux.HandlerFun. These function bodies kind of inherit the outer scope, and access this handler variable. The variable is reassigned outside of the functions, and thus the values each handler function has access to changes with it. What you can do to address the issue is mask the handler variable the loop uses, and create a scope that is unique to each handler. The classic way in languages like JavaScript (where this is - or used to be back when I wrote some JS - a common issue) is to wrap the code in an IIFE (Immediately Invoked Function Expression):

for _, handler := range *configHandlers {
func (handler *HandlerConfig) { // handler is now the argument passed to this function
mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(*handler.Response.Status)
fmt.Fprintf(w, *handler.Response.Body)
})
}(handler) // call the function with the _current_ value of handler
}

This is a tad messy, and because golang is properly block-scoped, you can just do this:

for _, handler := range *configHandlers {
h := handler // create a variable in the inner scope
mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
// now h will reference a copy unique to each iteration
w.WriteHeader(*h.Response.Status)
fmt.Fprintf(w, *h.Response.Body)
})
}

That ought to fix it. I've noticed some weirdness with your use of pointers in the types you've added to your question, though... Fields like Port being of type *string? Why wouldn't you just use string? No Same for the Body and Status fields in the Response type. By changing them to plain string fields you don't have to dereference them in your handler functions. It will look a lot cleaner.

A bigger worry is this field:

Handlers *[]HandlerConfig `yaml:"handlers"`

I'm not sure if you really know what the type of this field is, but it makes next to no sense. Handlers is now a pointer to a slice of HandlerConfig values. I'm assuming you wanted this field to be:

// Handlers is a slice of HandlerConfig values:
Handlers []HandlerConfig `yaml:"handlers"`
// or Handlers is a slice of pointers to HandlerConfig values
Handlers []*HandlerConfig `yaml:"handlers"`

Generally speaking, a pointer to a slice, especially in a config type is bad code.

答案2

得分: 0

如果你定义了一个结构体来表示你的YAML文件中的配置,你可以使用yaml包将yaml解析为该类型的实例化结构体。然后,你可以像处理其他结构体一样引用结构体中的字段。

package main

import (
	"fmt"

	"gopkg.in/yaml.v2"
)

type YamlExample struct {
	FieldOne    string `yaml:"fieldOne"`
	NestedField struct {
		Name string `yaml:"name"`
	} `yaml:"nestedField"`
}

const YamlEx string = `
fieldOne: one
nestedField:
    name: nestedFieldName
`

func main() {
	var yamlE YamlExample

	err := yaml.Unmarshal([]byte(YamlEx), &yamlE)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", yamlE)
}

示例链接

在你的情况下,你可能希望在一个结构体中处理路由,然后引用结构体中的字段来处理路由名称、请求体等。如果你的YAML存储在文件中,你需要使用类似io包的方法将文件读取到可以被YAML包解析的字节数组中。参考这里

英文:

If you define a struct that will represent the configuration in your YAML file, you can unmarshall the yaml into an instantiated struct of that type using the yaml package. From there, you can reference the fields in the struct as any other struct.

package main

import (
	"fmt"

	"gopkg.in/yaml.v2"
)

type YamlExample struct {
	FieldOne    string `yaml:"fieldOne"`
	NestedField struct {
		Name string `yaml:"name"`
	} `yaml:"nestedField"`
}

const YamlEx string = `
fieldOne: one
nestedField:
    name: nestedFieldName
`

func main() {
	var yamlE YamlExample

	err := yaml.Unmarshal([]byte(YamlEx), &yamlE)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", yamlE)
}

Link to example.

In your case, you'd probably want to handle the routes in a struct and then reference the fields in the struct for things like route name, how to handle the body of the request, etc. If your YAML is stored in a file, you'll have to use something like the io package to read the file into a byte array that the YAML package can parse. See here for a reference.

huangapple
  • 本文由 发表于 2022年4月29日 00:18:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/72047114.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定