Jacky's Blog Jacky's Blog
  • 首页
  • 关于
  • 项目
  • 大事记
  • 留言板
  • 友情链接
  • 分类
    • 干货
    • 随笔
    • 项目
    • 公告
    • 纪念
    • 尝鲜
    • 算法
    • 深度学习
首页 › 干货 › Go embed 静态文件

Go embed 静态文件

Jacky
3月 27, 2022干货阅读 2,365
目录
  1. 前言
  2. 使用方法
    1. 将一个文件嵌入字符串中
    2. 将一个文件嵌入
    3. 将一个或多个文件嵌入文件系统中 (type FS)
  3. 指令
    1. 字符串或[]byte
    2. 文件系统
  4. 应用
  5. 参考资料

前言

Go 编译出的二进制程序可以很方便的进行部署,但是如果在程序中引用了静态文件,则部署的时候还要带上静态文件。从 Go 1.16 开始,编译器提供将静态文件嵌入二进制程序中的功能。

使用方法

将一个文件嵌入字符串中

例子1.1

import _ "embed"

//go:embed hello.txt
var s string
print(s)

将一个文件嵌入 []byte

例子1.2

import _ "embed"

//go:embed hello.txt
var b []byte
print(string(b))

将一个或多个文件嵌入文件系统中 (type FS)

例子1.3

import "embed"

//go:embed hello.txt
var f embed.FS
data, _ := f.ReadFile("hello.txt")
print(string(data))

指令

在变量定义的上方加入 //go:embed <相对路径> 指明要嵌入的文件的路径或路径匹配参数。相对路径只能是与当前 go 文件同级的路径,不能使用 ../ 返回到上一级。

指令必须紧跟在包含单个变量声明的行之前。指令和声明之间只允许空行和//行注释。

这个变量的类型只能是 string, []byte, FS (或 type FS)。

例子2.1

package server

import "embed"

// content holds our static web server content.
//go:embed image/* template/*
//go:embed html/index.html
var content embed.FS

为了使代码更加清晰,//go:embed 指令接受多个空格分隔的路径参数,也可以使用多行 //go:embed 以避免出现太长的行。路径分割符是正斜杠 /,在Windows上也是如此。路径参数不能包含 . 、 .. 或空路径参数,也就是他们不能以斜杠作为路径的开始或结束。

要匹配目录中的所有内容,请使用 * 而非 .。

如果文件名中带有空格,可以使用双引号或后引号将路径括起来。

package server

import "embed"

//go:embed "text file" `or this`
var content string

如果路径参数是是一个目录名,则这个目录树下的所有文件都会被递归地嵌入,除了文件名以 . 和 _ 开头的的文件。

所以例子2.1也可以被下面的样子

// content is our static web server content.
//go:embed image template html/index.html
var content embed.FS

