Go: propuesta: cmd / go: admite la incorporación de activos estáticos (archivos) en binarios

Creado en 4 dic. 2019  ·  176Comentarios  ·  Fuente: golang/go

Existen muchas herramientas para incrustar archivos de activos estáticos en binarios:

En realidad, https://tech.townsourced.com/post/embedding-static-files-in-go/ enumera más:

Propuesta

Creo que es hora de hacer esto bien una vez y reducir la duplicación, agregando soporte oficial para incrustar recursos de archivos en la herramienta cmd / go.

Problemas con la situación actual:

  • Hay demasiadas herramientas
  • El uso de una solución basada en go: generate aumenta el historial de git con una segunda copia (y un poco más grande) de cada archivo.
  • No usar go: generate significa no ser go install -able o hacer que las personas escriban sus propios Makefiles, etc.

Metas:

  • no registre los archivos generados
  • no genere archivos * .go en absoluto (al menos no en el espacio de trabajo del usuario)
  • hacer que go install / go build hacer la incrustación automáticamente
  • permitir que el usuario elija por archivo / glob qué tipo de acceso necesita (por ejemplo, [] byte, func() io.Reader , io.ReaderAt , etc.)
  • ¿Quizás almacenar activos comprimidos en el binario donde sea apropiado (por ejemplo, si el usuario solo necesita un io.Reader )? ( editar : pero probablemente no; vea los comentarios a continuación)
  • Sin ejecución de código en el momento de la compilación ; esa es una política de Go de larga data. go build o go install no pueden ejecutar código arbitrario, al igual que go:generate no se ejecuta automáticamente en el momento de la instalación.

Los dos enfoques de implementación principales son //go:embed Logo logo.jpg o un paquete conocido ( var Logo = embed.File("logo.jpg") ).

ir: enfoque de incrustación

Para un enfoque go:embed , se podría decir que cualquier archivo go/build seleccionado *.go puede contener algo como:

//go:embed Logo logo.jpg

Que, digamos, se compila para:

func Logo() *io.SectionReader

(agregando una dependencia al paquete io )

O:

//go:embedglob Assets assets/*.css assets/*.js

compilando para, decir:

var Assets interface{
     Files() []string
     Open func(name string) *io.SectionReader
} = runtime.EmbedAsset(123)

Obviamente, esto no está completamente desarrollado. También debería haber algo para los archivos comprimidos que produzcan solo un io.Reader .

enfoque de paquete incrustado

El otro enfoque de alto nivel es no tener una sintaxis mágica //go:embed y, en su lugar, dejar que los usuarios escriban código Go en un nuevo paquete "embed" o "golang.org/x/foo/embed" :

var Static = embed.Dir("static")
var Logo = embed.File("images/logo.jpg")
var Words = embed.CompressedReader("dict/words")

Luego haga que cmd / go reconozca las llamadas a embed.Foo ("foo / *. Js") etc. y glob haga el trabajo en cmd / go, en lugar de en tiempo de ejecución. O tal vez ciertas etiquetas o indicadores de compilación podrían hacer que recurra a hacer cosas en tiempo de ejecución. Perkeep (vinculado arriba) tiene un modo de este tipo, que es bueno para acelerar el desarrollo incremental donde no le importa vincular un binario grande.

Preocupaciones

  • Elija un estilo (// go: embed * vs un paquete mágico).
  • ¿Bloquear ciertos archivos?

    • Probablemente bloquee la incrustación ../../../../../../../../../../etc/shadow

    • Tal vez bloquee el alcance de .git también

Proposal Proposal-Hold

Comentario más útil

@robpike y yo hablamos sobre una propuesta para hacer esto hace años (antes de que hubiera un proceso de propuesta) y nunca volvimos a hacer nada. Me ha estado molestando durante años que nunca terminamos de hacer eso. La idea, según recuerdo, era simplemente tener un nombre de directorio especial como "estático" que contenga los datos estáticos y hacerlos disponibles automáticamente a través de una API, sin necesidad de anotaciones.

No estoy convencido de la complejidad de una perilla "comprimida vs no". Si hacemos eso, la gente querrá que agreguemos control sobre qué compresión, nivel de compresión, etc. Todo lo que debemos agregar es la capacidad de incrustar un archivo de bytes simples. Si los usuarios quieren almacenar datos comprimidos en ese archivo, genial, los detalles dependen de ellos y no se necesita ninguna API por parte de Go.

Todos 176 comentarios

Vale la pena considerar si embedglob debería admitir un árbol de archivos completo, quizás usando la sintaxis ** admitida por algunos shells de Unix.

Algunas personas necesitarían la capacidad de servir los activos incrustados con HTTP utilizando http.FileServer .

Yo personalmente uso mjibson / esc (que hace eso) o en algunos casos mi propia implementación de incrustación de archivos que cambia el nombre de los archivos para crear rutas únicas y agrega un mapa de las rutas originales a las nuevas, por ejemplo, "/js/bootstrap.min.js": "/js/bootstrap.min.827ccb0eea8a706c4c34a16891f84e7b.js" . Entonces puede usar este mapa en las plantillas como esta: href="{{ static_path "/css/bootstrap.min.css" }}" .

Creo que una consecuencia de esto sería que no sería trivial averiguar qué archivos son necesarios para construir un programa.

El enfoque //go:embed introduce otro nivel de complejidad. Tendría que analizar los comentarios mágicos para incluso verificar el código. El enfoque de "paquete integrado" parece más compatible con el análisis estático.

(Solo reflexiono en voz alta aquí).

@opennota ,

necesitaría la capacidad de servir los activos incrustados con HTTP utilizando http.FileServer .

Sí, el primer enlace de arriba es un paquete que escribí ( en 2011, antes de Go 1 ) y todavía lo uso, y admite el uso de http.FileServer: https://godoc.org/perkeep.org/pkg/fileembed#Files.Open

@cespare ,

El enfoque // go: embed también introduce otro nivel de complejidad. Tendría que analizar los comentarios mágicos para incluso verificar el código. El enfoque de "paquete integrado" parece más compatible con el análisis estático.

Sí, buen punto. Ese es un argumento muy fuerte para usar un paquete. También lo hace más legible y documentable, ya que podemos documentarlo todo con godoc normal, en lugar de profundizar en los documentos de cmd / go.

@bradfitz - ¿Quieres cerrar este https://github.com/golang/go/issues/3035 ?

@agnivade , ¡gracias por encontrar eso! Pensé que lo recordaba pero no pude encontrarlo. Dejémoslo abierto por ahora y veamos qué piensan los demás.

Si optamos por el paquete mágico, podríamos usar el truco de tipos no exportados para asegurarnos de que las personas que llaman pasen constantes de tiempo de compilación como argumentos: https://play.golang.org/p/RtHlKjhXcda.

(Esta es la estrategia a la que se hace referencia aquí: https://groups.google.com/forum/#!topic/golang-nuts/RDA9Hag8RZw/discussion)

Una preocupación que tengo es cómo manejaría individualmente o todos los activos siendo demasiado grandes para caber en la memoria y si tal vez habría una etiqueta de compilación o la opción de acceso por archivo para elegir entre priorizar el tiempo de acceso frente a la huella de memoria o alguna implementación intermedia.

La forma en que resolví ese problema (porque, por supuesto, también tengo mi propia implementación :)) es proporcionar una implementación http.FileSystem que sirva a todos los activos integrados. De esa manera, no debe depender de los comentarios mágicos para apaciguar al verificador de tipos, los activos pueden ser servidos fácilmente por http, se puede proporcionar una implementación alternativa para propósitos de desarrollo (http.Dir) sin cambiar el código, y el final La implementación es bastante versátil, ya que http.FileSystem cubre bastante, no solo en la lectura de archivos, sino también en la lista de directorios.

Todavía se pueden usar comentarios mágicos o lo que sea para especificar lo que se debe incrustar, aunque probablemente sea más fácil especificar todos los elementos globales a través de un archivo de texto sin formato.

@AlexRouSg Esta propuesta solo sería para archivos que sean apropiados para incluir directamente en el ejecutable final. No sería apropiado usar esto para archivos que son demasiado grandes para caber en la memoria. No hay razón para complicar esta herramienta para manejar ese caso; en ese caso, simplemente no use esta herramienta.

@ianlancetaylor , creo que la distinción que @AlexRouSg estaba haciendo era entre tener los archivos proporcionados como []byte s globales (memoria no paginable y potencialmente grabable) frente a proporcionar una vista de solo lectura bajo demanda de una sección ELF que puede normalmente viven en el disco (en el ejecutable), como a través de una llamada Open que devuelve una *io.SectionReader . (No quiero hornear http.File o http.FileSystem en cmd / go o tiempo de ejecución ... net / http puede proporcionar un adaptador).

@bradfitz tanto http.File en sí es una interfaz sin dependencias técnicas con el paquete http . Puede ser una buena idea que cualquier método Open proporcione una implementación que se ajuste a esa interfaz, porque los métodos Stat y Readdir son bastante útiles para dichos activos

@urandom , sin embargo, no pudo implementar http.FileSystem sin hacer referencia al nombre "http.File" (https://play.golang.org/p/-r3KjG1Gp-8).

@robpike y yo hablamos sobre una propuesta para hacer esto hace años (antes de que hubiera un proceso de propuesta) y nunca volvimos a hacer nada. Me ha estado molestando durante años que nunca terminamos de hacer eso. La idea, según recuerdo, era simplemente tener un nombre de directorio especial como "estático" que contenga los datos estáticos y hacerlos disponibles automáticamente a través de una API, sin necesidad de anotaciones.

No estoy convencido de la complejidad de una perilla "comprimida vs no". Si hacemos eso, la gente querrá que agreguemos control sobre qué compresión, nivel de compresión, etc. Todo lo que debemos agregar es la capacidad de incrustar un archivo de bytes simples. Si los usuarios quieren almacenar datos comprimidos en ese archivo, genial, los detalles dependen de ellos y no se necesita ninguna API por parte de Go.

Un par de pensamientos:

  • No debería ser posible incrustar ningún archivo fuera del módulo que realiza la incrustación. Necesitamos asegurarnos de que los archivos sean parte de los archivos zip del módulo cuando los creamos, por lo que también significa que no hay enlaces simbólicos, conflictos de casos, etc. No podemos cambiar el algoritmo que produce archivos zip sin romper las sumas.
  • Creo que es más sencillo restringir la incrustación para que esté en el mismo directorio (si se usan comentarios //go:embed ) o en un subdirectorio específico (si se usa static ). Esto hace que sea mucho más fácil comprender la relación entre paquetes y archivos incrustados.

De cualquier manera, esto bloquea incrustando /etc/shadow o .git . Ninguno de los dos se puede incluir en un módulo zip.

En general, me preocupa expandir demasiado el alcance del comando go. Sin embargo, el hecho de que haya tantas soluciones a este problema significa que probablemente debería haber una solución oficial.

Estoy familiarizado con go_embed_data y go-bindata (de los cuales hay varias bifurcaciones), y esto parece cubrir esos casos de uso. ¿Hay algún problema importante que los demás resuelvan que esto no cubra?

Bloquear ciertos archivos no debería ser demasiado difícil, especialmente si usa un directorio static o embed . Los enlaces simbólicos pueden complicar eso un poco, pero puede evitar que incruste algo fuera del módulo actual o, si está en GOPATH, fuera del paquete que contiene el directorio.

No soy particularmente fanático de un comentario que se compila en código, pero también encuentro que el pseudopaquete que afecta la compilación también es un poco extraño. Si no se usa el enfoque de directorio, tal vez tenga un poco más de sentido tener una especie de declaración de nivel superior embed incorporada en el lenguaje. Funcionaría de manera similar a import , pero solo admitiría rutas locales y requeriría un nombre para asignarlo. Por ejemplo,

embed ui "./ui/build"

func main() {
  file, err := ui.Open("version.txt")
  if err != nil {
    panic(err)
  }
  version, err = ioutil.ReadAll(file)
  if err != nil {
    panic(err)
  }
  file.Close()

  log.Printf("UI Version: %s\n", bytes.TrimSpace(version))
  http.ListenAndServe(":8080", http.EmbeddedDir(ui))
}

Editar: Me ganaste, @jayconrod.

Para expandir https://github.com/golang/go/issues/35950#issuecomment -561703346, hay un acertijo sobre la API expuesta. Las formas obvias de exponer los datos son las interfaces []byte , string y Read -ish.

El caso típico es que desea que los datos incrustados sean inmutables. Sin embargo, todas las interfaces que exponen []byte (que incluye io.Reader , io.SectionReader , etc.) deben (1) hacer una copia, (2) permitir la mutabilidad o (3) ser inmutable a pesar de ser un []byte . Exponer los datos como string s resuelve eso, pero a costa de una API que a menudo terminará requiriendo copia de todos modos, ya que una gran cantidad de código que consume archivos incrustados eventualmente requiere porciones de bytes de una forma u otra.

Sugeriría la ruta (3): sea inmutable a pesar de ser un []byte . Puede aplicar esto de forma económica utilizando un símbolo de solo lectura para la matriz de respaldo. Esto también le permite exponer de forma segura los mismos datos que un []byte y un string ; los intentos de mutar los datos fallarán. El compilador no puede aprovechar la inmutabilidad, pero eso no es una gran pérdida. Esto es algo que el soporte de la cadena de herramientas puede traer a la mesa que (hasta donde yo sé) ninguno de los paquetes de codegen existentes lo hace.

(Un paquete de codegen de terceros podría hacer esto generando un archivo de ensamblaje genérico que contiene DATA símbolos que están marcados como de solo lectura, y luego archivos de ensamblaje cortos específicos de arco que exponen esos símbolos en la forma de string sy []byte s. Escribí CL 163747 específicamente con este caso de uso en mente, pero nunca logré integrarlo en ningún paquete de codegen).

No estoy seguro de qué estás hablando en términos de inmutabilidad. io.Reader ya impone la inmutabilidad. Ese es todo el punto. Cuando llamas a Read(buf) , copia los datos en el búfer que _tu_ proporcionaste. Cambiar buf después de eso no tiene ningún efecto en las partes internas del io.Reader .

Estoy de acuerdo con @DeedleFake. No quiero jugar juegos con soportes de matriz mágicos []byte . Está bien copiar del binario en búferes proporcionados por el usuario.

Solo otra arruga aquí: tengo un proyecto diferente que usa el código fuente de DTrace (incrustado). Esto es sensible a las diferencias entre \ n y \ r \ n. (Podemos discutir si esto es una tontería en DTrace o no; eso no viene al caso y es la situación actual).

Es muy útil que las cadenas con comillas invertidas traten a ambos como \ n independientemente de cómo aparezcan en la fuente, y confío en esto con un go-generate para incrustar el DTrace.

Entonces, si hay un archivo de inserción agregado al comando go, sugeriría gentilmente que las opciones para cambiar el manejo de CR / CRLF podrían ser muy útiles, particularmente para las personas que podrían estar desarrollando en diferentes sistemas donde los finales de línea predeterminados pueden ser un gotcha.

Al igual que con la compresión, realmente me gustaría detenerme en "copiar los bytes del archivo en el binario". Normalización CR / CRLF, normalización Unicode, gofmt'ing, todo lo que pertenece a otra parte. Compruebe los archivos que contienen los bytes exactos que desea. (Si su control de versiones no puede dejarlos en paz, tal vez verifique el contenido comprimido con gzip y comprímalos en tiempo de ejecución). Hay _muchos_ botones de control de archivos que podríamos imaginar añadiendo. Detengámonos en 0.

Puede que sea demasiado tarde para introducir un nuevo nombre de directorio reservado, por mucho que me gustaría.
(No era demasiado tarde en 2014, pero probablemente ahora sea demasiado tarde).
Por lo tanto, puede ser necesario algún tipo de comentario de inclusión voluntaria.

Supongamos que definimos un tipo runtime.Files. Entonces podrías imaginarte escribiendo:

//go:embed *.html (or static/* etc)
var files runtime.Files

Y luego, en tiempo de ejecución, solo llama a archivos. Abre para recuperar un interface { io.ReadSeeker; io.ReaderAt } con los datos. Tenga en cuenta que la var no se exporta, por lo que un paquete no puede desplazarse por los archivos incrustados de otro paquete.

Nombres TBD, pero en lo que respecta al mecanismo, parece que debería ser suficiente, y no veo cómo hacerlo más simple. (¡Las simplificaciones son bienvenidas, por supuesto!)

Hagamos lo que hagamos, también debe ser posible apoyar con Bazel y Gazelle. Eso significaría que Gazelle reconociera el comentario y escribiera una regla de Bazel diciendo los globos, y luego tendríamos que exponer una herramienta (go tool embedgen o lo que sea) para generar el archivo adicional para incluirlo en la compilación (el comando go sería haga esto automáticamente y nunca muestre el archivo adicional). Eso parece bastante sencillo.

Si varios munging no funcionan, entonces ese es un argumento en contra del uso de esta nueva función. No es un obstáculo para mí: puedo usar go generate como lo he estado haciendo, pero significa que no puedo beneficiarme de la nueva función.

Con respecto a munging en general, puedo imaginar una solución en la que alguien proporcione una implementación de una interfaz (algo así como un Reader () en un lado y algo para recibir el archivo en el otro, tal vez instanciado con un io.Reader desde el archivo en sí), que go cmd compilaría y ejecutaría para prefiltrar el archivo antes de incrustarlo. Entonces la gente puede proporcionar el filtro que desee. Me imagino que algunas personas proporcionarían filtros cuasi estándar como una implementación de dos2unix, compresión, etc. (tal vez incluso deberían ser encadenables).

Supongo que tendría que suponerse que sea cual sea el procesador integrado, debe ser compilable en ~ cada sistema de compilación, ya que sería construir una herramienta nativa temporal para este propósito.

Puede que sea demasiado tarde para introducir un nuevo nombre de directorio reservado, por mucho que me gustaría. [...] puede ser necesario algún tipo de comentario de inclusión voluntaria.

Si solo se puede acceder a los archivos a través de un paquete especial, digamos runtime/embed , entonces la importación de ese paquete podría ser la señal de suscripción.

El enfoque io.Read parece que podría agregar una sobrecarga significativa (en términos de copia y huella de memoria) para operaciones lineales conceptualmente simples como strings.Contains (como en cmd/go/internal/cfg ) o , críticamente, template.Parse .

Para esos casos de uso, parece ideal permitir que la persona que llama elija si tratará todo el blob como un string (presumiblemente mapeado en memoria) o un io.ReaderAt .

Sin embargo, eso parece compatible con el enfoque general runtime.Files : la cosa devuelta desde runtime.Files.Open podría tener un método ReadString() string que devuelva la representación mapeada en memoria.

puede ser necesario algún tipo de comentario de inclusión voluntaria.

Podríamos hacer eso con la versión go en el archivo go.mod . Antes de 1.15 (o lo que sea), el subdirectorio static contendría un paquete, y en 1.15 o superior contendría activos incrustados.

(Sin embargo, eso realmente no ayuda en el modo GOPATH ).

No estoy convencido de la complejidad de una perilla "comprimida vs no". Si hacemos eso, la gente querrá que agreguemos control sobre qué compresión, nivel de compresión, etc. Todo lo que debemos agregar es la capacidad de incrustar un archivo de bytes simples.

Si bien aprecio el impulso por la simplicidad, también debemos asegurarnos de satisfacer las necesidades de los usuarios.

12 de las 14 herramientas enumeradas en https://tech.townsourced.com/post/embedding-static-files-in-go/#comparison admiten la compresión, lo que sugiere que es un requisito bastante común.

Es cierto que uno podría hacer la compresión como un paso previo a la construcción fuera de go, pero eso aún requeriría 1) una herramienta para hacer la compresión 2) verificar algún tipo de assets.zip blob en vcs 3) probablemente una utilidad biblioteca alrededor de la api de inserción para deshacer la compresión. En ese momento no está claro cuál es el beneficio.

