如何在 Go (Golang) 中检索表单数据作为地图(如 PHP 和 Ruby)

Posted

技术标签:

【中文标题】如何在 Go (Golang) 中检索表单数据作为地图(如 PHP 和 Ruby)【英文标题】:How to retrieve form-data as map (like PHP and Ruby) in Go (Golang) 【发布时间】:2016-04-22 17:50:44 【问题描述】:

我是一名 php 开发人员。但目前正在迁移到 Golang...我正在尝试从 Form(Post 方法)中检索数据:

<!-- A really SIMPLE form -->
<form class="" action="/Contact" method="post">
  <input type="text" name="Contact[Name]" value="Something">   
  <input type="text" name="Contact[Email]" value="Else">
  <textarea name="Contact[Message]">For this message</textarea>
  <button type="submit">Submit</button>
</form>

在 PHP 中,我会简单地使用它来获取数据:

<?php 
   print_r($_POST["Contact"])
?>
// Output would be something like this:
Array
(
    [Name] => Something
    [Email] => Else
    [Message] => For this message
)

但是在进行中...要么我一个接一个地得到,要么得到整个东西,但不是 Contact[] 数组,例如 PHP

我想到了 2 个解决方案:

1)一一获取:

// r := *http.Request
err := r.ParseForm()

if err != nil 
    w.Write([]byte(err.Error()))
    return


contact := make(map[string]string)

contact["Name"] = r.PostFormValue("Contact[Name]")
contact["Email"] = r.PostFormValue("Contact[Email]")
contact["Message"] = r.PostFormValue("Contact[Message]")

fmt.Println(contact)

// Output
map[Name:Something Email:Else Message:For this Message]

请注意,地图键是完整的:“Contact[Name]”...

2) 范围整个地图r.Form 并“解析|获取”带有前缀的那些值 "Contact[" 然后用空字符串替换 "Contact[" 和 "]" 所以我只能通过 PHP 示例获取表单数组键

我自己完成了这项工作,但是...覆盖整个表格可能不是一个好主意 (?)

// ContactPost process the form sent by the user
func ContactPost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) 
    err := r.ParseForm()

    if err != nil 
        w.Write([]byte(err.Error()))
        return
    

    contact := make(map[string]string)

   for i := range r.Form 
       if strings.HasPrefix(i, "Contact[") 
           rp := strings.NewReplacer("Contact[", "", "]", "")
           contact[rp.Replace(i)] = r.Form.Get(i)
       
   

    w.Write([]byte(fmt.Sprint(contact)))

//Output
map[Name:Something Email:Else Message:For this Message]

两种解决方案都给我相同的输出...但在第二个示例中,我不一定需要知道“Contact[]”的键

我知道...我可能会忘记那个“表单数组”并在我的输入上使用name="Email" 并一一检索但是...我已经经历了一些场景,我使用一个包含更多的表单超过 2 个数据数组,并且对每个数组做不同的事情,比如 ORMs

问题 1:有没有更简单的方法让我的表单数组像 PHP 一样在 Golang 中作为实际地图?

问题 2:我应该逐一检索数据(同样繁琐,我可能会在某些时候更改表单数据并重新编译...)还是像我一样迭代整个事情在第二个例子中完成。

抱歉我的英语不好...提前谢谢!

【问题讨论】:

***.com/questions/23713817/… 不,这无济于事...请注意我的表单数据数组具有独立的键,即使没有键也无法正确识别哪个对应于哪个... 您可以很容易地编写自己的表单解析器,它遍历表单中的所有键,并将所有“Name[Key]”字段分组到一个子字典中。 因为我找不到类似的东西,这几乎就是我所做的,我自己的功能......好吧,我会为此创建一个结构来保存解析的表单并根据需要使用它 【参考方案1】:

我一直在使用点前缀约定:contact.name、contact.email

我决定在这里留下一个脚本,这样人们就不必花太多时间编写自己的自定义解析器。

这是一个简单的脚本,它遍历表单数据并将值放入遵循 PHP 和 Ruby 格式的结构中。

package formparser

import (
    "strings"
    "mime/multipart"
)

type NestedFormData struct 
    Value *ValueNode
    File *FileNode


type ValueNode struct 
    Value []string
    Children map[string]*ValueNode


type FileNode struct 
    Value []*multipart.FileHeader
    Children map[string]*FileNode


func (fd *NestedFormData) ParseValues(m map[string][]string)
    n := &ValueNode
        Children: make(map[string]*ValueNode),
    
    for key, val := range m 
        keys := strings.Split(key,".")
        fd.nestValues(n, &keys, val)
    
    fd.Value = n


func (fd *NestedFormData) ParseFiles(m map[string][]*multipart.FileHeader)
    n := &FileNode
        Children: make(map[string]*FileNode),
    
    for key, val := range m 
        keys := strings.Split(key,".")
        fd.nestFiles(n, &keys, val)
    
    fd.File = n