在大多数情况下他们是等价的。区别在于 image/* 嵌入的目录包含 image/.tmp、image/dir/.tmp 但 image 不包含。

//go:embed 指令既可以用于导出的变量(exported variables)也可以用于未导出的变量(unexported variables),这取决于你是否允许其他包访问该内容。他只能是包内变量(package scope),不能是代码块、函数块内的局部变量(local variables)

如果路径参数无效或匹配失败,则将无法通过编译。

字符串或[]byte

字符串或 []byte 类型的变量上方的 //go:embed 行只能有一个路径参数,且该参数只能匹配一个文件,字符串和 []byte 将使用文件中的内容进行初始化。

在使用字符串和 []byte 时的 //go:embed 指令要求导入 “embed” 包。但由于没有在代码中用到 embed 包内的变量或方法,所以这里要求空白导入 (_ “embed”) ,如例子1.1和1.2。

文件系统

对于单一文件的嵌入,字符串或 []byte 类型的变量通常是最好的。FS 类型允许嵌入一个文件树,例如一个静态 Web 服务器内容的目录,如例子1.3所示。

FS 实现了 io/fs 包内的 FS 接口,因此他可以用于任何可调用文件系统(type FS)的包,例如 net/http, text/template, and html/template。以下是一个例子。

http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(content))))

template.ParseFS(content, "*.tmpl")

应用

在 Nginx UI 项目中v1.2版本起,我们将前端编译后的产物嵌入 Go 编译出的二进制文件,这样将会极大地简化 Nginx UI 的部署。

我们这里使用了 gin-contrib/static 作为静态文件的处理器,当没有命中 route 内的任何规则时,会转到静态文件的处理器中进行下一步处理,如果文件系统中也没有找到该文件,则转到 NoRoute 进行处理。

r.Use(static.Serve("/", mustFS("")))

mustFS 的实现如下,实现了手动实现了 embed.FS 到 static.ServeFileSystem 的转换。

package router

import (
	"github.com/0xJacky/Nginx-UI/frontend"
	"github.com/gin-contrib/static"
	"github.com/gin-gonic/gin"
	"io/fs"
	"log"
	"net/http"
)

type serverFileSystemType struct {
   http.FileSystem
}

func (f serverFileSystemType) Exists(prefix string, _path string) bool {
   _, err := f.Open(path.Join(prefix, _path))
   return err == nil
}

func mustFS(dir string) (serverFileSystem static.ServeFileSystem) {

   sub, err := fs.Sub(frontend.DistFS, path.Join("dist", dir))

   if err != nil {
      log.Println(err)
      return
   }

   serverFileSystem = serverFileSystemType{
      http.FS(sub),
   }

   return
}

因为原来的前端是使用 Vue-Router,且采用 history mode,按照之前的部署方式,需要在 Nginx 配置如要使用如下配置,以作为伪静态将所有前端的请求放到 index.html 中处理。

location / {
  try_files $uri $uri/ /index.html;
}

到了Go这边,则需要重写 Gin 的 NoRoute 方法,此操作在于当请求没有命中 Gin Route 内的任何规则时,都将返回 index.html 的内容,由客户端进行进一步的处理,如果该请求也没有命中 Vue Route 内的任何规则,将会跳转到前端的 404 页面。

r.NoRoute(func(c *gin.Context) {
   accept := c.Request.Header.Get("Accept")
   if strings.Contains(accept, "text/html") {
      file, _ := mustFS("").Open("index.html")
      defer file.Close()
      stat, _ := file.Stat()
      c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
         bufio.NewReader(file), nil)
   }
})

最后,为了实现在浏览器缓存 js,我设计了如下中间件。当浏览器发送了 If-Modified-Since 头并且它的值与 settings.LastModified 相同时,则返回 304 Not Modified 的空响应。经测试,浏览器可以很好的缓存到静态文件。

func cacheJs() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.Contains(c.Request.URL.String(), "js") {
c.Header("Cache-Control", "max-age: 1296000")
if c.Request.Header.Get("If-Modified-Since") == settings.LastModified {
c.AbortWithStatus(http.StatusNotModified)
}
c.Header("Last-Modified", settings.LastModified)
}
}
}

参考资料

[1] embed, https://pkg.go.dev/embed

[2] Go embed 简明教程, https://colobu.com/2021/01/17/go-embed-tutorial/

文章最后修订于 2022年8月10日

赞(2)
PyTorch 入门与回归网络搭建的研究
上一篇
Gin validator 翻译器的初始化
下一篇
再想想
暂无评论
近期评论
  • Jacky发表在《Nginx UI》
  • daiwenzh5发表在《Nginx UI》
  • Jacky发表在《Nginx UI》
  • daiwenzh5发表在《Nginx UI》
  • Jacky发表在《Nginx UI》
2
  • 2
  • 0
Copyright © 2016-2023 Jacky's Blog. Designed by nicetheme.
粤ICP备16016168号-1
  • 首页
  • 关于
  • 项目
  • 大事记
  • 留言板
  • 友情链接
  • 分类
    • 干货
    • 随笔
    • 项目
    • 公告
    • 纪念
    • 尝鲜
    • 算法
    • 深度学习
# Mac # # Apple # # OS X # # iOS # # macOS #
Jacky
PHP C C++ Python | 舞象之年 | 物联网工程
174
文章
169
评论
267
喜欢