Tres de los objetivos enumerados en la propuesta inicial fueron:

  • no registre los archivos generados
  • hacer ir a instalar / ir a construir hacer la incrustación automáticamente
  • almacenar activos comprimidos en binario cuando sea apropiado

Si leemos el segundo de estos como "no requiere una herramienta separada para incrustar", entonces no admitir archivos comprimidos directa o indirectamente no cumple con los tres objetivos.

¿Es necesario que sea a nivel de paquete? El nivel de módulo parece una mejor granularidad ya que lo más probable es que un módulo = un proyecto.

Dado que este directorio no contendría el código Go †, ¿podría ser algo como _static ?

† o, si lo es, se trataría como bytes arbitrarios cuyo nombre termina en ".go" en lugar de como código Go que se compilará

Si se trata de un directorio especial, la lógica podría ser simplemente absorber cualquier cosa en ese árbol de directorios. El paquete de inserción mágico podría permitirle hacer algo como embed.Open("img/logo.svg") para abrir un archivo en un subdirectorio del árbol de activos.

Las cuerdas parecen lo suficientemente buenas. Pueden copiarse fácilmente en []byte o convertirse en Reader . La generación de código o las bibliotecas podrían usarse para proporcionar API más sofisticadas y manejar las cosas durante init . Eso podría incluir descompresión o crear un http.FileSystem .

Windows no tiene un formato especial para incrustar activos. ¿Debería usarse al crear un ejecutable de Windows? Si es así, ¿tiene eso alguna implicación para los tipos de operaciones que se pueden proporcionar?

No te olvides de gitfs 😂