func (fd *NestedFormData) nestValues(n *ValueNode, k *[]string, v []string) 
    var key string
    key, *k = (*k)[0], (*k)[1:]
    if len(*k) == 0 
            if _, ok := n.Children[key]; ok 
                    n.Children[key].Value = append(n.Children[key].Value, v...)
             else 
                    cn := &ValueNode
                            Value: v,
                            Children: make(map[string]*ValueNode),
                    
                    n.Children[key] = cn
            
     else 
        if _, ok := n.Children[key]; ok 
            fd.nestValues(n.Children[key], k,v)
         else 
            cn := &ValueNode
                Children: make(map[string]*ValueNode),
            
            n.Children[key] = cn
            fd.nestValues(cn, k,v)
        
    


func (fd *NestedFormData) nestFiles(n *FileNode, k *[]string, v []*multipart.FileHeader)
    var key string
    key, *k = (*k)[0], (*k)[1:]
    if len(*k) == 0 
        if _, ok := n.Children[key]; ok 
            n.Children[key].Value = append(n.Children[key].Value, v...)
         else 
            cn := &FileNode
                Value: v,
                Children: make(map[string]*FileNode),
            
            n.Children[key] = cn
        
     else 
        if _, ok := n.Children[key]; ok 
            fd.nestFiles(n.Children[key], k,v)
         else 
            cn := &FileNode
                Children: make(map[string]*FileNode),
            
            n.Children[key] = cn
            fd.nestFiles(cn, k,v)
        
    

然后你可以像这样使用这个包:

package main

import (
 "MODULE_PATH/formparser"
 "strconv"
 "fmt"
)

func main()
    formdata := map[string][]string
        "contact.name": []string"John Doe",
        "avatars.0.type": []string"water",
        "avatars.0.name": []string"Korra",
        "avatars.1.type": []string"air",
        "avatars.1.name": []string"Aang",
    
    f := &formparser.NestedFormData
    f.ParseValues(formdata)
    //then access form values like so
    fmt.Println(f.Value.Children["contact"].Children["name"].Value)
    fmt.Println(f.Value.Children["avatars"].Children[strconv.Itoa(0)].Children["name"].Value)
    fmt.Println(f.Value.Children["avatars"].Children[strconv.Itoa(0)].Children["type"].Value)
    fmt.Println(f.Value.Children["avatars"].Children[strconv.Itoa(1)].Children["name"].Value)
    fmt.Println(f.Value.Children["avatars"].Children[strconv.Itoa(1)].Children["type"].Value)
    //or traverse  the Children in a loop
    for key, child := range f.Value.Children 
        fmt.Println("Key:", key, "Value:", child.Value)
        if child.Children != nil 
            for k, c := range child.Children 
                fmt.Println(key + "'s child key:", k, "Value:", c.Value)
            
        
    
    //if you want to access files do not forget to call f.ParseFiles()

【讨论】:

【参考方案2】:

我也遇到了类似的问题,所以写了这个函数

func ParseFormCollection(r *http.Request, typeName string) []map[string]string 
    var result []map[string]string
    r.ParseForm()
    for key, values := range r.Form 
        re := regexp.MustCompile(typeName + "\\[([0-9]+)\\]\\[([a-zA-Z]+)\\]")
        matches := re.FindStringSubmatch(key)

        if len(matches) >= 3 

            index, _ := strconv.Atoi(matches[1])

            for ; index >= len(result); 
                result = append(result, map[string]string)
            

            result[index][matches[2]] = values[0]
        
    
    return result

它将表单键值对的集合转换为字符串映射列表。例如,如果我有这样的表单数据:

Contacts[0][Name] = Alice
Contacts[0][City] = Seattle
Contacts[1][Name] = Bob
Contacts[1][City] = Boston

我可以通过“联系人”的 typeName 调用我的函数:

for _, contact := range ParseFormCollection(r, "Contacts") 
    // ...

它会返回一个包含两个地图对象的列表,每个地图包含“名称”和“城市”的键。在 JSON 表示法中,它看起来像这样:

[
  
    "Name": "Alice",
    "City": "Seattle"
  ,
  
    "Name": "Bob",
    "City": "Boston"
  
]

顺便说一句,这正是我在 ajax 请求中将数据发布到服务器的方式:

$.ajax(
  method: "PUT",
  url: "/api/example/",
  dataType: "json",
  data: 
    Contacts: [
      
        "Name": "Alice",
        "City": "Seattle"
      ,
      
        "Name": "Bob",
        "City": "Boston"
      
    ]
  
)

如果您的表单数据键结构与我的不太匹配,那么您可以调整我正在使用的正则表达式以满足您的需求。

【讨论】:

