golang的http上传文件的实现细节严格遵循了multipart/form-data的RFC1867规范,细节网上已经分析了很多了
本篇只是讨论golang上如何使用官方库实现的框架
客户端上传很简单,利用"mime/multipart"官方库即可完成上传
可以看到这里上传文件时全部加载在内存的bodyBuf字符数组中,所以这里只是上传小文件,大文件这么上传会写爆内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 func postFile (filename, targetUrl, token string )  error {                             var (                                                                                bodyBuf         *bytes.Buffer                                                    bodyWriter      *multipart.Writer                                                file            *os.File                                                         err             error                                                            contentType     string                                                            client          http.Client                                                      req             *http.Request                                                    resp            *http.Response                                                   respBody        []byte                                                       )                                                                                bodyBuf = &bytes.Buffer{}                                                        bodyWriter = multipart.NewWriter(bodyBuf)                                                                                                                         fileWriter, err := bodyWriter.CreateFormFile("PolkaFile" ,filename)               if  err != nil {                                                                      return  err                                                                   }                                                                                                                                                                 file, err = os.Open(filename)                                                    defer file.Close()                                                               if  err != nil {                                                                      return  err                                                                   }                                                                                                                                                                 _, err = io.Copy(fileWriter, file)                                               if  err != nil {                                                                      return  err                                                                   }                                                                                          err = bodyWriter.Close();                                                        if  err != nil {                                                                      return  err                                                                   }                                                                                contentType = bodyWriter.FormDataContentType()                                                                                                                    req, err = http.NewRequest("POST" , targetUrl, bodyBuf)                           if  err != nil {                                                                      return  err                                                                   }                                                                                req.Header.Set("Content-Type" ,contentType)                                       req.Header.Set("Authorization" ,token)                                            resp, err = client.Do(req)                                                       if  err != nil {                                                                      return  err                                                                   }                                                                                defer resp.Body.Close()                                                                                                                                           respBody, err = ioutil.ReadAll(resp.Body)                                        if  err != nil {                                                                      return  err                                                                   }                                                                                log .Infof("resp status: %s,resp body: %s" ,resp.Status, string (respBody))         return  nil                                                                   } ```   ------------------------------------------- 服务端的上传也很简单 在golang http框架用mux的HandleFunc方法绑定的回调函数中写入几行代码即可 ```c func (this *proxy) upload(wr http.ResponseWriter, r *http.Request) {                    file, handle, err := r.FormFile("PolkaFile" )                                        defer file.Close()                                                                  if  err != nil {                                                                         log .Errorf("%v" ,err)                                                            }                                                                                   return                                                                           } ```   这里的handle的Filename字段对应了客户端上传时"PolkaFile" 这个字符串绑定的文件名 这里的file是一个接口 ```c type File interface {                                                                io.Reader                                                                        io.ReaderAt                                                                      io.Seeker                                                                        io.Closer                                                                    } ```   为什么要这么做呢,因为文件有可能存储在内存里或者是磁盘上 ```c 按照http.Request.FormFile()->http.Request.ParseMultipartForm()->multipart.Reader.ReadForm()这个调用链来看 在multipart.Reader.ReadForm()中有这么一段 fh := &FileHeader{                                                           Filename: filename,                                                      Header:   p.Header,                                                  }                                                                        n, err := io.CopyN(&b, p, maxMemory+1 )                                   if  err != nil && err != io.EOF {                                            return  nil, err                                                      }                                                                        if  n > maxMemory {                                                               file, err := ioutil.TempFile("" , "multipart-" )                           if  err != nil {                                                              return  nil, err                                                      }                                                                        defer file.Close()                                                       _, err = io.Copy(file, io.MultiReader(&b, p))                            if  err != nil {                                                              os.Remove(file.Name())                                                   return  nil, err                                                      }                                                                        fh.tmpfile = file.Name()                                             } else  {                                                                     fh.content = b.Bytes()                                                   maxMemory -= n                                                       } 
数据从p中读出,如果大于最大内存(默认值32MB),那么会用MultiReader重新读取,并且写入到TempFile中
这部分文件会在multipart.Reader.RemoveAll()中被销毁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func (f *Form) RemoveAll() error {                                                      var err error                                                                       for  _, fhs := range f.File {                                                            for  _, fh := range fhs {                                                                if  fh.tmpfile != ""  {                                                                   e := os.Remove(fh.tmpfile)                                                          if  e != nil && err == nil {                                                             err = e                                                                         }                                                                               }                                                                               }                                                                               }                                                                                   return  err                                                                      } 
RemoveAll()函数会在http.response.finishRequest()这个函数中被调用,也就是结束请求后被销毁
我们可以通过handle来获取这个结构体,但是无法操作content和tmpfile这两个未导出字段
1 2 3 4 5 6 7 type FileHeader struct  {     Filename string                                                                      Header   textproto.MIMEHeader                                                                                                                                           content []byte                                                                      tmpfile string                                                                   } 
只能通过这个File接口,所以数据必须通过io.Copy等方式进行一次复制,这样效率会大大降低
假如是一个[]byte数组,那么这个数组可以直接被发送数据,执行一次复制会加大内存的开销和GC的负担
加入是一个tmp文件,那么这个文件可以直接使用sendfile调用发送出去,执行一次复制再写到其他文件或者写到内存,也会增加内存的开销
要做到零拷贝,就只能通过unsafe来直接操作未导出字段
我写了些测试代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package common                                                                                                                                                          import  (                                                                                "fmt"                                                                                "net/textproto"                                                                  )                                                                                                                                                                       type FileHeader struct  {                                                                Filename string                                                                      Header   textproto.MIMEHeader                                                                                                                                           content []byte                                                                      tmpfile string                                                                   }                                                                                                                                                                       func (this *FileHeader) Set() {                                                         this.Filename = "123"                                                                this.content = []byte{1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 }                                         this.tmpfile = "456"                                                             }                                                                                                                                                                       func (this *FileHeader) Get() {                                                         fmt.Println(this.Filename)                                                          fmt.Println(this.content)                                                           fmt.Println(this.tmpfile)                                                       } 
这是这个山寨FileHeader,用于进行简单的GetSet
然后是UnSafeMultipart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package main                                                                                                                                                            import  (                                                                                "unsafe"                                                                             "fmt"                                                                                "coding.net/tedcy/Polka/common"                                                  )                                                                                                                                                                       type UnsafeMultipart struct  {                                                           common.FileHeader                                                               }                                                                                                                                                                       func (this *UnsafeMultipart) GetContent() []byte{                                       contentPtr := (*[]byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&this.Header)) + uintptr(unsafe.Sizeof(this.Header))))     return  *contentPtr                                                              }                                                                                                                                                                       func (this *UnsafeMultipart) GetTmpfileName() string {                                   tmpfileNamePtr := (*string )(                                                            unsafe.Pointer(uintptr(unsafe.Pointer(&this.Header)) +                                  uintptr(unsafe.Sizeof(this.Header)) +                                               uintptr(unsafe.Sizeof([]byte(nil)))))                                       return  *tmpfileNamePtr                                                          }                                                                                                                                                                       func main() {                                                                           u := &UnsafeMultipart{}                                                             u.Set()                                                                             u.Get()                                                                             fmt.Println(u.GetContent())                                                         fmt.Println(u.GetTmpfileName())                                                 } 
输出
1 2 3 4 5 123 [1 2 3 4 5 6 7 8 9 10] 456 [1 2 3 4 5 6 7 8 9 10] 456 
可以看到,已经取得了我要的未导出字段
unsafe有几个小细节
1 unsafe.Pointer()传入的只能是指针
2 unsafe.Pointer要参与运算只能转换为uintptr
3 uintptr得到的地址只能转换为unsafe.Pointer
4 最终转换的unsafe.Pointer也只能转换成某个类型的指针,例如[]byte, string
5 加一段测试代码
1 2 3 4 fmt.Println(uintptr(unsafe.Pointer(u)))                                         fmt.Println(uintptr(unsafe.Pointer(&u.FileHeader)))                             fmt.Println(uintptr(unsafe.Pointer(&u.Filename)))                               fmt.Println(uintptr(unsafe.Pointer(&u.Header))) 
输出是
1 2 3 4 859530421504 859530421504 859530421504 859530421520 
可以看到u的地址和u封装的FileHeader是同一个地址,和FileHeader内的Filename也是同一个地址
然后Filename虽然是string类型,但是实际占用是固定长度的大小
完毕
补充:
后来发现以上全是考虑太多了,go的http库有两种解析multipart上传的方式,另外一种可以满足需求
利用http request的MultipartReader()即可,这个函数是和ParseMultipartForm互斥的,无论哪个被调用,另外一个都不能再调用了
ParseMultipartForm是将表单的form全部解析好,而MultipartReader则是将表单解析成单独的Part内容,以供解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 reader, err := r.MultipartReader() if err != nil {     ... } for {     part, err := reader.NextPart()     if err == io.EOF {         break     }     if part.FileName() != "" {         //filecontent         continue     }     data, err := ioutil.ReadAll(part)     if err != nil {         ...     }     // form data, key = part.FormName(),value = string(data) } 
可以看到,解析完全依赖自己的处理逻辑,也就是避免了二次拷贝