¿Hay alguna razón por la que no pueda ser parte de go build / link ... por ejemplo, go build -embed example=./path/example.txt y algún paquete que exponga el acceso a él (por ejemplo, embed.File("example") , en lugar de usar go:embed ?

aunque necesitas un código auxiliar para eso en tu código

@egonelbre el problema con go build -embed es que todos los usuarios necesitarían usarlo correctamente. Esto debe ser completamente transparente y automático; Los comandos existentes go install o go get no pueden dejar de hacer lo correcto.

@bradfitz Recomendaría https://github.com/markbates/pkger sobre Packr. Utiliza la API de biblioteca estándar para trabajar con archivos.

func run() error {
    f, err := pkger.Open("/public/index.html")
    if err != nil {
        return err
    }
    defer f.Close()

    info, err := f.Stat()
    if err != nil {
        return err
    }

    fmt.Println("Name: ", info.Name())
    fmt.Println("Size: ", info.Size())
    fmt.Println("Mode: ", info.Mode())
    fmt.Println("ModTime: ", info.ModTime())

    if _, err := io.Copy(os.Stdout, f); err != nil {
        return err
    }
    return nil
}

O tal vez ciertas etiquetas o indicadores de compilación podrían hacer que recurra a hacer cosas en tiempo de ejecución. Perkeep (vinculado arriba) tiene un modo de este tipo, que es bueno para acelerar el desarrollo incremental donde no le importa vincular un binario grande.

mjibson / esc también hace esto, y es una gran mejora en la calidad de vida al desarrollar una aplicación web; no solo ahorra tiempo de vinculación, sino que también evita tener que reiniciar la aplicación, lo que puede llevar mucho tiempo y / o requerir la repetición de pasos adicionales para probar sus cambios, dependiendo de la implementación de la aplicación web.

Problemas con la situación actual:

  • El uso de una solución basada en go: generate aumenta el historial de git con una segunda copia (y un poco más grande) de cada archivo.

Metas:

  • no registre los archivos generados

Bueno, esta parte se puede resolver fácilmente simplemente agregando los archivos generados al archivo .gitignore o equivalente. Siempre hice eso ...

Por lo tanto, alternativamente, Go podría tener su propia herramienta de incrustación "oficial" que se ejecuta de forma predeterminada en go build y pedirle a la gente que ignore estos archivos como una convención. Esa sería la solución menos mágica disponible (y compatible con versiones anteriores de Go).

Solo estoy haciendo una lluvia de ideas / pensando en voz alta aquí ... pero en realidad me gusta la idea propuesta en general. 🙂

Además, dado que las directivas //go:generate no se ejecutan automáticamente en go build el comportamiento de go build puede parecer un poco inconsistente: //go:embed funcionará automáticamente pero para //go:generate tienes que ejecutar go generate manualmente. ( //go:generate ya puede romper el flujo go get si genera archivos .go necesarios para la compilación).

//go:generate ya puede interrumpir el flujo de go get si genera .go archivos necesarios para la compilación

Creo que el flujo habitual para eso, y el que generalmente he usado, aunque me costó un poco acostumbrarme, es usar go generate completo como una herramienta de final de desarrollo y simplemente confirmar los archivos que genera.

@bradfitz no necesita implementar http.FileSystem sí mismo. Si la implementación proporciona un tipo que implementa http.File , entonces sería trivial para cualquiera, incluido el paquete http stdlib, proporcionar un contenedor alrededor de la función Open , convirtiendo el tipo en http.File para cumplir con http.FileSystem

Sin embargo, @andreynering //go:generate y //go:embed son muy diferentes. Este mecanismo puede suceder sin problemas en el momento de la compilación porque no ejecutará código arbitrario. Creo que eso lo hace similar a cómo cgo puede generar código como parte de go build .

No estoy convencido de la complejidad de una perilla "comprimida vs no". Si hacemos eso, la gente querrá que agreguemos control sobre qué compresión, nivel de compresión, etc. Todo lo que debemos agregar es la capacidad de incrustar un archivo de bytes simples.

Si bien aprecio el impulso por la simplicidad, también debemos asegurarnos de satisfacer las necesidades de los usuarios.

12 de las 14 herramientas enumeradas en https://tech.townsourced.com/post/embedding-static-files-in-go/#comparison admiten la compresión, lo que sugiere que es un requisito bastante común.

No estoy seguro de estar de acuerdo con este razonamiento.

La compresión realizada por las otras bibliotecas es diferente de agregarla a esta propuesta en que no reducirán el rendimiento en las compilaciones posteriores, ya que las alternativas generalmente se generan antes de la compilación en lugar de durante el tiempo de compilación.

Los tiempos de compilación bajos son un valor agregado claro con Go over otros lenguajes y la compresión intercambia el tiempo de CPU para una huella de almacenamiento / transferencia reducida. Si muchos paquetes de Go comienzan a ejecutar compresiones en go build , agregaremos aún más tiempo de compilación que el tiempo agregado simplemente copiando activos durante las compilaciones. Soy escéptico de agregar compresión porque otros lo hacen. Siempre que el diseño inicial no impida por diseño una extensión futura que agregue soporte para, por ejemplo, compresión, ponerlo allí porque podría ser algo que podría beneficiar a algunos parece una cobertura innecesaria.

No es que la incrustación de archivos sea inútil sin compresión, la compresión es una buena opción para reducir el tamaño binario de tal vez 100 MB a 50 MB, lo cual es genial, pero tampoco es un factor decisivo para la funcionalidad de la mayoría de las aplicaciones que se me ocurren. . Especialmente no si la mayoría de los activos "más pesados" son archivos como JPEG o PNG que ya están bastante bien comprimidos.

¿Qué hay de mantener la compresión fuera por ahora y agregarla si en realidad mucha gente la pasa por alto? (y se puede hacer sin costos indebidos)

Para agregar al comentario anterior de @sakjur : la compresión me parece ortogonal. Por lo general, quiero comprimir un archivo binario completo o de lanzamiento, y no solo los activos. Particularmente cuando los binarios de Go en Go pueden llegar fácilmente a decenas de megabytes sin ningún activo.

@mvdan Supongo que una de mis preocupaciones es que, muy a menudo, cuando he visto incrustaciones es junto con algún otro preprocesamiento: minificación, compilación de mecanografiado, compresión de datos, procesamiento de imágenes, cambio de tamaño de imágenes, hojas de sprites. La única excepción son los sitios web que solo usan html/template . Entonces, al final, puede terminar usando algún tipo de "Makefile" de todos modos o cargando el contenido preprocesado. En ese sentido, creo que una bandera de línea de comandos funcionaría mejor con otras herramientas que los comentarios.

Supongo que una de mis preocupaciones es que, muy a menudo, cuando he visto la incrustación es junto con algún otro preprocesamiento: minificación, compilación de mecanografiado, compresión de datos, procesamiento de imágenes, cambio de tamaño de imágenes, hojas de sprites. La única excepción son los sitios web que solo usan html / template.

Gracias, es un dato útil. Quizás la necesidad de compresión no sea tan común como parecía. Si ese es el caso, estoy de acuerdo en que tiene sentido omitirlo.

No es que la incrustación de archivos sea inútil sin compresión, la compresión es una buena opción para reducir el tamaño binario de tal vez 100 MB a 50 MB, lo cual es genial, pero tampoco es un factor decisivo para la funcionalidad de la mayoría de las aplicaciones que se me ocurren. .

El tamaño binario es un gran problema para muchos desarrolladores de go (https://github.com/golang/go/issues/6853). Go comprime la información de depuración de DWARF específicamente para reducir el tamaño binario, aunque esto tiene un costo para el tiempo de enlace (https://github.com/golang/go/issues/11799, https://github.com/golang/go/ números / 26074). Si hubiera una manera fácil de reducir el tamaño binario a la mitad, creo que los desarrolladores aprovecharían esa oportunidad (aunque dudo que las ganancias aquí sean casi tan significativas).

Sin embargo, eso realmente no ayuda en el modo GOPATH.

Tal vez, si está en el modo GOPATH, esta función simplemente no se aplica, ya que imagino que el equipo de Go no planea hacer paridad de funciones para GOPATH para siempre. Ya hay características que no son compatibles con GOPATH (como seguridad con base de datos de suma de comprobación, descarga de dependencias a través de un servidor proxy y control de versiones de importación semántica)

Como mencionó @bcmills , tener el nombre del directorio estático en un archivo go.mod es una excelente manera de introducir esta función en Go 1.15, ya que la función se puede desactivar automáticamente en archivos go.mod que tienen una cláusula <= go1.14.

Dicho esto, esto también significa que los usuarios deben escribir manualmente cuál es la ruta del directorio estático.

Creo que el directorio de proveedores y las convenciones _test.go son excelentes ejemplos de cómo facilitaron mucho el trabajo con Go y esas dos funciones.

No recuerdo que muchas personas hayan solicitado la opción de personalizar el nombre del directorio del proveedor o que tengan la capacidad de cambiar la convención _test.go por otra. Pero si Go nunca presenta la función _test.go, las pruebas en Go se verían muy diferentes hoy.

Por lo tanto, tal vez un nombre menos genérico que static brinde mejores posibilidades de no colisión y, por lo tanto, tener un directorio convencional (similar a vendor y _test.go) podría ser una mejor experiencia de usuario en comparación con los comentarios mágicos.

Ejemplos de nombres de colisiones potencialmente bajas:

  • _embed - sigue la convención _test.go
  • go_binary_assets
  • .gobin sigue la convención .git
  • runtime_files - para que coincida con la estructura runtime.Files

Por último, el directorio vendor se agregó en Go 1.5. Entonces, ¿tal vez no sea tan malo agregar una nueva convención ahora? 😅

Creo que debería exponer un mmap-readonly []byte . Solo acceso sin formato a las páginas del ejecutable, paginado por el sistema operativo según sea necesario. Todo lo demás se puede proporcionar además de eso, con solo bytes.NewReader .

Si esto es inaceptable por alguna razón, proporcione ReaderAt no solo ReadSeeker ; la última es trivial de construir a partir de la primera, pero la otra forma no es tan buena: necesitaría un mutex para proteger el desplazamiento único y arruinar el rendimiento.

No es que la incrustación de archivos sea inútil sin compresión, la compresión es una buena opción para reducir el tamaño binario de tal vez 100 MB a 50 MB, lo cual es genial, pero tampoco es un factor decisivo para la funcionalidad de la mayoría de las aplicaciones que se me ocurren. .

El tamaño binario es un gran problema para muchos desarrolladores de Go (# 6853). Go comprime la información de depuración de DWARF específicamente para reducir el tamaño binario, aunque esto tiene un costo para el tiempo de enlace (# 11799, # 26074). Si hubiera una manera fácil de reducir el tamaño binario a la mitad, creo que los desarrolladores aprovecharían esa oportunidad (aunque dudo que las ganancias aquí sean casi tan significativas).

Ese es definitivamente un punto justo y puedo ver cómo mi argumento puede verse como un argumento a favor del descuido con respecto al tamaño de los archivos. Esa no era mi intención. Mi punto está más en línea con el envío de esta función sin compresión, que aún sería útil para algunos, y podrían proporcionar comentarios e ideas útiles sobre cómo agregar compresión correctamente de una manera que se sienta correcta a largo plazo. Los activos pueden aumentar de una manera que es poco probable que lo haga la información de depuración y es más fácil para los desarrolladores de paquetes que son instalados / importados por otros reducir el rendimiento de la compilación innecesariamente si la implementación lo facilita.

Otra opción sería convertir la compresión de activos en un indicador de compilación y dejar el compromiso entre el tamaño y el tiempo de compilación en manos del constructor en lugar del desarrollador. Eso acercaría la decisión al usuario final del binario, que podría tomar una decisión sobre si la compresión vale la pena. Otoh, esto correría el riesgo de crear un área de superficie aumentada para las diferencias entre el desarrollo y la producción, por lo que no es un método claro mejor que cualquier otra cosa y no es algo por lo que siento que me gustaría defender.

Mi herramienta de incrustación de activos actual carga contenido de los archivos de activos cuando se crea con -tags dev . Alguna convención como esa probablemente también sería útil aquí; acorta significativamente el ciclo de desarrollo cuando, por ejemplo, se juega con HTML o una plantilla.

De lo contrario, la persona que llama tendrá que envolver este mecanismo de nivel inferior con algunos envoltorios *_dev.go y *_nodev.go e implementar la carga no incrustada para el escenario dev . Ni siquiera es difícil, pero ese camino solo conducirá a una explosión de herramientas similar a la que describe el primer comentario sobre este tema. Esas herramientas tendrán que hacer menos que hoy, pero aún así se multiplicarán.

Creo que -tags dev no funcionar cuando se ejecuta fuera del módulo Go sería razonable (no puedo averiguar de dónde cargar los activos).

¿Qué pasa con un go tool embed que toma entradas y produce archivos de salida Go en un formato especial reconocido por la computadora como archivos incrustados a los que luego se puede acceder a través de runtime/emved o algo así? Entonces podrías hacer un simple //go:generate gzip -o - static.txt | go tool embed -o static.go .

Una gran desventaja, por supuesto, es que luego debe confirmar los archivos generados.

@DeedleFake este problema comenzó con

El uso de una solución basada en go: generate aumenta el historial de git con una segunda copia (y un poco más grande) de cada archivo.

Woops. No importa. Perdón.

No es que la incrustación de archivos sea inútil sin compresión, la compresión es una buena opción para reducir el tamaño binario de tal vez 100 MB a 50 MB, lo cual es genial, pero tampoco es un factor decisivo para la funcionalidad de la mayoría de las aplicaciones que se me ocurren. .

El tamaño binario es un gran problema para muchos desarrolladores de Go (# 6853). Go comprime la información de depuración de DWARF específicamente para reducir el tamaño binario, aunque esto tiene un costo para el tiempo de enlace (# 11799, # 26074). Si hubiera una manera fácil de reducir el tamaño binario a la mitad, creo que los desarrolladores aprovecharían esa oportunidad (aunque dudo que las ganancias aquí sean casi tan significativas).

Si es necesario, la gente tendrá los datos comprimidos comprometidos e incrustados, y habrá paquetes para proporcionar una capa entre runtime.Embed y el consumidor final que realiza la descompresión en línea.

Y luego de un año o dos a partir de ahora habrá un nuevo problema sobre la adición de compresión y se puede solucionar en ese momento.

Digo esto como uno de los 15 estándares en competencia cuando escribí goembed :)

@ tv42 escribió:

Creo que debería exponer un mmap-readonly []byte . Solo acceso sin formato a las páginas del ejecutable, paginado por el sistema operativo según sea necesario.

Este comentario se pasa por alto fácilmente y es increíblemente valioso.

@ tv42 ,

Creo que debería exponer un byte mmap-readonly []. Solo acceso sin formato a las páginas del ejecutable, paginado por el sistema operativo según sea necesario. Todo lo demás se puede proporcionar además de eso, con solo bytes.NewReader.

El tipo que ya es de solo lectura es string . Además: proporciona un tamaño, a diferencia de io.ReaderAt , y no depende de la biblioteca estándar. Eso es probablemente lo que queremos exponer.

El tipo que ya es de solo lectura es string .

Pero todo el ecosistema de Write etc funciona en []byte . Es simple pragmatismo. No veo que esa propiedad de solo lectura sea más un problema que io.Writer.Write docs diciendo

La escritura no debe modificar los datos del segmento, ni siquiera temporalmente.

Otro posible inconveniente es que al incrustar un directorio con go:generate puedo comprobar la salida de git diff y ver si hay algún archivo allí por error. ¿Con esta propuesta -? ¿Quizás el comando go imprimiría la lista de archivos que está incrustando?

@ tv42

Pero todo el ecosistema de Write, etc. funciona en [] bytes.

html/template embargo,

Go ya te permite usar -ldflags -X para configurar algunas cadenas (útil para configurar la versión de git, el tiempo de compilación, el servidor, el usuario, etc.), ¿podría extenderse este mecanismo para configurar io.Readers en lugar de cadenas?

@bradfitz ¿

@ tv42 Dijiste Write pero supongo que te refieres a Read . Puede convertir un string en un io.ReaderAt usando strings.NewReader , por lo que usar una cadena no parece una barrera allí.

@andreynering A string puede contener cualquier secuencia de bytes.

Un string puede contener cualquier secuencia de bytes.

Sí, pero su intención principal es contener texto y no datos arbitrarios. Supongo que esto puede causar un poco de confusión, en particular para los desarrolladores de Go sin experiencia.

Sin embargo, entendí totalmente la idea. Gracias por aclararlo.

@ianlancetaylor

Se supone que Read muta el segmento pasado. Write no lo es. Por lo tanto, la documentación de Write dice que esto no está permitido. No veo que se necesite nada más que documentar que los usuarios no deben escribir en el []byte devuelto.

El hecho de que strings.Reader exista no significa que io.WriteString encontrará una implementación eficiente de la escritura de cadenas. Por ejemplo, TCPConn no tiene WriteString .

Odiaría que Go incluyera una nueva función como esta solo para forzar la copia de todos los datos solo para escribirlos en un socket.

Además, la suposición general es que las cadenas son imprimibles por humanos y []byte menudo no lo es. Poner archivos JPEG en cadenas provocará muchos terminales desordenados.

@opennota

html / template funciona con cadenas, sin embargo.

Sí, eso es extraño, solo toma archivos por nombre de ruta, no como lectores. Dos respuestas:

  1. No hay razón para que los datos incrustados no puedan tener ambos métodos Bytes() []byte y String() string .

  2. Con suerte, no está analizando una plantilla cada una de las solicitudes; mientras que realmente tiene que enviar los datos de un JPEG a un socket TCP para cada solicitud que lo solicite.

@ tv42 Podemos agregar WriteString métodos según sea necesario.

No creo que el uso más común de esta funcionalidad sea escribir los datos sin modificar, por lo que no creo que debamos optimizar para ese caso.

No creo que el uso más común de esta funcionalidad sea escribir los datos sin modificar,

Creo que el uso más común de esta funcionalidad será servir activos web, imágenes / js / css, sin modificar.

Pero no confíe en mi palabra, echemos un vistazo a algunos de los importadores del archivo embebido de Brad:

#fileembed pattern .+\.(js|css|html|png|svg|js.map)$
#fileembed pattern .*\.png



md5-f8b48fccd03599094034bf2b507e9e67



#fileembed pattern .*\.js$

Etcétera..

Para datos anecdóticos: sé que si esto se implementara, lo usaría inmediatamente en dos lugares en el trabajo, y ambos serían para proporcionar acceso sin modificaciones a archivos de texto estáticos. Ahora mismo usamos un paso //go:generate para convertir los archivos a cadenas constantes (formato hexadecimal).

Votaría por el nuevo paquete en contraposición a la directiva. Mucho más fácil de controlar, más fácil de manejar / administrar y mucho más fácil de documentar y extender. por ejemplo, ¿puede encontrar fácilmente la documentación de una directiva de Go como "go: generate"? ¿Qué pasa con la documentación del paquete "fmt"? ¿Ves a dónde voy con esto?

Por lo tanto, alternativamente, Go podría tener su propia herramienta de inserción "oficial" que se ejecuta de forma predeterminada en go build

@andreynering Sé que otros administradores de paquetes y herramientas de lenguaje lo permiten, pero ejecutar código / comandos arbitrarios en el momento de la compilación es una vulnerabilidad de seguridad (por lo que espero que sean razones obvias).

Dos cosas más me vienen a la mente cuando pienso en esta función:

  • ¿Cómo funcionaría la incrustación de archivos automáticamente con el caché de compilación?
  • ¿Impide construcciones reproducibles? Si los datos se modifican de alguna manera (por ejemplo, comprimiéndolos), debe tener en cuenta la reproducibilidad.

stuffbin , vinculado en el primer comentario, se creó principalmente para permitir que las aplicaciones web autohospedadas incrusten activos estáticos (HTML, JS ...). Este parece ser un caso de uso común.

Salvo la discusión sobre compilación / compresión, otro punto débil es la falta de una abstracción del sistema de archivos en stdlib porque:

  • En la máquina de un desarrollador, los numerosos go run sy las compilaciones no tienen por qué verse sobrecargados por la sobrecarga de incrustar (mientras que, opcionalmente, comprimir) los activos. Una abstracción del sistema de archivos permitiría fácilmente _failover_ al sistema de archivos local durante el desarrollo.

  • Los activos pueden cambiar activamente durante el desarrollo, por ejemplo, una interfaz de Javascript completa en una aplicación web. La capacidad de cambiar sin problemas entre incrustado y el sistema de archivos local en lugar de los activos integrados permitiría evitar la compilación y la repetición del binario de Go solo porque los activos cambiaron.

Editar: Para concluir, si el paquete incrustado pudiera exponer una interfaz similar a un sistema de archivos, algo mejor que http.FileSystem, resolvería estas preocupaciones.

La capacidad de cambiar sin problemas entre incrustación y el sistema de archivos local

Seguramente esto se puede implementar a nivel de aplicación, y está más allá del alcance de esta propuesta, ¿no?

Seguramente esto se puede implementar a nivel de aplicación, y está más allá del alcance de esta propuesta, ¿no?

Lo siento, me acabo de dar cuenta, la forma en que lo redacté es ambigua. No estaba proponiendo una implementación de sistema de archivos dentro del paquete embed, sino solo una interfaz, algo mejor que http.FileSystem . Eso permitiría a las aplicaciones implementar cualquier tipo de abstracción.

Editar: error tipográfico.

@knadh Totalmente de acuerdo en que debería funcionar cuando solo usas go run también, la forma en que Packr maneja esto es realmente agradable. Sabe dónde están tus archivos, si no están incrustados en la aplicación, los carga desde el disco, ya que espera que sea básicamente "modo de desarrollo".

El autor de Packr también ha lanzado una nueva herramienta Pkger que está más centrada en los módulos Go. Todos los archivos son relativos a la raíz del módulo Go. Me gusta mucho esa idea, pero parece que Pkger no ha implementado la carga de desarrollo local desde el disco. En mi opinión, una combinación de ambos sería increíble.

No sé si ya está fuera de ejecución, pero si bien el "enfoque de paquete integrado" es bastante mágico, también proporciona algo de asombro porque la herramienta puede deducir qué hacer con el archivo en función de la llamada. por ejemplo, la API podría ser algo como

package embed
func FileReader(name string) io.Reader {…}
func FileReaderAt(name string) io.ReaderAt {…}
func FileBytes(name string) []byte {…}
func FileString(name string) string {…}

Si la herramienta go encuentra una llamada a FileReaderAt , sabe que los datos deben descomprimirse. Si solo encuentra llamadas FileReader , sabe que puede almacenar datos comprimidos. Si encuentra una llamada FileBytes , sabe que necesita hacer una copia, si solo encuentra FileString , sabe que puede servir desde la memoria de solo lectura. Etcétera.

No estoy convencido de que esta sea una forma razonable de implementar esto para la herramienta Go propiamente dicha. Pero quería mencionar esto, ya que permite obtener los beneficios de la compresión y la incrustación de copia cero sin tener perillas reales.

[editar] también, por supuesto, vamos a agregar estas cosas extrañas después del hecho, enfocándonos primero en un conjunto de características más mínimo [/ editar]

Si solo encuentra llamadas a FileReader ...

Esto excluiría el uso de los otros métodos a través de la reflexión.

[Editar] En realidad, creo que las implicaciones son más amplias que eso. Si el uso de FileReaderAt es una indicación de que los datos deben descomprimirse, entonces el uso de FileReaderAt() con cualquier entrada que no sea const implica que todos los archivos deben almacenarse sin comprimir.

No sé si eso es bueno o malo. Creo que la heurística mágica no será tan útil como podría parecer a primera vista.

Un argumento a favor de un comentario pragma ( //go:embed ) en lugar de un nombre de directorio especial ( static/ ): un comentario nos permite incrustar un archivo en el archivo de prueba para un paquete (o el archivo xtest ) pero no la biblioteca bajo prueba. El comentario solo debe aparecer en un archivo _test.go .

Espero que esto solucione un problema común con los módulos: es difícil acceder a los datos de prueba de otro paquete si ese paquete está en otro módulo. Un paquete podría proporcionar datos para otras pruebas con un comentario como //go:embedglob testdata/* en un archivo _test.go . El paquete podría importarse a un binario normal que no sea de prueba sin tener que extraer esos archivos.

@ fd0 ,

¿Cómo funcionaría la incrustación de archivos automáticamente con el caché de compilación?

Todavía funcionaría. Los hashes del contenido del archivo incrustado se mezclarían con la clave de caché.

¿Sería posible (o incluso una buena idea) tener un módulo / paquete / mecanismo que fuera prácticamente transparente, ya que desde dentro de su aplicación simplemente intenta abrir una ruta como

internal://static/default.css

y las funciones de Archivo leerán datos desde el interior del binario, o desde una ubicación alternativa
ex Package.Mount("internal[/<folder>.]", binary_path + "/resources/")

para crear "internal: //" con todos los archivos en el binario, recurra a la ruta ejecutable / recursos / si está en modo dev o si el archivo no se encuentra en binario (y tal vez lanzar una advertencia o algo para propósitos de registro)

Esto permitiría, por ejemplo, tener

Package.Mount("internal", binary_path  + "/resources/private/")
Package.Mount("anotherkeyword", binary_path  + "/resources/content/")

Probablemente sea mejor bloquear la ubicación alternativa a la ruta del ejecutable cuando está en modo 'lanzamiento', pero relaje esto en modo dev (permita solo carpetas en go_path o algo así)

De forma predeterminada, el paquete "monta" internal: // o alguna otra palabra clave, pero permite que el usuario cambie el nombre si lo desea ... por ejemplo, .ReMount ("internal", "myCustomName") o algo por el estilo.

Otra cosa ... ¿tendría sentido verificar el último cambio / hora de modificación en una ubicación alternativa y anular automáticamente el archivo interno si existe dicho archivo fuera de la aplicación (tal vez tenga una bandera que lo permita, configurable por el programador antes de la compilación)
Esto puede ser necesario para parches de aplicaciones súper rápidos, en los que no desea esperar a que se cree y distribuya una nueva compilación ... simplemente puede crear la carpeta y copiar el archivo allí, y el binario cambiará al nuevo. expediente.

En Windows, ¿sería posible o tendría sentido usar recursos (como en un blob de datos binarios en un recurso)?
Y un poco sin relación, pero tal vez este paquete también podría ocuparse de agrupar íconos en el ejecutable, o datos de manifiesto, o tal vez incluso otros recursos. Me doy cuenta de que es solo Windows ...
Me imagino que el constructor podría registrar las últimas fechas de modificación / cambio de los archivos en las carpetas alternativas y solo activar una "creación de blob de datos" si un archivo cambia y almacena el blob en algún lugar.
Tal vez solo cree un archivo de "caché" si el usuario elige habilitar la compresión en estos archivos empaquetados (si finalmente se decide comprimirlos) ... si se elige la compresión, solo el archivo en particular que se modificó tendría que ser recomprimido en el momento de la compilación , otros archivos simplemente se copiarían en el binario desde la caché.

Un problema que veo es que si el paquete permite nombres personalizados, necesitaría tener una lista negra de algún tipo, como no permitir "udp, archivo, ftp, http, https y varias otras palabras clave populares".

En cuanto al almacenamiento como matriz de bytes / cadena o compresión ... en mi humilde opinión, sea cual sea la decisión que se tome, debería dejar espacio para actualizar fácilmente en el futuro ... por ejemplo, podría comenzar sin compresión y solo tener una lista de compensaciones y tamaños de archivo y nombres de archivos, pero permiten agregar compresión fácilmente en el futuro (ex método zlib, lzma, tamaño comprimido, tamaño sin comprimir si es necesario para asignar suficiente memoria para descomprimir fragmentos, etc.

Personalmente, estaría feliz si el ejecutable se pudiera empaquetar con UPX o equivalente, supongo que el binario se descomprimiría en la memoria y todo funcionaría.

Algunos pensamientos que se relacionan indirectamente:

  • Me gusta el enfoque package embed porque usa la sintaxis Go
  • Creo que la necesidad de compresión y otras manipulaciones no se trata del tamaño binario, se trata de querer almacenar solo la forma de contenido más compatible con las diferencias en un repositorio, para que no haya un estado "desincronizado" en el que alguien se olvide de regenerar y enviar un formulario comprimido al cambiar la "fuente", y para que el paquete permanezca "disponible para obtener". Sin abordar estos puntos, solo estamos resolviendo el problema de estandarización, que puede ser aceptable, pero no parece ideal.
  • Creo que podríamos eludir la necesidad de que la cadena de herramientas soporte activamente compresiones / transformaciones específicas si la interacción embed puede proporcionar opcionalmente un "códec". Exactamente cómo definir el códec depende de la sintaxis de integración, pero imagino algo como
package embed

type Codec interface {
    // Encode transforms a source representation to an in-binary encoded asset.
    Encode(io.Writer, io.Reader) error

    // Decode transforms an in-binary asset to its active representation that the embedded application wants to use.
    Decode(io.Writer, io.Reader) error
}

Esto puede cubrir casos de uso muy específicos, como este ideado:

package main

func NewJSONShrinker() embed.Codec {
   return jsonShrinker{}
}

type jsonShrinker struct{}
func (_ jsonShrinker)  Encode(io.Writer, io.Reader) error {
    // use json.Compact + gzip.Encode...
}
func (_ jsonShrinker)  Decode(io.Writer, io.Reader) error {
    // use gzip.Decode + json.Indent
}

Usarlo podría verse como

// go:embed file.name NewJSONShrinker

func main() {
    embed.NewFileReader("file.name") // codec is implied by the comment above
}

o posiblemente

func main() {
    f, err := embed.NewFileReaderCodec("file.name", NewJSONShrinker())
    ...
}

En la segunda forma, existe la complicación de que la cadena de herramientas necesita entender estáticamente qué códec usar, porque tiene que hacer el paso Encode en tiempo de compilación. Por lo tanto, tendríamos que rechazar cualquier valor de Codec que no se pueda determinar fácilmente en el momento de la compilación.

Dadas estas dos opciones, creo que elegiría el comentario mágico más los códecs. Da como resultado una característica más poderosa que aborda todos los objetivos establecidos aquí. Además, no creo que los comentarios mágicos sean inaceptables aquí. Ya los toleramos a través de go:generate para este propósito en este momento. En todo caso, uno podría considerar que el paquete mágico solo es más una desviación de los modismos actuales. El ecosistema Go en este momento no tiene muchas características que permitan que un archivo fuente indique a la cadena de herramientas que use archivos fuente adicionales, y creo que el único que no es un comentario mágico en este momento es la palabra clave import .

Si hacemos compresión, no habrá ningún tipo de códec ni perillas de nivel de compresión. Es decir, tener alguna perilla es el mayor argumento para no admitir la compresión en absoluto.

La única opción que me gustaría exponer, si es que hay alguna, es: acceso aleatorio o no. Si no necesita acceso aleatorio, las herramientas y el tiempo de ejecución pueden elegir la compresión adecuada y no exponerla a los usuarios. Y probablemente cambiaría / mejoraría con el tiempo.

Pero he llegado al lado de @rsc de no tener compresión debido a una comprensión que tenía: el contenido que es más comprimible (HTML, JS, CSS, etc.) es el contenido al que aún le gustaría tener acceso aleatorio (para ser atendido a través de, digamos, http.FileServer , que admite solicitudes de rango)

Y mirando el tamaño combinado de HTML / CSS / JS de Perkeep que incrustamos, es de 48 KB sin comprimir. El binario del servidor Perkeep es de 49 MB. (Estoy ignorando el tamaño de las imágenes incrustadas porque ya están comprimidas). Así que parece que no vale la pena, pero podría agregarse más tarde.

De una discusión con @rsc , parece que podríamos hacer una combinación de los enfoques anteriores:

En tiempo de ejecución del paquete,

package runtime

type Files struct {
     // unexported field(s), at least 1 byte long so Files has a unique address
}

func (f *Files) Open(...) (...) { ...}
func (f *Files) Stat(...) (...) { ...}
func (f *Files) EnumerateSomehow(...) { ...}

Luego en tu código:

package yourcode

//go:embed static/*
//go:embed logo.jpg
var website runtime.Files

func F() {
     ... = website.Open("logo.jpg")
}

Luego, la herramienta cmd / go analizaría los comentarios go:embed y agruparía esos patrones + hash esos archivos y los registraría con el tiempo de ejecución, usando &website .

El tiempo de ejecución tendría efectivamente un mapa de cada dirección de Archivos a lo que son sus contenidos y en qué parte del archivo ejecutable están (o cuáles son sus nombres de sección ELF / etc). Y tal vez si admiten o no el acceso aleatorio, si terminamos haciendo alguna compresión.

@gdamore ,

Solo otra arruga aquí: tengo un proyecto diferente que usa el código fuente de DTrace (incrustado). Esto es sensible a las diferencias entre n y rn.
...
Si varios munging no funcionan, entonces ese es un argumento en contra del uso de esta nueva función.

También puede munge en tiempo de ejecución para eliminar cualquier retorno de carro que se incruste de los usuarios de Windows que ejecutan go install. He escrito ese filtro io.Reader un par de veces.

Pero he llegado al lado de @rsc de no tener compresión debido a una comprensión que tenía: el contenido que es más comprimible (HTML, JS, CSS, etc.) es el contenido al que aún le gustaría tener acceso aleatorio (para ser atendido a través de, digamos, http.FileServer, que admite solicitudes de rango)

La compresión y el acceso aleatorio no son completamente excluyentes. Vea, por ejemplo, un poco de discusión aquí: https://stackoverflow.com/questions/429987/compression-formats-with-good-support-for-random-access-within-archives

La compresión y el acceso aleatorio no son completamente excluyentes

Sí, si quisiéramos una búsqueda de grano grueso con algo de sobrecarga para llegar a la posición correcta. Trabajé un poco en este espacio con

Me temo que la sobrecarga sería lo suficientemente grande como para que no quisiéramos hacer eso automáticamente para las personas.

Lo suficientemente justo. La pregunta importante es si preferiríamos una API que nos permita cambiar de opinión sobre esto de forma económica más adelante, si las necesidades cambian o si los experimentos muestran que la sobrecarga es aceptable.

@bradfitz buen punto. Y ciertamente puedo hacer eso. FWIW, en mi repositorio también configuré git para que sea menos tóxico al ver archivos .d. Aún así, encuentro útil la propiedad de las cadenas incrustadas con comillas inversas, ya que es predecible y no está sujeta a los caprichos de git o del sistema.

A lo que me refería con la idea del códec es que la compresión no es la única transformación que uno podría desear y que un tipo de códec proporcionado por el usuario permite que la cadena de herramientas ignore indicadores distintos de "qué códec". Cualquier nivel de compresión, o el algoritmo en absoluto, compresión o de otro tipo, tendría que ser específico del códec utilizado. Estoy totalmente de acuerdo en que tratar de "admitir la compresión" en el sentido de proporcionar un conjunto específico de formatos y controles sería una búsqueda inútil con todas las variaciones que la gente podría pedir. De hecho, estaría más entusiasmado con los usos poco comunes, como el preprocesamiento de datos i18n, quizás, o el procesamiento de conjuntos de datos como en latlong , así que creo que vale la pena considerar opciones a su alrededor.

Pensé en otra forma de proporcionar la misma flexibilidad que podría ser más agradable. La directiva // go:embed podría ser una invocación de comando, al igual que // go:generate . Para el caso más simple, algo como

// go:embed "file.name" go run example.com/embedders/cat file.name

La diferencia clave es, por supuesto, que la salida estándar de la invocación del comando está incrustada bajo el nombre proporcionado. El ejemplo también usa un paquete de simulación con go run para mostrar cómo se haría para hacer que el comando sea independiente del sistema operativo, ya que cat puede no estar disponible en todas las compilaciones de Go.

Eso se encarga del paso de "codificar" de la transformación, y quizás la tarea del paso de "decodificar" se pueda dejar al usuario. El paquete runtime / embed solo puede proporcionar los bytes que el usuario solicitó a la cadena de herramientas que insertara, cualquiera que sea la codificación. Esto está bien porque el usuario sabe cuál debería ser el proceso de decodificación.

Una gran desventaja de esto es que no veo una buena manera de incrustar un globo de varios archivos de esta manera, más allá de que los bytes incrustados sean un zip o algo así. Eso podría ser lo suficientemente bueno, ya que un comando zip aún podría usar un glob, y es en el lado de la definición donde realmente te preocupas por el glob. Pero también podríamos tener dos características de esta propuesta, una para hacer incrustaciones simples y otra para ejecutar incrustaciones de generador.

Un posible inconveniente que se me ocurrió es que agrega un paso de final abierto en la compilación, asumiendo que las incrustaciones deben ser manejadas por go build y no requieren una invocación adicional de la cadena de herramientas como lo hace go generate . Aunque creo que está bien. Quizás se puede esperar que la herramienta administre su propia caché para evitar la repetición de operaciones costosas, o quizás pueda comunicarse con la cadena de herramientas para usar la caché de Go. Eso suena como un problema que se puede resolver y encaja con el tema general de go build haciendo más por nosotros (como buscar módulos).

¿Uno de los objetivos de este proyecto es garantizar que las compilaciones de Go no requieran herramientas externas ni go: generar líneas?

Si no, parece que vale la pena mantener las cosas simples y solo admitir un segmento de bytes o una cadena porque si el usuario quiere compresión con muchas perillas, puede hacerlo en su archivo make (o similar), ir a generar línea, etc. antes de construir de todos modos, por lo que no parece que valga la pena sumarlos a cualquiera que sea el resultado de esta propuesta.

Si no requerir Make o similar es un objetivo, entonces supongo que podría tener sentido usar la compresión, pero personalmente usaría Make, go generate, etc. .

@SamWhited ,

¿Uno de los objetivos de este proyecto es garantizar que las compilaciones de Go no requieran herramientas externas ni go: generar líneas?

Si.

Si la gente quiere usar go: generate o Makefiles u otras herramientas, hoy tienen docenas de opciones.

Queremos algo que sea portátil, seguro y correcto que funcione de forma predeterminada. (y para ser claros: seguro significa que no podemos ejecutar código arbitrario en el momento de "instalar", por la misma razón que go: generate no se ejecuta de forma predeterminada)

@ stephens2424

Creo que podríamos eludir la necesidad de que la cadena de herramientas soporte activamente compresiones / transformaciones específicas si la interacción de inserción puede proporcionar opcionalmente un "códec".

Sin ejecución de código arbitrario durante go build .

Sin ejecución de código arbitrario durante la compilación.

Sí, lo veo ahora. Supongo que no hay manera de conciliar tener solo archivos "fuente" comprometidos en un repositorio, querer archivos "procesados" incrustados, para que el paquete sea "fácil de obtener" _y_ mantener go build simple y seguro. Todavía estoy a favor de la estandarización aquí, pero supongo que esperaba tener mi pastel y comérmelo también. ¡Vale la pena intentarlo! ¡Gracias por solucionar el problema!

@flimzy

Esto excluiría el uso de los otros métodos a través de la reflexión.

No hay métodos en lo que mencioné, solo funciones. No se pueden detectar en tiempo de ejecución y no hay forma de hacer referencia a ellos sin mencionarlos por su nombre en la fuente. Y tenga en cuenta que los valores de interfaz devueltos por las diferentes funciones no tienen que ser del mismo tipo; de hecho, esperaría que sean tipos no exportados con exactamente el método requerido para implementar esa interfaz o una instancia de *strings.Reader etc., lo que tenga sentido en el contexto.

Sin embargo, podría decirse que la idea adolece de pasar las funciones exportadas del paquete embed como valores. Aunque incluso eso probablemente no sería un problema, la firma contiene un tipo no exportado (ver más abajo), por lo que no puede declarar una variable, argumento o retorno de su tipo. Puede pasarlos a reflect.ValueOf a sí mismos, en teoría. Ni siquiera sé si eso te permitiría llamarlos realmente (aún tendrías que construir un valor de su tipo de parámetro, que no se exporta. No sé si reflect lo permite).

Pero sea como sea: aún sería posible (y más simple) ser simplemente pesimista en caso de que cualquier función de nivel superior de embed se use como valor y asuma las restricciones que crea en todos los archivos incrustados. Lo que significaría que si decides hacer cosas extremadamente raras e inútiles con el paquete embed, perderás algunas optimizaciones (sobre las que no necesariamente hacemos ninguna promesa de todos modos). Parece justo.

De hecho, creo que las implicaciones son más amplias que eso. Si el uso de FileReaderAt es una indicación de que los datos deben descomprimirse, entonces el uso de FileReaderAt () con cualquier entrada no constante implica que todos los archivos deben almacenarse sin comprimir.

No tiene sentido permitir entradas que no sean constantes, ya que el nombre del archivo debe conocerse estáticamente para realizar la incrustación. Sin embargo, fue impreciso de mi parte usar string como el tipo de parámetros de nombre de archivo: realmente deberían haber sido un type filename string no

@Merovio

No tiene sentido permitir entradas no constantes

Creo que estamos hablando de cosas diferentes. Me refiero a las entradas a las funciones de acceso (es decir, FileReaderAt() ). Estoy seguro de que estará de acuerdo en que la entrada no constante tiene sentido allí.

Y mi punto es: Supongamos que hemos incrustado 100 archivos, pero tenemos una llamada FileReaderAt(filename) , donde filename no es constante; No hay forma de saber a cuál (si es que hay alguno) de los archivos incrustados se accederá de esta manera, por lo tanto, todos deben almacenarse sin comprimir.

@flimzy estábamos hablando de lo mismo, en serio, no pensé que los nombres de archivo no constantes tuvieran sentido :) Lo cual, pensándolo bien, estaba mal y era un descuido. Lo siento por eso. Las facilidades para agrupar o incluir directorios completos y luego iterar sobre ellos en realidad son bastante importantes, sí. Todavía creo que esto podría resolverse, por ejemplo, tomando la decisión por colección (dir / glob) y solo permitiendo seleccionar aquellos por nombres constantes, pero como dije: en realidad no es una API que consideraría súper apropiada para la herramienta Go debido a que mágico es. Entonces, entrar en la maleza de esta manera probablemente le esté dando al concepto más espacio en la discusión del que merece :)

Otro caso que no vi en los mensajes anteriores y que me hizo considerar incrustar un archivo en un binario de Go fue la imposibilidad de distribuir correctamente un paquete contenedor de una biblioteca compartida de C usando la compilación / instalación de go regular (la biblioteca compartida permanece en el fuentes).

No lo hice al final, pero eso definitivamente me haría reconsiderarlo para este caso. De hecho, la biblioteca C tiene muchas dependencias que serían más fáciles de distribuir como una biblioteca compartida. Esta biblioteca compartida se podría incrustar mediante los enlaces de Go.

¡¡¡Guau!!!

@ Julio-Guerra
Estoy bastante seguro de que aún tendría que extraerlos al disco y luego usar dlopen y dlsym para llamar a las funciones C.

Editar: malinterpreté un poco tu publicación, me acabo de dar cuenta de que estás hablando de crear un binario para la distribución

Fuera de los activos estáticos http, para los blobs incrustados que necesita en un puntero en la memoria, sería bueno tener una función que devolviera el puntero a la memoria incrustada que ya está en proceso. De lo contrario, habría que asignar nueva memoria y hacer una copia desde io.Reader. Eso consumiría el doble de memoria.

@glicerina , de nuevo, eso es string . Un string es un puntero y una longitud.

¿No sería genial tener alguna forma de marcar el código para que se ejecute en tiempo de compilación y proporcionar el resultado en tiempo de ejecución? De esa manera, podría leer cualquier archivo, comprimirlo si lo desea en tiempo de compilación y en tiempo de ejecución podría acceder a él. Eso funcionaría para algunos cálculos, ya que funcionaría para precargar el contenido del archivo.

@burka como se dijo antes en el hilo, go build no ejecutará código arbitrario.

@burka , eso está explícitamente fuera de alcance. Esa decisión (sin ejecución de código en el momento de la compilación) se tomó hace mucho tiempo y este no es el error para cambiar esa política.

Un efecto secundario de esta propuesta es que los proxies go nunca pueden optimizar los archivos que almacenan para que sean solo archivos go. Un proxy debe almacenar un repositorio completo porque no sabrá si el código de Go incrusta alguno de los archivos que no son de Go.

No sé si los proxies ya se optimizan para esto, pero es posible que algún día quieran hacerlo.

@leighmcculloch Aunque tampoco creo que este sea el caso hoy. Todos los archivos que no sean de Go en un paquete de Go deben incluirse en los archivos de módulo, ya que pueden ser necesarios para go test . También puede tener archivos C para cgo, como otro ejemplo.

Esta es una dirección emocionante, definitivamente la necesitamos para nuestros casos de uso.

Dicho esto, siento que hay diferentes casos de uso con diferentes requisitos, pero la mayoría de los que comentan sobre _cómo creen que debería hacerse, están imaginando implícitamente sus propios casos de uso, pero no los definen explícitamente.

Podría ser útil, al menos realmente útil para mí, si pudiéramos delinear los diferentes casos de los desafíos que presenta cada caso de uso.

Por ejemplo, nuestro caso de uso principal es incrustar HTML + CSS + JS + JPG +, etc. para que cuando se ejecute la aplicación go, pueda escribir esos archivos en un directorio de modo que puedan ser servidos por un http.FileServer . Dado que, en el caso de uso, la mayoría de los comentarios que he leído sobre lectores y escritores me han resultado ajenos porque no necesitamos acceder a los archivos desde Go, simplemente permitimos que go-bindata copie en el disco _ (aunque tal vez haya una manera de aprovechar mejores técnicas que simplemente aún no nos hemos dado cuenta que deberíamos considerar). _

Pero nuestros desafíos son los siguientes: normalmente usamos GoLand con su depurador y trabajaremos en la aplicación web realizando cambios continuos. Entonces, durante el desarrollo, necesitamos http.FileServer para cargar los archivos directamente desde nuestro directorio fuente. Pero cuando la aplicación se ejecuta, http.FileServer necesita leer esos archivos desde el directorio donde los archivos fueron escritos por la solución de incrustación. Lo que significa que cuando compilamos tenemos que ejecutar go-bindata para actualizar los archivos y luego registrarlos en Git. Y todo eso es generalmente viable con go-bindata , aunque ciertamente no es una idea.

Sin embargo, en otras ocasiones necesitamos ejecutar un ejecutable compilado, por lo que podemos adjuntar un depurador al programa en ejecución y aún así hacer que ese programa cargue archivos desde el directorio fuente y no desde el directorio donde el archivo incrustado está escrito por go-bindata . Actualmente no tenemos una buena solución para esto.

Entonces esos son nuestros casos de uso y desafíos. ¿Quizás otros podrían definir explícitamente los otros casos de uso y el conjunto de desafíos relacionados, de modo que estas discusiones puedan abordar explícitamente los diversos espacios de problemas y / o denotar explícitamente que este esfuerzo no abordará las necesidades específicas de un espacio de problemas dado?

Gracias de antemano por considerar.

Dado que no veo que se mencione como un caso de uso, también nos beneficiaríamos de esto para nuestro directorio de plantillas al que accedemos a través de template.ParseFiles.

Me parece que el enfoque más limpio es un método para participar a través de go.mod . Esto aseguraría que fuera compatible con versiones anteriores (ya que los proyectos existentes tendrían que optar por usarlo) y permitiría que las herramientas (como los proxies go) determinen qué archivos se necesitan. El comando go mod init podría actualizarse para incluir una versión predeterminada para nuevos proyectos para que sea más fácil de usar en el futuro.

Puedo ver argumentos para que el directorio sea un nombre estándar (si requerimos optar por participar, entonces puede ser un nombre más limpio / simple) o tener el nombre del directorio definido en go.mod y permitir a los usuarios elija el nombre (pero con un valor predeterminado proporcionado por go mod init .

En mi opinión, una solución como esta logra un equilibrio en la facilidad de uso y menos "magia".

@jayconrod escribió:

Un argumento a favor de un comentario pragma (// go: embed) en lugar de un nombre de directorio especial (estático /): un comentario nos permite incrustar un archivo en el archivo de prueba para un paquete (o el archivo xtest) pero no la biblioteca bajo prueba.

Esta es una muy buena observación. Aunque si quisiéramos usar el nombre de directorio especial, podríamos usar un mecanismo familiar: static para todas las compilaciones, static_test para compilaciones de prueba, static_amd64 para compilaciones amd64 y pronto. Sin embargo, no veo una forma obvia de proporcionar compatibilidad con etiquetas de compilación arbitrarias.

Podría haber un archivo de manifiesto en el directorio estático (por defecto, cuando se proporciona un manifiesto vacío, se incluye todo excepto el manifiesto) que incluye globs y permite especificar etiquetas de compilación y quizás compresión posterior, etc.

Una ventaja es que si go list llega a un directorio que contiene un manifiesto, puede omitir ese árbol al estilo # 30058

Una desventaja es que podría obtener muchos accesos y no gracias

Un mecanismo simple de control cero para agrupar archivos en un paquete podría ser un directorio especial go.files en un directorio de paquete (similar a go.mod en un módulo). El acceso estaría limitado a ese paquete, a menos que elija exportar un símbolo.

Editar: propuesta de función única runtime/files :

package files

func Open(name string) (io.ReadCloser, error) {
    // runtime opens embedded file based on caller package
    return rc, nil
}
package foo

import "runtime/files"

func ReadPackageFile(name string) ([]byte, error) {
    rc, err := files.Open(name)
    if err != nil {
        return nil, err
    }
    defer rc.Close()
    return ioutil.ReadAll(rc)
}

El enfoque import "C" ya ha sentado un precedente para las rutas de importación "mágicas". En mi opinión, ha funcionado bastante bien.

Dado que no veo que se mencione como un caso de uso, también nos beneficiaríamos de esto para nuestro directorio de plantillas al que accedemos a través de template.ParseFiles.

Hay otro desafío: si bien el binario puede contener todos los archivos necesarios, esos mismos archivos serían los predeterminados que proporciono como desarrollador. Sin embargo, el usuario final debe poder personalizar las plantillas como, por ejemplo, la impresión o las políticas de privacidad. Por lo que veo, eso significa que debe haber alguna forma de exportar mis archivos predeterminados y luego dejar que el binario use los archivos personalizados en tiempo de ejecución, o alguna forma de reemplazar las versiones incrustadas por las personalizadas.

Creo que eso podría hacerse proporcionando una API con funciones para 'exportar' y 'reemplazar' un recurso incrustado. Luego, el desarrollador podría proporcionar algunas opciones de línea de comando al usuario final (utilizando internamente las llamadas API mencionadas).

Todo esto, por supuesto, basado en la suposición de que realmente habrá algún tipo de incrustación que definitivamente facilitaría la implementación.

Gracias por abrir el problema. En el trabajo, hemos pensado en la misma idea de función, ya que necesitamos incrustar archivos en casi todos los proyectos de Golang. Las bibliotecas existentes funcionan bien, pero creo que esta es una característica que Golang está pidiendo a gritos. Es un lenguaje creado para convertirse en un único binario estático. Debería adoptar eso al permitirnos cargar los archivos de activos requeridos en el binario, con una API universal y amigable para los desarrolladores.

Solo quiero proporcionar rápidamente mis detalles de implementación favoritos. Varias personas han hablado de proporcionar automáticamente una API para leer los archivos incrustados, en lugar de necesitar otra señal como un comentario mágico. Creo que ese debería ser el camino a seguir, ya que ofrece una sintaxis programática familiar al enfoque. Optar por un paquete especial, posiblemente runtime/embed como se mencionó anteriormente, satisfaría eso y permitiría una fácil extensibilidad en el futuro. Una implementación como la siguiente tendría más sentido para mí:

type EmbedPackage interface {
    Bytes(filename string) []bytes
    BytesCompressed(filename string, config interface{}) []bytes // compressed in-binary as configured by some kind of config struct, memoizes decompression during runtime on first access
    Reader(filename string) io.Reader
    File(filename string) os.File // readonly and contains all metadata
    Dir(filepath string) []os.File 
    Glob(pattern string) []os.File // like filepath.Glob()

    // maybe? this could allow to load JSON, YAML, INI, TOML, etc files more easily
    // but would probably be too much for the std lib implementation
    Unmarshal(filename string, config interface{}, ptr interface{}) 
}

El uso de ese paquete en algún lugar de su código debería hacer que el compilador proporcione ese archivo al tiempo de ejecución incrustándolo automáticamente.

// embed a file that is compressed in-binary and automatically decompressed on first access
var LongText = embed.BytesCompressed("legal.html", embed.Config{ Compression: "gzip", CompressionLevel: "9" })

// loads a single file as reader for easy access
var FewLinesOfText = bufio.NewReader(embed.Reader("lines.txt"))
for _, line := range FewLinesOfText.ReadLines() { ... }

// embeds all files in the directory
var PdfFontFiles = embed.Dir("/fonts")

// unmarshals file into custom config
var PdfProcessingConfig MyPdfProcessingConfig
embed.Unmarshal("/pdf_conversion.json", embed.Config{ Encoding: "text/json" }, &PdfProcessingConfig)

También creo que los problemas de seguridad y reproducibilidad no deberían ser un problema si restringimos la importación a archivos en o posiblemente 1 nivel de directorio por debajo del directorio go.mod, que también se ha mencionado anteriormente en el hilo. Las rutas de inserción absolutas se resolverían en relación con ese nivel de directorio.

No poder acceder a los archivos durante el proceso de compilación generará un error de compilación.

También es posible crear un archivo zip detrás de un binario, de modo que pueda convertirse efectivamente en un binario autoextraíble. ¿Quizás esto sea útil en algunos casos de uso? Hice esto como un experimento aquí: https://github.com/sanderhahn/gozip

Go ya tiene „testdata“. Las pruebas unitarias usan IO regular para hacer lo que quieran. El alcance de la prueba significa que el contenido no se envía. Eso es todo lo que hay que saber, sin lujos, sin magia, sin lógica de contenedor comprimido, sin indirecciones configurables, sin META-INF. Hermoso, sencillo, elegante. ¿Por qué no tener una carpeta de "datos" para las dependencias del ámbito de ejecución en paquete?

Podemos escanear fácilmente proyectos de Go existentes en Github ea y crear una serie de proyectos que ya usan una carpeta de "datos" y, por lo tanto, requieren adaptación.

Otra cosa que no me queda clara. Para la discusión de un directorio static , no me queda 100% claro si estamos discutiendo un directorio static _source_ , o un directorio static donde los archivos estarán disponibles _at runtime_ ?

Y esta distinción es particularmente importante porque se relaciona con el proceso de desarrollo y el código de depuración que está en desarrollo.

@mikeschinkel , a partir de la publicación original, queda bastante claro que la inserción se realizaría en el momento de la compilación:

hacer que go install / go build hacer la incrustación automáticamente

La publicación original, y algunos de los comentarios anteriores, también discuten tener un modo "dev" para cargar archivos en tiempo de ejecución.

@mvdan Gracias por la respuesta. Entonces, ¿está pensando que esto significa que el directorio /static/ sería relativo a la raíz del repositorio de la aplicación, el repositorio del paquete y / o posiblemente ambos?

¿Y esa ubicación de los archivos en tiempo de ejecución dependería totalmente de dónde el desarrollador quisiera colocarlos?

Si todo eso es cierto, y parece lógico, sería útil que los programas compilados con información de depuración pudieran opcionalmente cargar archivos desde su ubicación de origen para facilitar la depuración sin mucha lógica y código adicionales y no estandarizados.

Un par de personas antes mencionadas proxies de módulo. Creo que es una gran prueba de fuego para un buen diseño de esta función.

Hoy en día, parece posible, sin ejecutar código de usuario, implementar un proxy de módulo funcional que elimine los archivos que no se utilizan en la compilación. Algunos de los diseños anteriores significarían que los proxies de módulo deben ejecutar el código de usuario para averiguar qué archivos estáticos también se incluirán.

Las personas también mencionaron go.mod como opción de suscripción.

Idea: ¿especificación en el archivo go.mod? Facilita el análisis de otras herramientas.

module github.com/foo/bar

data internal/static ./static/*.tmpl.html

Esto crearía un paquete en tiempo de compilación con los datos del archivo. La sintaxis glob puede ser buena aquí, pero tal vez simplificar y solo incrustar directorios sea lo suficientemente bueno. (Aparte: +1 para la sintaxis global ** .)

import "github.com/foo/bar/internal/static"

f, err := static.Open("static/templates/foo.tmpl")

Algo como StripPrefix podría ser bueno aquí, pero no es necesario. Fácil de crear un paquete contenedor que utiliza las rutas de archivo que desee.

Podría simplificarse aún más:

module github.com/foo/bar

data ./static/*.tmpl.html
import "runtime/moddata"

moddata.Open("static/foo.tmpl")

Pero es un poco intuitivo que moddata tenga un comportamiento diferente dependiendo del paquete / módulo de llamada. Haría más difícil escribir ayudantes (por ejemplo, convertidor http.Filesystem)

Hoy en día, parece posible, sin ejecutar código de usuario, implementar un proxy de módulo funcional que elimine los archivos que no se utilizan en la compilación. Algunos de los diseños anteriores significarían que los proxies de módulo deben ejecutar el código de usuario para averiguar qué archivos estáticos también se incluirán.

No creo que haya un cambio significativo aquí. En particular, el código C ya podría incluir cualquier archivo en el árbol, por lo que un proxy de módulo que quisiera hacer esto necesitaría analizar C. Parece que en ese momento, cualquier comentario mágico o API que introduzcamos será un pequeño paso.

Algunos de los diseños anteriores significarían que los proxies de módulo deben ejecutar el código de usuario para averiguar qué archivos estáticos también se incluirán.

Creo que está bastante claro que "la herramienta Go no debe ejecutar código de usuario durante la compilación" es una línea dibujada en la arena que no se cruzará aquí. Y si la herramienta go no puede ejecutar el código de usuario, entonces debe ser posible saber qué archivos incluir sin él.

He estado tratando de condensar mis diversos pensamientos sobre este caso de uso en algo convincente, y por lo tanto, un gran +1 a lo que sugirió @broady . Creo que en su mayor parte resume lo que he estado pensando. Sin embargo, creo que la palabra clave debería ser el verbo embed lugar del sustantivo data .

  1. Los archivos incrustados se sienten como algo que debería importarse en lugar de tener un comentario especial o un paquete mágico. Y en un proyecto de Go, el archivo go.mod es donde un desarrollador puede especificar los módulos / archivos que se necesitan, por lo que tiene sentido extenderlo para admitir la incrustación.

  2. Además, una colección de archivos incrustados me parece que serían más valiosos y reutilizables si se pudiera incluir un paquete en lugar de algo ad-hoc agregado a un proyecto de Go utilizando una sintaxis única. La idea aquí es que si las incrustaciones se implementaran como paquetes, las personas podrían desarrollarlas y compartirlas a través de Github y otras personas podrían usarlas en sus proyectos. Imagine paquetes mantenidos por la comunidad y de uso gratuito en GitHub que contienen:

    una. Archivos para países donde cada archivo contiene todos los códigos postales de ese país,
    B. Un archivo con todas las cadenas de agente de usuario conocidas para identificar navegadores,
    C. Imágenes de la bandera de cada país del mundo,
    D. Información de ayuda detallada que describe los errores que ocurren comúnmente en un programa Go,
    mi. etcétera...

  3. Un nuevo esquema de URL como goembed:// , o tal vez uno existente , que se puede usar para abrir y leer archivos del paquete, lo que permite aprovechar _ (¿todas?) _ API de manipulación de archivos existentes en lugar de crear nuevas ones, algo como lo siguiente que sería relativo al incrustado contenido en el paquete actual:

    data, err := ioutil.ReadFile("goembed://postal-codes.txt")    
    if (err != nil) {
      fmt.Println(err)
    }
    

Con los conceptos anteriores, nada se siente como _ "magia" _; todo sería manejado con elegancia por un mecanismo que parece que fue diseñado para un propósito. Se necesitaría muy poca extensión; un nuevo verbo en go.mod y un nuevo esquema de URL que Go reconocería internamente. Todo lo demás se proporcionaría tal cual desde Go.

Qué hago ahora

Utilizo code.soquee.net/pkgzip para esto en este momento (es una bifurcación de statik que cambia la API para evitar el estado global y los efectos secundarios de importación). Mi flujo de trabajo normal (al menos en una aplicación web) es incrustar activos agrupados en un archivo ZIP y luego servirlos usando golang.org/x/tools/godoc/vfs/zipfs y golang.org/x/tools/godoc/vfs/httpfs .

ir: enfoque de incrustación

Hay dos cosas que probablemente me impedirían adoptar el enfoque go:embed :

  1. El código generado no aparecerá en la documentación
  2. Los activos pueden estar dispersos por toda la base del código (esto es cierto para el uso de herramientas externas y go:generate también, por lo que generalmente prefiero usar un archivo MAKE para generar varios conjuntos de activos antes de la construcción, luego puede verlos todos en el archivo MAKE)

También hay un problema que no incluyo anteriormente porque podría ser una característica para algunos que tener activos como parte de un paquete (a diferencia de todo el módulo) significa que toda la complejidad de las etiquetas de compilación, los paquetes de prueba, etc. .aplicar a ellos, necesitamos una forma de especificar si son públicos o privados para ese paquete, etc. Esto parece una gran complejidad de compilación adicional.

Lo que me gusta de esto es que se pueden escribir bibliotecas que faciliten la importación de activos. P.ej. una biblioteca con un solo archivo Go que solo incrusta una fuente, o algunos íconos podrían publicarse y podría importarlos como cualquier otro paquete Go. En el futuro, podría obtener una fuente de icono simplemente importándola:

import "forkaweso.me/forkawesome/v2"

enfoque de paquete incrustado

Si bien me gusta la idea de que todo esto sea un código Go normal y explícito, odio la idea de que este sea otro paquete mágico que no se pueda implementar fuera de la biblioteca estándar.

¿Se definiría un paquete de este tipo como parte de la especificación del idioma? Si no, es otro lugar donde el código de Go se rompería entre diferentes implementaciones, lo que también se siente mal. Probablemente seguiría usando una herramienta externa para evitar esta rotura.

Además, como han mencionado otros, el hecho de que esto se haga en el momento de la compilación significa que este paquete solo puede tomar cadenas literales o constantes como argumentos. Actualmente no hay forma de representar esto en el sistema de tipos, y sospecho que será un punto de confusión. Esto podría resolverse introduciendo algo así como funciones constantes, pero ahora estamos hablando de cambios importantes en el lenguaje, lo que lo convierte en algo inútil. De lo contrario, no veo una buena manera de solucionar este problema.

Híbrido

Me gusta la idea de un enfoque híbrido. En lugar de reutilizar los comentarios (que terminan esparcidos por todas partes y, en una nota personal, simplemente se sienten asquerosos), me gustaría ver todos los activos en un solo lugar, probablemente el archivo go.mod como otros ha dicho:

module forkaweso.me/forkawesome/v2

go 1.15

embed (
    fonts/forkawesome-webfont.ttf
    fonts/forkawesome-webfont.woff2
)

Esto significa que los activos no se pueden incluir ni excluir mediante etiquetas de compilación arbitrarias o en paquetes arbitrarios (por ejemplo, el paquete _testing) sin crear un módulo separado. Creo que esta reducción en la complejidad puede ser deseable (no hay una etiqueta de compilación oculta en una biblioteca que está tratando de importar y no puede averiguar por qué no tiene el activo correcto porque la importación de la biblioteca debería haberlo incrustado) , pero YMMV. Si esto es deseable, los comentarios similares a pragma podrían usarse todavía, excepto que no generan código y en su lugar usan el mismo enfoque que estoy a punto de describir para la versión go.mod .

A diferencia de la propuesta original, esto no generaría ningún código. En cambio, la funcionalidad para, por ejemplo. leer la sección de datos del archivo ELF (o sin embargo, esto termina siendo almacenado en cualquier sistema operativo que esté usando) se agregaría cuando corresponda (por ejemplo, os o debug/elf , etc.) y luego, opcionalmente, se crearía un nuevo paquete que se comporta exactamente como el paquete descrito en el OP, excepto que en lugar de ser mágico y hacer la incrustación en sí, simplemente lee los archivos incrustados (lo que significa que podría implementarse fuera de la biblioteca estándar Si es deseado).

Esto soluciona problemas como tener que restringir el paquete mágico para permitir solo literales de cadena como argumentos, pero significa que es más difícil verificar si los activos incrustados se usan realmente en algún lugar o terminan siendo un peso muerto. También evita nuevas dependencias entre los paquetes de bibliotecas estándar, porque el único paquete que necesita importar algo adicional es un paquete nuevo.

var IconFont = embed.Dir("forkaweso.me/forkawesome/v2/fonts/")
var Logo = embed.File("images/logo.jpg")

Como se vio anteriormente, poner los recursos en el módulo aún podría llevarlos a ese módulo en particular si lo desea. La API real y la forma en que selecciona un activo pueden necesitar algo de trabajo.

Otra idea más: en lugar de agregar un nuevo tipo de verbo embed en go.mod , podríamos introducir un nuevo tipo de paquete, un paquete de datos, que se importa y se usa en go.mod en el manera usual. Aquí hay un boceto de hombre de paja.

Si un paquete contiene exactamente un archivo .go , static.go , y ese archivo contiene solo comentarios y una cláusula de paquete, entonces un paquete es un paquete de datos. Cuando se importa, cmd / go completa el paquete con funciones exportadas que brindan acceso a los archivos que contiene, que están incrustados en el binario resultante.

Si es un paquete real, eso significaría que se aplicarían las reglas internal y podemos tener controles de acceso sin agregar nada a la API.

Entonces, ¿qué pasa con la inclusión automática de todos los archivos y subcarpetas que no son .go (siguiendo las reglas sin código real) en el directorio?

Si un paquete contiene exactamente un archivo .go , static.go , y ese archivo contiene solo comentarios y una cláusula de paquete, entonces un paquete es un paquete de datos.

¿Se realizaría esta comprobación antes de la aplicación de etiquetas de compilación? Si es así, ese parece ser otro caso especial, que tal vez quiera evitarse. De lo contrario, es muy posible que un paquete pueda verse como un paquete Go estándar para algunas etiquetas de compilación y como un paquete de datos para otras. Eso parece extraño, pero ¿tal vez sea deseable?

@flimzy
Eso le permitiría a uno usar archivos incrustados con una etiqueta y definir los mismos fns / vars que el paquete generado y entregar los archivos de otra manera (¿tal vez de forma remota?) Con otra etiqueta.

Sería bueno si hubiera una bandera de compilación para generar las funciones de envoltura, por lo que solo tiene que completar los espacios en blanco.

@josharian

Si un paquete contiene exactamente un archivo .go , static.go , y ese archivo contiene solo comentarios y una cláusula de paquete, entonces un paquete es un paquete de datos.

Puedo imaginar que los paquetes de "datos" tengan su propia funcionalidad específica de dominio, como una búsqueda de código postal. El enfoque que acaba de proponer no permitiría nada más que los datos sin procesar y, por lo tanto, anularía los beneficios de poder empaquetar la lógica con los datos.

Puedo imaginar que los paquetes de "datos" tengan su propia funcionalidad específica de dominio, como una búsqueda de código postal.

Puede exponer la funcionalidad en my.pkg / postalcode y poner datos en my.pkg / postalcode / data (o my.pkg / postalcode / internal / data).

Veo el atractivo de hacerlo de la manera que sugieres, pero plantea un montón de preguntas: ¿Cómo funciona la compatibilidad con versiones anteriores? ¿Cómo se marca un paquete de datos como tal? ¿Qué se hace si el paquete tiene funciones que entrarían en conflicto con las que agregaría cmd / go? (No digo que no tengan respuestas, solo que es más sencillo no tener que responderlas).

@josharian , considere el comentario de verificación de tipo anterior (https://github.com/golang/go/issues/35950#issuecomment-561443566).

@bradfitz sí, esto sería un cambio de idioma y requeriría soporte para go / types.

En realidad, hay una manera de hacer esto sin que sea un cambio de idioma: requiere que static.go contenga funciones sin cuerpo que coincidan exactamente con lo que cmd / go completaría.

requiere static.go para contener funciones sin cuerpo que coincidan exactamente con lo que cmd / go completaría.

Si genera funciones por archivo en lugar de capturar todos los embed.File() , eso permitiría controles fáciles de exportación por activo.

Entonces, las cosas generadas se verían así:

EmbededFoo() embed.Asset {...}
embededBar() embed.Asset {...}

Una publicación de blog que escribí sobre archivos estáticos hace 4 meses. Vea la última oración en las conclusiones :-)

@josharian

Puede exponer la funcionalidad en my.pkg / postalcode y poner datos en my.pkg / postalcode / data (o my.pkg / postalcode / internal / data).

Eso, aunque poco elegante, podría abordar mis preocupaciones.

¿Cómo funciona la compatibilidad con versiones anteriores?

No veo cómo se aplican aquí las preocupaciones sobre el BC. ¿Puedes elaborar?

¿Cómo se marca un paquete de datos como tal?

Con la instrucción embed en la instrucción go.mod ?

Quizás no sigo lo que me pides.

Pero le daré la vuelta; ¿Cómo se marca un paquete con solo datos como tal?

¿Qué se hace si el paquete tiene funciones que entrarían en conflicto con las que agregaría cmd / go?

  1. Usando el enfoque propuesto, no creo que sea necesario que cmd / go agregue funciones.

  2. Incluso si cmd / go necesita agregar funciones, imagino que el comportamiento de los conflictos en un paquete _existente_ sería _undefined_.

    La propuesta asume que el desarrollador sigue el principio de responsabilidad única y, por lo tanto, solo debe construir un paquete con datos para ser un paquete centrado en datos y no agregar datos a un paquete centrado en la lógica existente.

    Por supuesto, un desarrollador _podría_ agregar a un paquete existente, en cuyo caso el comportamiento sería indefinido. IOW, si un desarrollador ignora el idioma, estaría en un territorio inexplorado.

No digo que no tengan respuestas, solo que es más sencillo no tener que responderlas.

Excepto que creo que las respuestas son simples. Al menos para los que has posado hasta ahora.

Creo que cualquier solución que agregue símbolos o valores para símbolos debe tener un alcance de paquete, no un alcance de módulo. Porque la unidad de compilación de Go es un paquete, no un módulo.

Así que esto omite cualquier uso de go.mod para especificar la lista de archivos a importar.

@dolmen si el resultado final está en su propio paquete, entonces el alcance en sí sería un módulo.

@urandom No, el alcance es el paquete (s) que importa ese paquete generado. Pero de todos modos, no creo que un paquete completo generado esté en el alcance de esta propuesta.

@urandom No, el alcance es el paquete (s) que importa ese paquete generado. Pero de todos modos, no creo que un paquete completo generado esté en el alcance de esta propuesta.

Independientemente de cómo se implemente esta propuesta, dado que varios paquetes de módulos utilizarán el resultado final, tiene sentido que la definición de lo que se incrusta se especifique a nivel de módulo. También existe un precedente de esto, en el ecosistema Java, donde los archivos incrustados tienen un alcance de módulo y se agregan desde un directorio mágico.

además, go.mod presenta la forma más limpia de agregar dicha característica (sin comentarios mágicos o directorios mágicos) sin romper los programas existentes.

Aquí hay algo que aún no se ha mencionado: la API para las herramientas de procesamiento de código fuente Go (compilador, analizadores estáticos) es tan importante como la API en tiempo de ejecución. Este tipo de API es un valor fundamental de Go que ayuda a hacer crecer el ecosistema (como go/ast / go/format y go mod edit ).

Esta API podría ser utilizada por herramientas de preprocesador (en go:generate pasos en particular) para obtener la lista de archivos que se incrustarán o podría usarse para generar la referencia.

En el caso de un paquete especial, no veo nada que cambiar en go.mod parsing ( go mod tools) o go/ast parser.

@dolmen

_ "Creo que cualquier solución que agregue símbolos o valores para símbolos debe tener un alcance de paquete, no de módulo. Debido a que la unidad de compilación para Go es paquete, no módulo. Por lo tanto, esto omite cualquier uso de go.mod para especificar la lista de archivos para importar. "_

¿Qué es el módulo? Los módulos son _ " una colección de paquetes Go relacionados que se versionan juntos como una sola unidad ". _ Por lo tanto, un módulo puede consistir en un solo paquete, y un solo paquete puede ser la totalidad de un módulo.

Como tal, go.mod es el lugar correcto para especificar la lista de archivos para importar asumiendo que el equipo de Go adopta go.mod en lugar de comentarios especiales y paquetes mágicos. Eso es a menos que y hasta que el equipo de Go decida agregar un archivo go.pkg .

Además, si el equipo de Go aceptara go.mod como el lugar para especificar archivos incrustados, entonces cualquiera que desee incrustar archivos debe proporcionar un go.mod con las instrucciones embed y el paquete que está representado por el directorio en el que reside el archivo go.mod sería el paquete que contiene los archivos incrustados.

Pero si eso no es lo que quiere el desarrollador, debe crear otro archivo go.mod y ponerlo en el directorio del paquete que desea que contenga sus archivos incrustados.

¿Existe un escenario legítimo que imagina en el que estas restricciones no serían viables?

@mikeschinkel , un módulo es una colección de paquetes _relacionados_. Sin embargo, es posible (¡y razonable!) Usar un paquete de un módulo sin extraer las dependencias transitivas (¡y datos!) De otros paquetes dentro de ese módulo.

Los archivos de datos generalmente son dependencias por paquete, no por módulo, por lo que la información sobre cómo ubicar esas dependencias debe colocarse con el paquete, no almacenada como metadatos separados a nivel de módulo.

@bcmills

Parece que uno puede reemplazar 'Archivos de datos' en su mensaje con 'módulos' y seguirá siendo cierto.
Es bastante común tener módulos específicos como dependencias para paquetes específicos propios.
Sin embargo, los ponemos a todos al alcance de la mano.

@urandom , no todos los paquetes de los módulos indicados en el archivo go.mod están vinculados al binario final. (Poner una dependencia en el archivo go.mod es _no_ equivalente a vincular esa dependencia en el programa).

Meta-punto

Está claro que esto es algo que le importa a mucha gente, y el comentario original de Brad en la parte superior fue menos una propuesta terminada que un boceto inicial / punto de partida / llamado a la acción. Creo que tendría sentido en este punto terminar esta discusión específica, hacer que Brad y tal vez un par de personas colaboren en un documento de diseño detallado, y luego comenzar un nuevo número para una discusión de ese documento específico (aún por escribir) . Parece que eso ayudaría a enfocar lo que se ha convertido en una conversación en expansión.

¿Pensamientos?

No estoy seguro de estar de acuerdo con cerrar este, pero podemos Propuesta-Retenerlo hasta que haya un documento de diseño y bloquear los comentarios por un momento. (Casi todos los comentarios son redundantes en este punto, ya que hay demasiados comentarios para que la gente los lea para ver si su comentario es redundante ...)

Tal vez cuando haya un documento de diseño, este pueda cerrarse.

O cierre este y vuelva a abrir # 3035 (con los comentarios congelados) para que al menos un problema abierto lo rastree.

Lamento hacer esto justo después de hablar sobre el cierre, pero la discusión cercana saltó justo después del comentario de @bcmills y antes de que pudiera aclarar.

"_Sin embargo, es posible (¡y razonable!) Usar un paquete de un módulo sin extraer las dependencias transitivas (¡y datos!) De otros paquetes dentro de ese módulo". _

Sí, claramente es _posible_. Pero al igual que cualquier mejor práctica, una mejor práctica para los paquetes de datos podría ser crear un solo paquete para un módulo, lo que resuelve su inquietud. Si eso significa un go.mod en la raíz y otro go.mod en el subdirectorio que contiene el paquete de datos, que así sea.

Supongo que estoy defendiendo que no se haga perfecto ser el enemigo de lo bueno aquí, donde lo bueno aquí se identifica como go.mod siendo un lugar perfecto para especificar archivos de inserción dado que, por su naturaleza, son una lista de los componentes de un módulo.

Lo sentimos, pero los paquetes son el concepto fundamental en Go, no los módulos.
Los módulos son simplemente grupos de paquetes que se versionan como una unidad.
Los módulos no aportan semántica adicional a la de los paquetes individuales.
Eso es parte de su simplicidad.
Todo lo que hagamos aquí debería estar vinculado a paquetes, no a módulos.

Donde sea que vaya y como sea que se haga, debería haber una forma de obtener una lista de todos los activos que se incrustarán usando la lista go (no solo los patrones utilizados).

Dejar en espera hasta que Brad y otros elaboren un documento de diseño formal.

Bloquear ciertos archivos no debería ser demasiado difícil, especialmente si usa un directorio static o embed . Los enlaces simbólicos pueden complicar eso un poco, pero puede evitar que incruste algo fuera del módulo actual o, si está en GOPATH, fuera del paquete que contiene el directorio.

No soy particularmente fanático de un comentario que se compila en código, pero también encuentro que el pseudopaquete que afecta la compilación también es un poco extraño. Si no se usa el enfoque de directorio, tal vez tenga un poco más de sentido tener una especie de declaración de nivel superior embed incorporada en el lenguaje. Funcionaría de manera similar a import , pero solo admitiría rutas locales y requeriría un nombre para asignarlo. Por ejemplo,

embed ui "./ui/build"

func main() {
  file, err := ui.Open("version.txt")
  if err != nil {
    panic(err)
  }
  version, err = ioutil.ReadAll(file)
  if err != nil {
    panic(err)
  }
  file.Close()

  log.Printf("UI Version: %s\n", bytes.TrimSpace(version))
  http.ListenAndServe(":8080", http.EmbeddedDir(ui))
}

Editar: Me ganaste, @jayconrod.

Esto es limpio y legible, sin embargo, no estoy seguro de que el equipo de Go quiera introducir una nueva palabra clave.

La idea, según recuerdo, era simplemente tener un nombre de directorio especial como "estático" que contenga los datos estáticos y hacerlos disponibles automáticamente a través de una API, sin necesidad de anotaciones.

Usar static como un nombre de directorio especial es un poco confuso y prefiero ir con assets .
Otra idea que no vi en el hilo es permitir la importación de assets como paquete, por ejemplo, import "example.com/internal/assets" . La API expuesta todavía necesita un diseño, pero al menos se ve más limpia que los comentarios especiales o los nuevos paquetes de estilo runtime/files .

Otra idea que no vi en el hilo es permitir la importación de activos como un paquete.

Esto se propuso aquí: https://github.com/golang/go/issues/35950#issuecomment -562966654

Una complicación es que para permitir que se produzca la verificación de tipo, es necesario que se trate de un cambio de idioma o proporcionar funciones sin cuerpo para que las complete

Esa es una idea similar, pero el diseño static.go permite convertir una ruta de importación arbitraria en un paquete de datos, mientras que el directorio assets funciona más como testdata , internal o vendor en términos de ser "especial". Uno de los posibles requisitos que podría tener assets es no contener paquetes Go (o solo permitir documentos), es decir, para compatibilidad implícita con versiones anteriores.

Esto también podría combinarse con la API runtime/files -thingy para obtener los archivos. Es decir, usar import s sin procesar para incrustar árboles de directorios con archivos y luego usar algún paquete de tiempo de ejecución para acceder a ellos. Podría ser incluso os.Open , pero es poco probable que se acepte.

El shurcooL/vfsgen juntos shurcooL/httpgzip tiene una característica interesante en la que el contenido se puede servir sin descompresión.

p.ej

    rsp.Header().Set("Content-Type", "image/png")
    httpgzip.ServeContent(rsp, req, "", time.Time{}, file)

Se propone una característica similar para C ++: std::embed :

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1040r0.html
https://mobile.twitter.com/Cor3ntin/status/1208389050698215427

Puede ser útil como inspiración para un diseño y para recopilar posibles casos de uso.

Llego un poco tarde a la fiesta, pero tenía una idea. En lugar de comentarios especiales, un directorio especial fijo (estático), o el enfoque aparentemente no-no de extender go.mod: ¿Qué tal un nuevo archivo de manifiesto por paquete: go.res?

  • Contiene una lista de archivos. Sin rutas ni globs, solo nombres en el directorio del paquete actual. Generelo a partir de un glob antes de comprometerse si es necesario.

    • __Editar__ es posible que necesite una sola línea package mypackagename en la parte superior, como lo haría un archivo go. Alternativamente, puede incluir el nombre del paquete en el nombre del archivo (por ejemplo, mypackagename.go.res). Personalmente, me gusta más la línea de encabezado package .

  • Un nuevo paquete básico llamado "recurso" o tal vez "io / recurso". Tiene al menos una función: func Read(name string) (io.Reader, bool) para leer recursos incrustados en el paquete actual.

    • __Editar__ No estoy seguro de que los paquetes principales funcionen de esta manera. Puede que tenga que ser una función privada de paquete generado (por ejemplo, func readresource(name string) (io.Reader, bool) )

  • Si desea recursos en un subdirectorio, convierta el subdirectorio en un paquete agregando un archivo go.res y al menos un archivo .go . El archivo go exporta su propia API pública para acceder a los recursos del paquete del subdirectorio. El archivo go y la API exportada son necesarios, porque los recursos de otros paquetes no se exportan automáticamente (por diseño). También puede personalizar cómo se exportan de esta manera.

    • __Editar__ alternativamente, si necesita una estructura de directorio y / o compresión, use un recurso tar. Esto permite cosas como paquetes de paquetes web, que ya requieren compilación (y podrían beneficiarse de la precompresión). Llevarlos un paso más allá hacia un alquitrán es simple.

  • __Editar__ ¿Necesita un manifiesto? Solo incluye el archivo go.res como recurso. Ni siquiera necesitamos crear una función de lista de recursos.

Extremadamente simple. Una nueva función. Un nuevo archivo. Sin caminos. Sin compresión. Sin nueva sintaxis. Sin magia. Extensible. Acceso de solo lectura a través del lector (pero abierto a patrones de acceso alternativos en el futuro). Posibilidad casi nula de romper paquetes existentes. Sigue siendo el paquete la construcción principal en marcha.

__Editar__ Después de una búsqueda en github language:go filename:go.res extension:res , parece que go.res sería un nombre de archivo bastante seguro de usar. No hay coincidencias en los repositorios go, y solo unas pocas en repositorios no go.

Me gusta la idea de @ chris.ackermanm. Pero preferiría una combinación:

Un archivo go.res que especifica el espacio de nombres dentro de un directorio.

Esto permite

  • múltiples inclusiones siempre que el espacio de nombres sea diferente
  • no conocer los archivos antes y tener que generar una lista

El último debe abordar la salida del paquete web y los me gusta que pueden cambiar el diseño debido a actualizaciones, diferentes opciones, lo que se te ocurra.

Con respecto a la compresión: creo que es más una característica en términos de no hacer explotar los tamaños binarios y debería ser transparente para el código de uso.

Más tarde, podría permitir reescrituras como

filename => stored-as.png

Solo mis 2 ¢

@ sascha-andres Parece que ultra simplicidad y cero magia es el tono de este hilo. Vea las modificaciones que hice a mi comentario según sus sugerencias.

No me gusta el mapeo. No hay necesidad. Eso es posible al exponer su propia función de lectura desde un paquete separado de todos modos, y ahora necesitamos una nueva sintaxis de archivo, o algo más complejo que el archivo por línea.

Hola

¡Esta propuesta es genial!

Y tengo mi enfoque para los activos de emebed. no es necesario introducir ninguna herramienta que no sea GNU bintools. Está un poco sucio, pero me funciona bien por ahora. Solo quiero compartirlo y ver si ayuda.

mi enfoque es simplemente incrustar mis activos (comprimidos con tar & gz) en una sección elf / pe32 con objcopy, y leerlo a través del paquete debug / elf y debug / pe32 junto con zip cuando sea necesario. todo lo que necesito recordar es no tocar ninguna sección existente. todos los activos son inmutables y luego el código lee el contenido y lo procesa en la memoria.

Soy bastante inexperto en diseño de lenguajes o diseño de compiladores. así que simplemente usaría el enfoque descrito anteriormente y usaría .goassets o algo así como el nombre de la sección. y hacer que la compresión sea opcional.

mi enfoque es simplemente incrustar mis activos (comprimidos con tar & gz) en una sección elf / pe32 con objcopy, y leerlo a través del paquete debug / elf y debug / pe32 junto con zip cuando sea necesario. todo lo que necesito recordar es no tocar ninguna sección existente. todos los activos son inmutables y luego el código lee el contenido y lo procesa en la memoria.

Parece que funciona en elf / pe32 pero ¿qué pasa con mach-o / plan9 ?

Otro problema es que se basa en abrir un identificador de archivo en el ejecutable, si el ejecutable se ha sobrescrito / actualizado / eliminado, esto devolverá datos diferentes, no estoy seguro de si es un problema legítimo o una característica inesperada.

Yo mismo lo intenté un poco (usando debug / macho ), pero no veo una manera de hacer que esta plataforma funcione, estoy construyendo en macOS y las binutils de GNU instaladas parecen corromper los mach-o-x86-64 file (eso podría ser mi falta de comprensión de la estructura mach-o y demasiado tiempo desde que incluso miré objcopy ).

Otro problema es que se basa en abrir un identificador de archivo en el ejecutable.

Estoy bastante seguro de que el cargador de programas cargará (o podría) cargar la sección de recursos en la memoria, por lo que no es necesario utilizar paquetes de depuración. Aunque acceder a los datos requeriría muchos más retoques con los archivos objeto de lo que vale.

¿Por qué no seguir lo que funciona? Por ejemplo, cómo lo hace Java . Requeriría que las cosas fueran un gran go-ish, pero algo en las líneas:

  • cree un archivo go.res o modifique go.mod para que apunte al directorio donde se encuentran los recursos
  • todos los archivos de este directorio se incluyen automáticamente, sin excepciones por parte del compilador en el ejecutable final
  • El lenguaje proporciona una API similar a una ruta para acceder a estos recursos.

La compresión, etc.deben estar fuera del alcance de este paquete de recursos y están hasta cualquier // go:generate scripts si es necesario.

¿Alguien ha mirado markbates / pkger ? Es una solución bastante simple de usar go.mod como directorio de trabajo actual. Suponiendo que se va a incrustar un index.html , abrirlo sería pkger.Open("/index.html") . Creo que esta es una mejor idea que codificar un directorio static/ en el proyecto.

También vale la pena mencionar que, por lo que pude ver, Go no tiene requisitos de estructura significativos para un proyecto. go.mod es solo un archivo y no mucha gente usa vendor/ . Personalmente, no creo que un directorio static/ sea ​​bueno.

Como ya tenemos una forma de inyectar (aunque limitada) datos en una compilación a través de la bandera de enlace ldflags -X importpath.name=value , ¿podría ajustarse la ruta del código para aceptar -X importpath.name=@filename para inyectar datos arbitrarios externos?

Me doy cuenta de que esto no cubre todos los objetivos declarados del problema original, pero como una extensión de la funcionalidad existente -X , ¿parece un paso adelante razonable?

(Y si eso funciona, entonces extender la sintaxis go.mod como una forma más ordenada de especificar los valores ldflags -X es el siguiente paso razonable?)

Es una idea muy interesante, pero me preocupan las implicaciones de seguridad.

Es bastante común hacer -X 'pkg.BuildVersion=$(git rev-parse HEAD)' , pero no querríamos dejar ir. Mod ejecutar comandos arbitrarios, ¿verdad? (Supongo que go generate lo hace, pero eso no es algo que normalmente se ejecuta para los paquetes OSS descargados). Si go.mod no puede manejar eso, termina perdiendo un caso de uso importante, por lo que ldflags aún sería muy común.

Luego está el otro problema de asegurarse de que @filename no sea un enlace simbólico a / etc / passwd o lo que sea.

El uso del vinculador excluye la compatibilidad con WASM y posiblemente otros destinos que no utilizan un vinculador.

Basándonos en la discusión aquí, @bradfitz y yo

Video: https://golang.org/s/draft-embed-video
Diseño: https://golang.org/s/draft-embed-design
Preguntas y respuestas: https://golang.org/s/draft-embed-reddit
Código: https://golang.org/s/draft-embed-code

@rsc En mi opinión, la propuesta de go: embed es inferior a proporcionar ejecución de código Go _universal_ sandboxed en tiempo de compilación, lo que incluiría la lectura de archivos y la transformación de datos leídos en un _formato óptimo_ más adecuado para el consumo en tiempo de ejecución.

@atomsymbol Eso suena como algo muuuucho fuera del alcance de esta edición.

@atomsymbol Eso suena como algo muuuucho fuera del alcance de esta edición.

Soy consciente de eso.

Leí la propuesta y escaneé el código, pero no pude encontrar una respuesta a esto: ¿Este esquema de incrustación contendrá información sobre el archivo en el disco (~ os.Stat)? ¿O se restablecerán estas marcas de tiempo para el tiempo de compilación? De cualquier manera, estas son piezas de información útiles que se utilizan en varios lugares, por ejemplo, podemos enviar un 304 para activos sin cambios basados ​​en esto.

¡Gracias!

Editar: lo encontré en el hilo de reddit.

El tiempo de modificación para todos los archivos incrustados es el tiempo cero, exactamente por las preocupaciones de reproducibilidad que enumeró. (Los módulos ni siquiera registran los tiempos de modificación, nuevamente por la misma razón).

https://old.reddit.com/r/golang/comments/hv96ny/qa_goembed_draft_design/fytj7my/

De cualquier manera, estas son piezas de información útiles que se utilizan en varios lugares, por ejemplo, podemos enviar un 304 para activos sin cambios basados ​​en esto.

Un encabezado ETag basado en el hash de los datos del archivo resolvería ese problema sin tener que saber nada sobre las fechas. Pero eso tendría que ser conocido por http.HandlerFS o algo para poder funcionar y no desperdiciar recursos tendría que hacerse solo una vez por archivo.

Pero eso tendría que ser conocido por http.HandlerFS o algo para poder funcionar y no desperdiciar recursos tendría que hacerse solo una vez por archivo.

¿Cómo sabría http.HandlerFS que fs.FS era inmutable? ¿Debería haber una interfaz opcional IsImmutable() bool ?

¿Cómo sabría http.HandlerFS que fs.FS era inmutable? ¿Debería haber una interfaz opcional IsImmutable() bool ?

No quiero entrar en detalles de implementación porque no soy el diseñador de estas cosas, pero http.HandlerFS podría verificar si es un tipo embed.FS y actuar sobre eso como un caso especial, no creo que nadie quiera hacerlo expanda la API de FS ahora mismo. También podría haber un argumento de opción para HandlerFS específicamente para decirle que trate un sistema de archivos como inmutable. Además, si esto se hace al iniciar la aplicación y todos los ctime / mtime tienen un controlador de valor cero, FS podría usar esa información para "saber" que el archivo no ha cambiado, pero también hay sistemas de archivos que pueden no tener mtime o tenerlo deshabilitado. También podría haber problemas allí.

No estaba viendo los comentarios sobre este tema.

@atomsymbol ¡ bienvenido de nuevo! Es genial verte comentando aquí de nuevo.
Estoy de acuerdo en principio en que si tuviéramos sandboxing muchas cosas serían más fáciles.
Por otro lado, muchas cosas pueden ser más difíciles: es posible que las compilaciones nunca terminen.
En cualquier caso, definitivamente no tenemos ese tipo de espacio aislado en la actualidad. :-)

@kokes No estoy seguro de los detalles,
pero nos aseguraremos de servir un incrustado. Los archivos a través de HTTP obtienen ETags correctos de forma predeterminada.

Presenté el número 41191 para aceptar el borrador del diseño publicado en julio.
Voy a cerrar este tema como reemplazado por aquél.
Gracias por la gran discusión preliminar aquí.

¿Fue útil esta página
0 / 5 - 0 calificaciones