很好的答案。我发现直接将结果附加到地图(丢弃索引)更加灵活。这样,如果索引不连续就没有问题。【参考方案3】:

我也有同样的问题。数组表单参数的提交在我来自的 Ruby/Rails 世界中也是惯用的。但是,经过一些研究,看起来这并不是真正的“Go-way”。

我一直在使用点前缀约定:contact.namecontact.email 等。

func parseFormHandler(writer http.ResponseWriter, request *http.Request) 
    request.ParseForm()

    userParams := make(map[string]string)

    for key, _ := range request.Form 
        if strings.HasPrefix(key, "contact.") 
            userParams[string(key[8:])] = request.Form.Get(key)
        
    

    fmt.Fprintf(writer, "%#v\n", userParams)


func main() 
    server := http.ServerAddr: ":8088"
    http.HandleFunc("/", parseFormHandler)
    server.ListenAndServe()

运行这个服务器然后卷曲它:

$ curl -id "contact.name=Jeffrey%20Lebowski&contact.email=thedude@example.com&contact.message=I%20hate%20the%20Eagles,%20man." http://localhost:8088

结果:

HTTP/1.1 200 OK
Date: Thu, 12 May 2016 16:41:44 GMT
Content-Length: 113
Content-Type: text/plain; charset=utf-8

map[string]string"name":"Jeffrey Lebowski", "email":"thedude@example.com", "message":"I hate the Eagles, man."

使用大猩猩工具包

您还可以使用Gorilla Toolkit's Schema Package 将表单参数解析为结构,如下所示:

type Submission struct 
    Contact Contact


type Contact struct 
    Name    string
    Email   string
    Message string


func parseFormHandler(writer http.ResponseWriter, request *http.Request) 
    request.ParseForm()

    decoder := schema.NewDecoder()
    submission := new(Submission)
    err := decoder.Decode(submission, request.Form)
    if err != nil 
        log.Fatal(err)
    

    fmt.Fprintf(writer, "%#v\n", submission)

运行这个服务器然后卷曲它:

$ curl -id "Contact.Name=Jeffrey%20Lebowski&Contact.Email=thedude@example.com&Contact.Message=I%20hate%20the%20Eagles,%20man." http://localhost:8088

结果:

HTTP/1.1 200 OK
Date: Thu, 12 May 2016 17:03:38 GMT
Content-Length: 128
Content-Type: text/plain; charset=utf-8

&main.SubmissionContact:main.ContactName:"Jeffrey Lebowski", Email:"thedude@example.com", Message:"I hate the Eagles, man."

【讨论】:

【参考方案4】:

有没有更简单的方法让我的表单数组像 PHP 一样在 Golang 中作为实际地图?

您可以使用http.Request 类型的PostForm 成员。它是url.Values 类型——实际上是(ta-da)map[string][]string,你可以这样对待。不过,您仍然需要先致电req.ParseForm()

if err := req.ParseForm(); err != nil 
    // handle error


for key, values := range req.PostForm 
    // [...]

请注意,PostForm字符串列表的映射。这是因为理论上,每个字段都可以在 POST 正文中出现多次。 PostFormValue() 方法通过隐式返回多个值的 first 来处理此问题(这意味着,当您的 POST 正文为 &amp;foo=bar&amp;foo=baz 时,req.PostFormValue("foo") 将始终返回 "bar")。

另请注意,PostForm 绝不会像 PHP 中使用的那样包含 嵌套结构。由于 Go 是静态类型的,POST 表单值总是string(名称)到[]string(值/秒)的映射。

就我个人而言,我不会对 Go 应用程序中的 POST 字段名称使用括号语法 (contact[email]);无论如何,这是一个特定于 PHP 的构造,正如您已经注意到的那样,Go 并不能很好地支持它。

我应该一一检索数据(同样繁琐,我可能会在某个时候更改表单数据并重新编译...)还是像我在第二个示例中所做的那样迭代整个事情。

对此可能没有正确的答案。如果您将 POST 字段映射到具有静态字段的结构,则必须在某些时候显式映射它们(或使用 reflect 来实现一些神奇的自动映射)。

【讨论】:

我必须在你的代码之前添加 req.PostFormValue("") if err := req.ParseForm(); ...让它工作!我不知道为什么...谢谢

以上是关于如何在 Go (Golang) 中检索表单数据作为地图(如 PHP 和 Ruby)的主要内容,如果未能解决你的问题,请参考以下文章

Go 使用 GraphQL - 基础教程

如何将从表单提交中检索到的参数作为参数传递给 django 中的函数

golang-101-hacks(12)——切片作为函数参数传递

带有 mgo 的 Go (golang) 中的 MongoDB:如何更新记录、确定更新是不是成功并在单个原子操作中获取数据?

Go_15:GoLang中面向对象的三大特性

如何让golang mysql驱动程序在2秒内超时ping?