Go: proposta: cmd / go: suporte a incorporação de ativos estáticos (arquivos) em binários

Criado em 4 dez. 2019  ·  176Comentários  ·  Fonte: golang/go

Existem muitas ferramentas para incorporar arquivos de ativos estáticos em binários:

Na verdade, https://tech.townsourced.com/post/embedding-static-files-in-go/ lista mais:

Proposta

Acho que é hora de fazer isso bem uma vez e reduzir a duplicação, adicionando suporte oficial para incorporar recursos de arquivo na ferramenta cmd / go.

Problemas com a situação atual:

  • Existem muitas ferramentas
  • Usando uma solução baseada em go: generate incha o histórico do git com uma segunda cópia (um pouco maior) de cada arquivo.
  • Não usar go: generate significa não ser go install -able ou fazer as pessoas escreverem seus próprios Makefiles, etc.

Metas:

  • não faça check-in dos arquivos gerados
  • não gere arquivos * .go (pelo menos não na área de trabalho do usuário)
  • fazer go install / go build fazer a incorporação automaticamente
  • deixe o usuário escolher por arquivo / glob que tipo de acesso é necessário (por exemplo, [] byte, func() io.Reader , io.ReaderAt , etc)
  • Talvez armazenar ativos compactados no binário quando apropriado (por exemplo, se o usuário precisar apenas de io.Reader )? ( editar : mas provavelmente não; veja os comentários abaixo)
  • Sem execução de código em tempo de compilação ; essa é uma política Go de longa data. go build ou go install não pode executar código arbitrário, assim como go:generate não executa automaticamente no momento da instalação.

As duas principais abordagens de implementação são //go:embed Logo logo.jpg ou um pacote bem conhecido ( var Logo = embed.File("logo.jpg") ).

go: abordagem embed

Para uma abordagem go:embed , pode-se dizer que qualquer arquivo go/build -selecionado *.go pode conter algo como:

//go:embed Logo logo.jpg

Que, digamos, compila para:

func Logo() *io.SectionReader

(adicionando uma dependência ao pacote io )

Ou:

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

compilando para, digamos:

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

Obviamente, isso não está totalmente desenvolvido. Seria necessário haver algo para arquivos compactados também que rendesse apenas io.Reader .

abordagem de pacote embutido

A outra abordagem de alto nível é não ter uma sintaxe mágica de //go:embed e, em vez disso, apenas permitir que os usuários escrevam o código Go em algum novo pacote "embed" ou "golang.org/x/foo/embed" :

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

Em seguida, faça com que cmd / go reconheça as chamadas para embed.Foo ("foo / *. Js") etc e glob fazer o trabalho em cmd / go, em vez de no tempo de execução. Ou talvez certas tags ou sinalizadores de construção possam fazer com que ele volte a fazer coisas em tempo de execução. O Perkeep (link acima) tem esse modo, que é bom para acelerar o desenvolvimento incremental onde você não se preocupa em vincular um grande binário.

Preocupações

  • Escolha um estilo (// go: embed * vs a magic package).
  • Bloquear certos arquivos?

    • Provavelmente bloqueará a incorporação de ../../../../../../../../../../etc/shadow

    • Talvez bloqueie o alcance de .git também

Proposal Proposal-Hold

Comentários muito úteis

@robpike e eu conversamos sobre uma proposta para fazer isso anos atrás (antes que houvesse um processo de proposta) e nunca mais voltamos a fazer nada. Há anos me incomoda que nunca terminamos de fazer isso. A ideia, pelo que me lembro, era apenas ter um nome de diretório especial como "estático" contendo os dados estáticos e torná-los automaticamente disponíveis por meio de uma API, sem a necessidade de anotações.

Não estou convencido sobre a complexidade de um botão "comprimido versus não". Se fizermos isso, as pessoas vão querer que adicionemos controle sobre qual compressão, nível de compressão e assim por diante. Tudo o que precisamos adicionar é a capacidade de incorporar um arquivo de bytes simples. Se os usuários desejam armazenar dados compactados nesse arquivo, ótimo, os detalhes são com eles e não há necessidade de API por parte do Go.

Todos 176 comentários

Vale a pena considerar se embedglob deve suportar uma árvore de arquivos completa, talvez usando a sintaxe ** suportada por alguns shells Unix.

Algumas pessoas precisariam da capacidade de servir os ativos incorporados com HTTP usando o http.FileServer .

Eu pessoalmente uso mjibson / esc (que faz isso) ou, em alguns casos, minha própria implementação de incorporação de arquivos que renomeia arquivos para criar caminhos exclusivos e adiciona um mapa dos caminhos originais aos novos, por exemplo, "/js/bootstrap.min.js": "/js/bootstrap.min.827ccb0eea8a706c4c34a16891f84e7b.js" . Então você pode usar este mapa nos modelos como este: href="{{ static_path "/css/bootstrap.min.css" }}" .

Acho que uma consequência disso seria que não seria trivial descobrir quais arquivos são necessários para construir um programa.

A abordagem //go:embed introduz outro nível de complexidade. Você teria que analisar os comentários mágicos para até mesmo verificar o código. A abordagem do "pacote embutido" parece mais amigável para a análise estática.

(Apenas meditando em voz alta aqui.)

@opennota ,

precisaria da capacidade de servir os ativos incorporados com HTTP usando o http.FileServer .

Sim, o primeiro link acima é um pacote que escrevi ( em 2011, antes do Go 1 ) e ainda uso, e ele suporta o uso de http.FileServer: https://godoc.org/perkeep.org/pkg/fileembed#Files.Open

@cespare ,

A abordagem // go: embed também introduz outro nível de complexidade. Você teria que analisar os comentários mágicos para até mesmo verificar o código. A abordagem do "pacote embutido" parece mais amigável para a análise estática.

Sim, bom ponto. Esse é um argumento muito forte para usar um pacote. Também o torna mais legível e documentável, já que podemos documentar tudo com godoc regular, em vez de nos aprofundar nos documentos cmd / go.

@bradfitz - Deseja fechar https://github.com/golang/go/issues/3035 ?

@agnivade , obrigado por encontrar isso! Pensei ter me lembrado disso, mas não consegui encontrar. Vamos deixar em aberto por enquanto e ver o que os outros pensam.

Se escolhermos o pacote mágico, poderíamos usar o truque do tipo não exportado para garantir que os chamadores passem as constantes de tempo de compilação como argumentos: https://play.golang.org/p/RtHlKjhXcda.

(Esta é a estratégia referenciada aqui: https://groups.google.com/forum/#!topic/golang-nuts/RDA9Hag8RZw/discussion)

Uma preocupação que eu tenho é como isso lidaria com o indivíduo ou todos os ativos sendo muito grandes para caber na memória e se haveria talvez uma tag de construção ou opção de acesso por arquivo para escolher entre o tempo de acesso de preservação versus espaço de memória ou alguma implementação intermediária.

a maneira como resolvi esse problema (porque é claro que também tenho minha própria implementação :)) é fornecer uma implementação http.FileSystem que atende a todos os ativos incorporados. Dessa forma, você não deve depender de comentários mágicos para apaziguar o verificador de tipo, os ativos podem ser facilmente atendidos por http, uma implementação de fallback pode ser fornecida para fins de desenvolvimento (http.Dir) sem alterar o código e o final a implementação é bastante versátil, já que http.FileSystem cobre bastante, não apenas na leitura de arquivos, mas também na listagem de diretórios.

Ainda é possível usar comentários mágicos ou qualquer outra coisa para especificar o que precisa ser incorporado, embora seja provavelmente mais fácil especificar todos os globs por meio de um arquivo de texto simples.

@AlexRouSg Esta proposta seria apenas para arquivos que são apropriados para incluir diretamente no executável final. Não seria apropriado usar isso para arquivos muito grandes para caber na memória. Não há razão para complicar essa ferramenta para lidar com esse caso; para esse caso, apenas não use esta ferramenta.

@ianlancetaylor , acho que a distinção que @AlexRouSg estava fazendo era entre ter os arquivos fornecidos como []byte s globais (memória não paginável e potencialmente gravável) vs fornecer uma visualização somente leitura sob demanda de uma seção ELF que pode normalmente ao vivo no disco (no executável), como por meio de uma chamada Open que retorna um *io.SectionReader . (Não quero assar http.File ou http.FileSystem em cmd / go ou runtime ... net / http pode fornecer um adaptador.)

@bradfitz tanto http.File em si é uma interface sem dependências técnicas para o pacote http . Pode ser uma boa ideia para qualquer método Open fornecer uma implementação que esteja em conformidade com essa interface, porque os métodos Stat e Readdir são bastante úteis para tais ativos

@urandom , não foi possível implementar http.FileSystem, entretanto, sem se referir ao nome "http.File" (https://play.golang.org/p/-r3KjG1Gp-8).

@robpike e eu conversamos sobre uma proposta para fazer isso anos atrás (antes que houvesse um processo de proposta) e nunca mais voltamos a fazer nada. Há anos me incomoda que nunca terminamos de fazer isso. A ideia, pelo que me lembro, era apenas ter um nome de diretório especial como "estático" contendo os dados estáticos e torná-los automaticamente disponíveis por meio de uma API, sem a necessidade de anotações.

Não estou convencido sobre a complexidade de um botão "comprimido versus não". Se fizermos isso, as pessoas vão querer que adicionemos controle sobre qual compressão, nível de compressão e assim por diante. Tudo o que precisamos adicionar é a capacidade de incorporar um arquivo de bytes simples. Se os usuários desejam armazenar dados compactados nesse arquivo, ótimo, os detalhes são com eles e não há necessidade de API por parte do Go.

Algumas idéias:

  • Não deve ser possível incorporar nenhum arquivo fora do módulo que está fazendo a incorporação. Precisamos ter certeza de que os arquivos fazem parte dos arquivos zip do módulo quando os criamos, de modo que também significa que não há links simbólicos, conflitos de caso, etc. Não podemos alterar o algoritmo que produz arquivos zip sem quebrar as somas.
  • Eu acho que é mais simples restringir a incorporação para estar no mesmo diretório (se //go:embed comentários são usados) ou um subdiretório específico (se static é usado). Isso torna muito mais fácil entender a relação entre os pacotes e os arquivos incorporados.

De qualquer maneira, isso bloqueia a incorporação de /etc/shadow ou .git . Nenhum deles pode ser incluído em um zip de módulo.

Em geral, estou preocupado em expandir muito o escopo do comando go. No entanto, o fato de haver tantas soluções para esse problema significa que provavelmente deveria haver uma solução oficial.

Estou familiarizado com go_embed_data e go-bindata (dos quais existem vários garfos), e isso parece cobrir esses casos de uso. Existem problemas importantes que os outros resolvem e que isso não cobre?

Bloquear certos arquivos não deve ser muito difícil, especialmente se você usar um diretório static ou embed . Os links simbólicos podem complicar isso um pouco, mas você pode simplesmente evitar que ele incorpore qualquer coisa fora do módulo atual ou, se você estiver no GOPATH, fora do pacote que contém o diretório.

Não sou particularmente fã de um comentário que compila em código, mas também acho um pseudo-pacote que afeta a compilação um pouco estranho. Se a abordagem de diretório não for usada, talvez faça um pouco mais sentido ter algum tipo de declaração de nível superior embed realmente embutida na linguagem. Funcionaria de forma semelhante a import , mas só suportaria caminhos locais e exigiria um nome para ser atribuído. Por exemplo,

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))
}

Edit: Você chegou antes de mim, @jayconrod.

Para expandir em https://github.com/golang/go/issues/35950#issuecomment -561703346, há um quebra-cabeça sobre a API exposta. As maneiras óbvias de expor os dados são as interfaces []byte , string e Read -ish.

O caso típico é que você deseja que os dados incorporados sejam imutáveis. No entanto, todas as interfaces que expõem []byte (que inclui io.Reader , io.SectionReader , etc.) devem (1) fazer uma cópia, (2) permitir mutabilidade ou (3) ser imutável apesar de ser um []byte . Expor os dados como string s resolve isso, mas ao custo de uma API que muitas vezes vai acabar exigindo cópia de qualquer maneira, já que muitos códigos que consomem arquivos embutidos eventualmente requerem fatias de bytes de uma forma ou de outra.

Eu sugeriria a rota (3): seja imutável apesar de ser []byte . Você pode impor isso de forma barata usando um símbolo somente leitura para a matriz de apoio. Isso também permite que você exponha com segurança os mesmos dados que []byte e string ; as tentativas de transformar os dados falharão. O compilador não pode tirar vantagem da imutabilidade, mas isso não é uma perda muito grande. Isso é algo que o suporte do conjunto de ferramentas pode trazer para a mesa que (até onde eu sei) nenhum dos pacotes codegen existentes fazem.

(Um pacote codegen de terceiros poderia fazer isso gerando um arquivo de montagem genérico contendo DATA símbolos que são marcados como somente leitura e, em seguida, arquivos de montagem específicos de arco curto expondo esses símbolos na forma de string se []byte s. Escrevi CL 163747 especificamente com esse caso de uso em mente, mas nunca tive a oportunidade de integrá-lo a nenhum pacote codegen.)

Não tenho certeza do que você está falando em termos de imutabilidade. io.Reader já impõe imutabilidade. Esse é o ponto principal. Quando você chama Read(buf) , ele copia os dados para o buffer que _você_ forneceu. Mudar buf depois disso tem efeito zero nas partes internas de io.Reader .

Eu concordo com @DeedleFake. Não quero jogar com fundos mágicos de []byte array. Não há problema em copiar do binário para os buffers fornecidos pelo usuário.

Apenas mais uma questão aqui - eu tenho um projeto diferente que usa o código-fonte do DTrace (incorporado). Isso é sensível às diferenças entre \ n e \ r \ n. (Podemos discutir se isso é uma coisa idiota no DTrace ou não - isso não vem ao caso e é a situação hoje.)

É muito útil que strings com backticked tratem ambos como \ n independentemente de como eles aparecem no código-fonte, e eu confio nisso com um go-generate para incorporar o DTrace.

Portanto, se houver um arquivo incorporado adicionado ao comando go, sugiro gentilmente que as opções para alterar a manipulação de CR / CRLF podem ser muito úteis, especialmente para pessoas que podem estar desenvolvendo em sistemas diferentes onde as terminações de linha padrão podem ser um pegadinha.

Como acontece com a compactação, eu realmente gostaria de parar em "copiar os bytes do arquivo para o binário". Normalização CR / CRLF, normalização Unicode, gofmt'ing, tudo o que pertence a outro lugar. Verifique os arquivos que contêm os bytes exatos que você deseja. (Se seu controle de versão não pode deixá-los sozinhos, talvez verifique o conteúdo compactado com gzip e gunzip-los em tempo de execução.) Existem _muitos_ botões de munging de arquivo que podemos imaginar adicionar. Vamos parar em 0.

Pode ser tarde demais para introduzir um novo nome de diretório reservado, por mais que eu queira.
(Não era tarde demais em 2014, mas provavelmente é tarde agora.)
Portanto, algum tipo de comentário opt-in pode ser necessário.

Suponha que definamos um tipo runtime.Files. Então você pode imaginar escrevendo:

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

E então, em tempo de execução, você apenas chama files.Open para receber de volta interface { io.ReadSeeker; io.ReaderAt } com os dados. Observe que a var não foi exportada, portanto, um pacote não pode sair por aí vasculhando os arquivos embutidos de outro pacote.

Nomeia TBD, mas no que diz respeito ao mecanismo, parece que isso deve ser suficiente e não vejo como torná-lo mais simples. (Simplificações bem-vindas, é claro!)

Faça o que fizermos, deve ser possível apoiar com Bazel e Gazelle também. Isso significaria que o Gazelle reconhecesse o comentário e escrevesse uma regra do Bazel dizendo os globs, e então precisaríamos expor uma ferramenta (go tool embedgen ou qualquer outra) para gerar o arquivo extra para incluir na compilação (o comando go faça isso automaticamente e nunca mostre o arquivo extra). Isso parece bastante direto.

Se várias munging não resolverem, isso é um argumento contra o uso desse novo recurso. Não é um obstáculo para mim - posso usar o go generate como venho fazendo, mas significa que não posso me beneficiar do novo recurso.

Com relação ao munging em geral - posso imaginar uma solução onde alguém fornece uma implementação de uma interface (algo como um Reader () de um lado, e algo para receber o arquivo do outro - talvez instanciado com um io.Reader do próprio arquivo) - que o go cmd criaria e executaria para pré-filtrar o arquivo antes de incorporar. Então, as pessoas podem fornecer o filtro que quiserem. Eu imagino que algumas pessoas forneceriam filtros quase padrão como uma implementação dos2unix, compressão, etc. (Talvez eles devam ser encadeados).

Eu acho que deve haver uma suposição de que qualquer que seja o processador embutido, ele deve ser compilável em ~ todos os sistemas de compilação, já que seria necessário construir uma ferramenta nativa temporária para esse propósito.

Pode ser tarde demais para introduzir um novo nome de diretório reservado, por mais que eu queira. [...] algum tipo de comentário opt-in pode ser necessário.

Se os arquivos só puderem ser acessados ​​por meio de um pacote especial, digamos runtime/embed , importar esse pacote pode ser o sinal de aceitação.

A abordagem io.Read parece que pode adicionar sobrecarga significativa (em termos de cópia e pegada de memória) para operações lineares conceitualmente simples como strings.Contains (como em cmd/go/internal/cfg ) ou , criticamente, template.Parse .

Para esses casos de uso, parece ideal permitir que o chamador escolha se deve tratar o blob inteiro como um string (presumivelmente mapeado em memória) ou io.ReaderAt .

Isso parece compatível com a abordagem geral runtime.Files , entretanto: a coisa retornada de runtime.Files.Open poderia ter um método ReadString() string que retorna a representação mapeada em memória.

algum tipo de comentário opt-in pode ser necessário.

Poderíamos fazer isso com o go versão no go.mod arquivo. Antes de 1.15 (ou qualquer outra coisa), o subdiretório static conteria um pacote e, em 1.15 ou superior, conteria ativos incorporados.

(Isso não ajuda muito no modo GOPATH , no entanto.)

Não estou convencido sobre a complexidade de um botão "comprimido versus não". Se fizermos isso, as pessoas vão querer que adicionemos controle sobre qual compressão, nível de compressão e assim por diante. Tudo o que precisamos adicionar é a capacidade de incorporar um arquivo de bytes simples.

Embora eu aprecie a busca pela simplicidade, também devemos nos certificar de que estamos atendendo às necessidades dos usuários.

12 das 14 ferramentas listadas em https://tech.townsourced.com/post/embedding-static-files-in-go/#comparison suportam compactação, o que sugere que é um requisito bastante comum.

É verdade que se poderia fazer a compressão como uma etapa de pré-construção fora de go, mas isso ainda exigiria 1) uma ferramenta para fazer a compressão 2) verificar algum tipo de assets.zip blob em vcs 3) provavelmente um utilitário biblioteca em torno da API de incorporação para desfazer a compactação. Nesse ponto, não está claro qual é o benefício.

Três dos objetivos listados na proposta inicial foram:

  • não faça check-in dos arquivos gerados
  • make go install / go build faz a incorporação automaticamente
  • ativos da loja compactados no binário quando apropriado

Se lermos o segundo como "não requer uma ferramenta separada para incorporação", então, não oferecer suporte a arquivos compactados direta ou indiretamente não atenderá a todos esses três objetivos.

Isso precisa ser no nível do pacote? O nível do módulo parece uma granularidade melhor, pois muito provavelmente um módulo = um projeto.

Uma vez que este diretório não conteria o código Go †, poderia ser algo como _static ?

† ou, se for, seria tratado como bytes arbitrários cujo nome termina em ".go" em vez de como código Go a ser compilado

Se for um diretório especial, a lógica poderia ser apenas engolir qualquer coisa e tudo nessa árvore de diretório. O pacote mágico de incorporação pode permitir que você faça algo como embed.Open("img/logo.svg") para abrir um arquivo em um subdiretório da árvore de ativos.

As cordas parecem boas o suficiente. Eles podem ser facilmente copiados em []byte ou convertidos em Reader . A geração de código ou bibliotecas podem ser usadas para fornecer APIs mais sofisticadas e lidar com coisas durante init . Isso pode incluir descompressão ou criação de http.FileSystem .

O Windows não tem um formato especial para incorporar ativos? Isso deve ser usado ao construir um executável do Windows? Em caso afirmativo, isso tem implicações para os tipos de operações que podem ser fornecidas?

Não se esqueça do gitfs 😂

Há alguma razão para que não faça parte do go build / link ... por exemplo, go build -embed example=./path/example.txt e algum pacote que expõe o acesso a ele (por exemplo, embed.File("example") , em vez de usar go:embed ?

você precisa de um esboço para isso em seu código, embora

@egonelbre, o problema com go build -embed é que todos os usuários precisam usá-lo corretamente. Isso precisa ser totalmente transparente e automático; Os comandos go install ou go get não podem parar de fazer a coisa certa.

@bradfitz Eu recomendaria https://github.com/markbates/pkger em vez de Packr. Ele usa a biblioteca padrão API para trabalhar com arquivos.

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
}

Ou talvez certas tags ou sinalizadores de construção possam fazer com que ele volte a fazer coisas em tempo de execução. O Perkeep (link acima) tem esse modo, que é bom para acelerar o desenvolvimento incremental onde você não se preocupa em vincular um grande binário.

mjibson / esc também faz isso, e é uma grande melhoria de qualidade de vida ao desenvolver um webapp; você não apenas economiza tempo de vinculação, mas também evita ter que reiniciar o aplicativo, o que pode levar muito tempo e / ou exigir a repetição de etapas extras para testar suas alterações, dependendo da implementação do webapp.

Problemas com a situação atual:

  • Usando uma solução baseada em go: generate incha o histórico do git com uma segunda cópia (um pouco maior) de cada arquivo.

Metas:

  • não faça check-in dos arquivos gerados

Bem, esta parte é facilmente resolvida apenas adicionando os arquivos gerados ao arquivo .gitignore ou equivalente. Eu sempre fiz isso ...

Então, como alternativa, Go poderia ter sua própria ferramenta de incorporação "oficial" que é executada por padrão em go build e pedir às pessoas que ignorem esses arquivos como uma convenção. Essa seria a solução menos mágica disponível (e compatível com versões anteriores do Go).

Estou apenas fazendo um brainstorming / pensando em voz alta aqui ... mas, na verdade, gosto da ideia proposta no geral. 🙂

Além disso, como //go:generate diretivas não são executadas automaticamente em go build o comportamento de go build pode parecer um pouco inconsistente: //go:embed funcionará automaticamente, mas para //go:generate você deve executar go generate manualmente. ( //go:generate já pode quebrar o fluxo de go get se ele gerar .go arquivos necessários para a construção).

//go:generate já pode interromper o fluxo go get se gerar .go arquivos necessários para a construção

Acho que o fluxo normal para isso, e aquele que geralmente uso, embora tenha demorado um pouco para me acostumar, é usar go generate inteiramente como uma ferramenta de fim de desenvolvimento e apenas enviar os arquivos que ele gera.

@bradfitz não precisa implementar http.FileSystem si. Se a implementação fornece um tipo que implementa http.File , então seria trivial para qualquer um, incluindo o pacote stdlib http, fornecer um invólucro em torno da função Open , convertendo o tipo em http.File para estar em conformidade com http.FileSystem

@andreynering //go:generate e //go:embed são muito diferentes, no entanto. Este mecanismo pode acontecer perfeitamente no momento da construção porque não executará código arbitrário. Acredito que seja semelhante a como o cgo pode gerar código como parte de go build .

Não estou convencido sobre a complexidade de um botão "comprimido versus não". Se fizermos isso, as pessoas vão querer que adicionemos controle sobre qual compressão, nível de compressão e assim por diante. Tudo o que precisamos adicionar é a capacidade de incorporar um arquivo de bytes simples.

Embora eu aprecie a busca pela simplicidade, também devemos nos certificar de que estamos atendendo às necessidades dos usuários.

12 das 14 ferramentas listadas em https://tech.townsourced.com/post/embedding-static-files-in-go/#comparison suportam compactação, o que sugere que é um requisito bastante comum.

Não tenho certeza se concordo com esse raciocínio.

A compactação feita por outras bibliotecas é diferente de adicioná-la a esta proposta, pois não reduzirá o desempenho em compilações subsequentes, uma vez que as alternativas geralmente são geradas antes da compilação, e não durante o tempo de compilação.

Os baixos tempos de construção são um claro valor agregado com o Go over outras linguagens e a compactação troca o tempo de CPU por um espaço de armazenamento / transferência reduzido. Se muitos pacotes Go começarem a executar compressões em go build , adicionaremos ainda mais tempo de construção do que o tempo adicionado, simplesmente copiando ativos durante as compilações. Eu sou cético quanto a adicionar compressão por causa de outros fazendo isso. Contanto que o projeto inicial não impeça por projeto uma extensão futura que adiciona suporte para, por exemplo, compressão, colocá-lo lá porque pode ser algo que poderia beneficiar alguns parece uma cobertura desnecessária.

A incorporação de arquivos não seria inútil sem compactação, a compactação é uma boa opção para reduzir o tamanho binário de talvez 100 MB para 50 MB - o que é ótimo, mas também não é um obstáculo claro para a funcionalidade da maioria dos aplicativos que consigo imaginar . Especialmente se a maioria dos ativos "mais pesados" forem arquivos como JPEGs ou PNGs, que já estão bem compactados.

Que tal manter a compressão fora por enquanto e adicioná-la se muitas pessoas realmente não perceberem? (e pode ser feito sem custos indevidos)

Para adicionar ao comentário de @sakjur acima: a compressão parece ortogonal para mim. Geralmente, desejo compactar um arquivo binário ou de lançamento inteiro, e não apenas os ativos. Particularmente quando os binários Go no Go podem facilmente chegar a dezenas de megabytes sem quaisquer ativos.

@mvdan Acho que uma das minhas preocupações é que muitas vezes quando vejo a incorporação é junto com algum outro pré-processamento: minificação, compilação de texto digitado, compressão de dados, processamento de imagem, redimensionamento de imagem, sprite-folhas. A única exceção são os sites que usam apenas html/template . Então, no final, você pode acabar usando algum tipo de "Makefile" de qualquer maneira ou carregando o conteúdo pré-processado. Nesse sentido, eu acho que um sinalizador de linha de comando funcionaria melhor com outras ferramentas do que comentários.

Acho que uma das minhas preocupações é que muitas vezes quando vejo a incorporação é junto com algum outro pré-processamento: minificação, compilação de texto digitado, compactação de dados, processamento de imagem, redimensionamento de imagem, sprite-folhas. A única exceção são os sites que usam apenas html / template.

Obrigado, esse é um ponto de dados útil. Talvez a necessidade de compressão não seja tão comum quanto parecia. Se for esse o caso, concordo que faz sentido deixá-lo de fora.

A incorporação de arquivos não seria inútil sem compactação, a compactação é uma boa opção para reduzir o tamanho binário de talvez 100 MB para 50 MB - o que é ótimo, mas também não é um obstáculo claro para a funcionalidade da maioria dos aplicativos que consigo imaginar .

O tamanho binário é um grande negócio para muitos desenvolvedores go (https://github.com/golang/go/issues/6853). Go compacta as informações de depuração DWARF especificamente para reduzir o tamanho do binário, embora isso acarrete um custo para o tempo de link (https://github.com/golang/go/issues/11799, https://github.com/golang/go/ edições / 26074). Se houvesse uma maneira fácil de cortar o tamanho do binário pela metade, acho que os desenvolvedores aproveitariam essa oportunidade (embora eu duvide que os ganhos aqui seriam quase tão significativos).

Isso realmente não ajuda no modo GOPATH, embora

Talvez, se você estiver no modo GOPATH, esse recurso simplesmente não se aplique, uma vez que imagino que a equipe de Go não planeja fazer paridade de recursos para GOPATH para sempre? Já existem recursos que não são suportados no GOPATH (como segurança com db de checksum, download de dependências por meio de um servidor proxy e controle de versão de importação semântica)

Como @bcmills mencionou, ter o nome do diretório estático em um arquivo go.mod é uma ótima maneira de introduzir esse recurso no Go 1.15, pois o recurso pode ser desativado automaticamente em arquivos go.mod que têm uma cláusula <= go1.14.

Dito isso, isso também significa que os usuários precisam escrever manualmente qual é o caminho do diretório estático.

Acho que o diretório do fornecedor e as convenções _test.go são ótimos exemplos de como eles tornaram o trabalho com Go e esses dois recursos muito mais fácil.

Não me lembro de muitas pessoas solicitando a opção de personalizar o nome do diretório do fornecedor ou tendo a capacidade de alterar a convenção _test.go para outra coisa. Mas se o Go nunca introduzisse o recurso _test.go, os testes no Go seriam muito diferentes hoje.

Portanto, talvez um nome menos genérico do que static ofereça melhores chances de não colisão e, portanto, ter um diretório convencional (semelhante a vendor e _test.go) pode ser uma melhor experiência do usuário em comparação com comentários mágicos.

Exemplos de nomes com potencial de baixa colisão:

  • _embed - segue a convenção _test.go
  • go_binary_assets
  • .gobin segue a convenção .git
  • runtime_files - de modo que corresponda à estrutura runtime.Files

Por último, o diretório vendor foi adicionado ao Go 1.5. Entããão, talvez não seja tão ruim adicionar uma nova convenção agora? 😅

Eu acho que deveria expor um mmap-readonly []byte . Apenas acesso bruto às páginas do executável, paginado pelo sistema operacional conforme necessário. Todo o resto pode ser fornecido além disso, com apenas bytes.NewReader .

Se isso for inaceitável por algum motivo, forneça ReaderAt não apenas ReadSeeker ; o último é trivial de construir a partir do primeiro, mas a outra maneira não é tão boa: seria necessário um mutex para proteger o deslocamento único e arruinar o desempenho.

A incorporação de arquivos não seria inútil sem compactação, a compactação é uma boa opção para reduzir o tamanho binário de talvez 100 MB para 50 MB - o que é ótimo, mas também não é um obstáculo claro para a funcionalidade da maioria dos aplicativos que consigo imaginar .

O tamanho binário é um grande negócio para muitos desenvolvedores go (# 6853). Go compacta as informações de depuração DWARF especificamente para reduzir o tamanho do binário, embora isso acarrete um custo para o tempo de link (# 11799, # 26074). Se houvesse uma maneira fácil de cortar o tamanho do binário pela metade, acho que os desenvolvedores aproveitariam essa oportunidade (embora eu duvide que os ganhos aqui seriam quase tão significativos).

Esse é definitivamente um ponto justo e posso ver como meu argumento pode ser visto como um argumento a favor do descuido com relação ao tamanho dos arquivos. Essa não era minha intenção. Meu ponto está mais de acordo com o envio desse recurso sem compactação, o que ainda seria útil para alguns, e eles poderiam fornecer feedback e percepções úteis sobre como adicionar compactação de forma adequada de uma forma que pareça correta a longo prazo. Os ativos podem aumentar de uma forma que as informações de depuração dificilmente farão e é mais fácil para os desenvolvedores de pacotes que são instalados / importados por outros reduzirem o desempenho de construção desnecessariamente se a implementação tornar mais fácil fazer isso.

Outra opção seria tornar a compactação de ativos um sinalizador de construção e deixar o compromisso entre o tamanho e o tempo de construção para o construtor, e não para o desenvolvedor. Isso moveria a decisão para mais perto do usuário final do binário, que poderia decidir se a compactação vale a pena. Otoh, isso arriscaria criar uma área de superfície aumentada para diferenças entre desenvolvimento e produção, então não é um método claramente melhor do que qualquer outra coisa e não é algo que eu gostaria de defender.

Minha ferramenta de incorporação de ativos atual carrega conteúdo dos arquivos de ativos quando construída com -tags dev . Alguma convenção como essa provavelmente seria útil aqui também; ele encurta o ciclo de desenvolvimento significativamente quando, por exemplo, mexer com HTML ou um modelo.

Caso contrário, o chamador terá que envolver este mecanismo de nível inferior com alguns *_dev.go e *_nodev.go wrappers e implementar o carregamento não incorporado para o cenário dev . Nem mesmo é difícil, mas esse caminho levará apenas a uma explosão semelhante de ferramentas que o primeiro comentário sobre esse problema descreve. Essas ferramentas terão que fazer menos do que hoje, mas ainda assim se multiplicarão.

Acho que -tags dev deixar de funcionar quando executado fora do módulo Go seria razoável (não consigo descobrir de onde carregar os ativos).

Que tal apenas um go tool embed que recebe entradas e produz arquivos de saída Go em um formato especial reconhecido pelo computador como arquivos embutidos que podem então ser acessados ​​por meio de runtime/emved ou algo assim. Então você poderia simplesmente fazer um simples //go:generate gzip -o - static.txt | go tool embed -o static.go .

Uma grande desvantagem, é claro, é que você precisa enviar os arquivos gerados.

@DeedleFake, esse problema começou com

Usando uma solução baseada em go: generate incha o histórico do git com uma segunda cópia (um pouco maior) de cada arquivo.

Woops. Esquece. Desculpa.

A incorporação de arquivos não seria inútil sem compactação, a compactação é uma boa opção para reduzir o tamanho binário de talvez 100 MB para 50 MB - o que é ótimo, mas também não é um obstáculo claro para a funcionalidade da maioria dos aplicativos que consigo imaginar .

O tamanho binário é um grande negócio para muitos desenvolvedores go (# 6853). Go compacta as informações de depuração DWARF especificamente para reduzir o tamanho do binário, embora isso acarrete um custo para o tempo de link (# 11799, # 26074). Se houvesse uma maneira fácil de cortar o tamanho do binário pela metade, acho que os desenvolvedores aproveitariam essa oportunidade (embora eu duvide que os ganhos aqui seriam quase tão significativos).

Se houver necessidade, as pessoas terão os dados compactados comprometidos e incorporados, e haverá pacotes para fornecer uma camada entre runtime.Embed e o consumidor final que faz a descompressão em linha.

E então, daqui a um ou dois anos, haverá um novo problema sobre como adicionar compactação e isso poderá ser resolvido.

Eu digo isso como um dos 15 padrões concorrentes quando escrevi goembed :)

@ tv42 escreveu:

Eu acho que deveria expor um mmap-readonly []byte . Apenas acesso bruto às páginas do executável, paginado pelo sistema operacional conforme necessário.

Este comentário é facilmente esquecido e extremamente valioso.

@ tv42 ,

Eu acho que deveria expor um byte mmap-readonly []. Apenas acesso bruto às páginas do executável, paginado pelo sistema operacional conforme necessário. Todo o resto pode ser fornecido além disso, com apenas bytes.NewReader.

O tipo que já é somente leitura é string . Além disso: fornece um tamanho, ao contrário de io.ReaderAt , e não depende da biblioteca padrão. Provavelmente é isso que queremos expor.

O tipo que já é somente leitura é string .

Mas todo o ecossistema de Write etc funciona em []byte . É um simples pragmatismo. Não vejo essa propriedade somente leitura sendo um problema mais do que io.Writer.Write docs dizendo

A gravação não deve modificar os dados da fatia, mesmo temporariamente.

Outra desvantagem potencial é que, ao incorporar um diretório com go:generate , posso verificar a saída de git diff e ver se algum arquivo está lá por engano. Com esta proposta -? Talvez o comando go imprima a lista de arquivos que está incorporando?

@ tv42

Mas todo o ecossistema de Write etc funciona em [] byte.

html/template funciona com strings, no entanto.

Go já permite que você use -ldflags -X para definir algumas strings (útil para definir a versão git, tempo de compilação, servidor, usuário etc.), este mecanismo poderia ser estendido para definir io.Readers em vez de strings?

@bradfitz Você está propondo usar strings aqui mesmo para dados que não são texto? É comum incorporar pequenos arquivos binários, como ícones e pequenas imagens, etc.

@ tv42 Você disse Write mas presumo que quis dizer Read . Você pode transformar um string em io.ReaderAt usando strings.NewReader , então usar uma string não parece uma barreira aqui.

@andreynering A string pode conter qualquer sequência de bytes.

Um string pode conter qualquer sequência de bytes.

Sim, mas sua principal intenção é conter texto e não dados arbitrários. Suponho que isso pode causar um pouco de confusão, em particular para desenvolvedores Go inexperientes.

Eu entendi totalmente a ideia, no entanto. Obrigado por esclarecer.

@ianlancetaylor

Read deve transformar a fatia passada. Write não é. Portanto, Write documentation diz que isso não é permitido. Não vejo nada mais sendo necessário do que documentar que os usuários não devem escrever no []byte retornado.

Só porque strings.Reader existe não significa que io.WriteString encontrará uma implementação eficiente para escrever strings. Por exemplo, TCPConn não tem WriteString .

Eu odiaria que Go incluísse um novo recurso como este apenas para forçar todos os dados a serem copiados apenas para gravá-los em um soquete.

Além disso, a suposição geral é que as strings podem ser impressas por humanos e []byte frequentemente não. Colocar JPEGs em strings causará muitos terminais bagunçados.

@opennota

html / template funciona com strings, no entanto.

Sim, isso é estranho, ele só aceita arquivos por nome de caminho, não como leitores. Duas respostas:

  1. Não há razão para que os dados incorporados não tenham os dois métodos Bytes() []byte e String() string .

  2. Esperançosamente, você não está analisando um modelo a cada solicitação; ao passo que você realmente precisa enviar os dados de um JPEG para um soquete TCP para cada solicitação que os solicite.

@ tv42 Podemos adicionar métodos WriteString conforme necessário.

Não acho que o uso mais comum dessa funcionalidade será gravar os dados não modificados, então não acho que devemos otimizar para esse caso.

Não acho que o uso mais comum dessa funcionalidade será escrever os dados não modificados,

Acho que o uso mais comum absoluto dessa funcionalidade será servir ativos da web, images / js / css, sem modificações.

Mas não acredite na minha palavra, vamos dar uma olhada em alguns dos importadores do conjunto de arquivos de Brad:

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



md5-f8b48fccd03599094034bf2b507e9e67



#fileembed pattern .*\.js$

E assim por diante..

Para dados anedóticos: eu sei que se isso fosse implementado, eu o usaria imediatamente em dois lugares no trabalho, e ambos seriam para fornecer acesso não modificado a arquivos textuais estáticos. No momento, usamos uma etapa //go:generate para converter os arquivos em strings constantes (formato hexadecimal).

Eu votaria no novo pacote em oposição à diretiva. Muito mais fácil de controlar, mais fácil de manusear / gerenciar e muito mais fácil de documentar e estender. Por exemplo, você pode encontrar facilmente a documentação para uma diretiva Go, como “go: generate”? E sobre a documentação do pacote “fmt”? Você vê onde estou indo com isso?

Então, como alternativa, Go poderia ter sua própria ferramenta de incorporação "oficial" que é executada por padrão em go build

@andreynering Eu sei que outros gerenciadores de pacotes e ferramentas de linguagem permitem isso, mas executar códigos / comandos arbitrários em tempo de construção é uma vulnerabilidade de segurança (pelo que espero serem razões óbvias).

Duas coisas adicionais vêm à minha mente quando penso sobre esse recurso:

  • Como a incorporação de arquivos funcionaria automaticamente com o cache de construção?
  • Impede compilações reproduzíveis? Se os dados forem alterados de alguma forma (por exemplo, compactando-os), deve-se levar em consideração a reprodutibilidade.

stuffbin , vinculado no primeiro comentário, foi desenvolvido para permitir principalmente que aplicativos da web auto-hospedados incorporem ativos estáticos (HTML, JS ...). Este parece ser um caso de uso comum.

Excluindo a discussão de compilação / compressão, outro ponto problemático é a falta de uma abstração do sistema de arquivos no stdlib porque:

  • Na máquina de um desenvolvedor, os numerosos go run compilações não precisam ser sobrecarregados com a sobrecarga de incorporar (enquanto opcionalmente compactar) ativos. Uma abstração do sistema de arquivos permitiria facilmente _failover_ para o sistema de arquivos local durante o desenvolvimento.

  • Os ativos podem mudar ativamente durante o desenvolvimento, por exemplo, um frontend Javascript completo em um aplicativo da web. A capacidade de alternar perfeitamente entre incorporar e o sistema de arquivos local em vez dos ativos incorporados permitiria evitar a compilação e a reexecução do binário Go apenas porque os ativos mudaram.

Edit: Para concluir, se o pacote embed pudesse expor uma interface semelhante a um sistema de arquivos, algo melhor do que http.FileSystem, isso resolveria essas preocupações.

A capacidade de alternar perfeitamente entre incorporar e o sistema de arquivos local

Certamente isso pode ser implementado no nível do aplicativo e está além do escopo desta proposta, não?

Certamente isso pode ser implementado no nível do aplicativo e está além do escopo desta proposta, não?

Desculpe, acabei de perceber, a forma como eu formulei é ambígua. Eu não estava propondo uma implementação de sistema de arquivos dentro do pacote embed, mas apenas uma interface, algo melhor do que http.FileSystem . Isso permitiria aos aplicativos implementar qualquer tipo de abstração.

Edit: Typo.

@knadh Concordo totalmente que deve funcionar quando você apenas usa go run também, a maneira como Packr lida com isso é muito boa. Ele sabe onde seus arquivos estão, se não estiverem embutidos no aplicativo, então ele os carrega do disco, pois espera que seja basicamente o "modo de desenvolvimento".

O autor de Packr também lançou uma nova ferramenta Pkger que é mais focada em Módulos Go. Os arquivos lá são todos relativos à raiz do Módulo Go. Eu realmente gosto dessa ideia, mas Pkger parece não ter implementado o carregamento de desenvolvimento local do disco. Uma combinação de ambos seria incrível, IMO.

Não sei se já está fora de execução, mas embora a "abordagem de pacote embutido" seja bem mágica, ela também oferece algo incrível porque a ferramenta pode deduzir o que fazer com o arquivo com base na chamada. por exemplo, a API pode 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 {…}

Se a ferramenta go encontrar uma chamada para FileReaderAt , ela saberá que os dados devem ser descompactados. Se ele encontrar apenas FileReader chamadas, ele saberá que pode armazenar dados compactados. Se encontrar uma chamada FileBytes , sabe que precisa fazer uma cópia, se encontrar apenas FileString , sabe que pode servir a partir da memória somente leitura. E assim por diante.

Não estou convencido de que essa seja uma maneira razoável de implementar isso para a ferramenta Go adequada. Mas eu queria mencionar isso, pois permite obter os benefícios da compactação e incorporação de cópia zero sem ter nenhum botão real.

[editar] também, é claro, vamos adicionar essas coisas estranhas extras após o fato, focando em um conjunto de recursos mais mínimo primeiro [/ edit]

Se ele encontrar apenas chamadas FileReader ...

Isso impediria o uso de outros métodos por meio de reflexão.

[Editar] Na verdade, acho que as implicações são mais amplas do que isso. Se o uso de FileReaderAt é uma indicação de que os dados devem ser descompactados, então o uso de FileReaderAt() com qualquer entrada não const implica que todos os arquivos devem ser armazenados descompactados.

Não sei se isso é bom ou ruim. Só acho que a heurística mágica não será tão útil quanto pode parecer à primeira vista.

Um argumento a favor de um pragma de comentário ( //go:embed ) em vez de um nome de diretório especial ( static/ ): um comentário nos permite incorporar um arquivo no arquivo de teste de um pacote (ou no arquivo xtest ), mas não a biblioteca em teste. O comentário só precisa aparecer em um arquivo _test.go .

Espero que isso resolva um problema comum com módulos: é difícil acessar dados de teste para outro pacote se esse pacote estiver em outro módulo. Um pacote pode fornecer dados para outros testes com um comentário como //go:embedglob testdata/* em um arquivo _test.go . O pacote pode ser importado para um binário normal que não seja de teste, sem extrair esses arquivos.

@ fd0 ,

Como a incorporação de arquivos funcionaria automaticamente com o cache de construção?

Ainda funcionaria. Os hashes de conteúdo do arquivo embutido seriam misturados à chave de cache.

Seria possível (ou mesmo uma boa ideia) ter um módulo / pacote / mecanismo que fosse praticamente transparente, já que de dentro de sua aplicação você simplesmente tenta abrir um caminho como

internal://static/default.css

e as funções de arquivo lerão os dados de dentro do binário ou de um local alternativo
ex Package.Mount("internal[/<folder>.]", binary_path + "/resources/")

para criar "internal: //" com todos os arquivos no binário, retroceda para o caminho / recursos executáveis ​​/ se estiver no modo dev ou se o arquivo não for encontrado no binário (e talvez lance um aviso ou algo para fins de registro)

Isso permitiria, por exemplo, ter

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

Provavelmente é melhor bloquear o local alternativo para o caminho do executável quando no modo 'release', mas relaxe no modo dev (permitir apenas pastas em go_path ou algo parecido)

Por padrão, o pacote "monta" internal: // ou alguma outra palavra-chave, mas permite que o usuário o renomeie se quiser .. ex .ReMount ("interno", "myCustomName") ou algo parecido.

Outra coisa ... faria sentido verificar a última mudança / hora modificada em um local alternativo e substituir automaticamente o arquivo interno se houver tal arquivo fora do aplicativo (talvez tenha um sinalizador que permita isso, configurável pelo programador antes da construção)
Isso pode ser desejado para patches super rápidos de aplicativos, onde você não quer esperar que uma nova compilação seja feita e distribuída .. você pode apenas criar a pasta e copiar o arquivo lá, e o binário mudará para o novo Arquivo.

No Windows, seria possível ou faria sentido usar recursos (como no blob de dados binários em um recurso)
E um pouco não relacionado, mas talvez este pacote também pudesse lidar com o agrupamento de ícones no executável, ou dados de manifesto, ou talvez até mesmo outros recursos? Sei que é só Windows ...
Eu imagino que o construtor poderia registrar as datas da última modificação / alteração dos arquivos nas pastas alternativas e apenas acionar uma "criação de blob de dados" se um arquivo for alterado e armazená-lo em cache em algum lugar.
Talvez apenas criar um arquivo "cache" se o usuário escolher habilitar a compactação nesses arquivos agrupados (se for decidido eventualmente compactá-los) ... se a compactação for escolhida, apenas o arquivo específico que foi modificado terá que ser recompactado no momento da compilação , outros arquivos seriam apenas copiados do cache para o binário.

Um problema que vejo é se o pacote permite nomes personalizados, ele precisa ter uma lista negra de algum tipo, como em não permitir "udp, arquivo, ftp, http, https e várias outras palavras-chave populares"

Quanto ao armazenamento como array de bytes / string ou compressão ... imho qualquer que seja a decisão tomada, deve haver espaço para uma atualização fácil no futuro ... por exemplo, você poderia começar sem compressão e apenas ter uma lista de deslocamentos e tamanhos de arquivo e nomes de arquivos, mas tornam possível adicionar compactação facilmente no futuro (ex método zlib, lzma, tamanho compactado, tamanho descompactado se for necessário para alocar memória suficiente para desempacotar pedaços etc etc.

Eu pessoalmente ficaria feliz se o executável pudesse ser empacotado com UPX ou equivalente, suponho que o binário seria descompactado na memória e tudo funcionaria.

Alguns pensamentos que se relacionam indiretamente:

  • Eu gosto da abordagem package embed por usar a sintaxe Go
  • Eu acho que a necessidade de compressão e outras manipulações não é sobre o tamanho binário, é sobre querer armazenar apenas a forma mais amigável de diff de conteúdo em um repositório, para que não haja um estado "fora de sincronia" onde alguém se esquece de regenerar e comprometa uma forma compactada ao alterar a "fonte" e para que o pacote permaneça "disponível para obtenção". Sem abordar esses pontos, estamos apenas resolvendo o problema de padronização, que pode ser aceitável, mas não parece ideal.
  • Acho que poderíamos evitar a necessidade do conjunto de ferramentas de oferecer suporte ativo a compactação / transformações específicas se a interação embed puder opcionalmente fornecer um "codec". Exatamente como definir o codec depende da sintaxe de integração, mas 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
}

Isso pode abranger casos de uso muito específicos, como este planejado:

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
}

Usá-lo pode parecer

// go:embed file.name NewJSONShrinker

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

ou possivelmente

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

Na segunda forma, há a complicação de que a cadeia de ferramentas precisa entender estaticamente qual Codec usar, porque ela precisa fazer a etapa Encode em tempo de compilação. Portanto, teríamos que proibir qualquer valor de Codec que não pudesse ser facilmente determinado em tempo de compilação.

Dadas essas duas opções, acho que escolheria o comentário mágico mais os codecs. Isso resulta em um recurso mais poderoso que aborda todos os objetivos declarados aqui. Além disso, não acho que comentários mágicos sejam inaceitáveis ​​aqui. Nós já os toleramos via go:generate para este propósito agora. De qualquer forma, pode-se considerar o pacote mágico sozinho mais um afastamento dos idiomas atuais. O ecossistema Go no momento não tem muitos recursos que permitem que um arquivo de origem instrua o conjunto de ferramentas a usar arquivos de origem adicionais, e acho que o único que não é um comentário mágico agora é a palavra-chave import .

Se fizermos a compactação, não haverá nenhum tipo de codec ou knobs de nível de compactação. Ou seja, ter qualquer botão é o maior argumento para não oferecer suporte à compressão.

A única escolha que gostaria de expor, se houver, é: acesso aleatório ou não. Se você não precisa de acesso aleatório, o conjunto de ferramentas e o tempo de execução podem escolher qualquer compactação apropriada e não expô-la aos usuários. E provavelmente mudaria / melhoraria com o tempo.

Mas descobri que o não tem compactação devido a uma percepção que tive: o próprio conteúdo que é mais compactável (HTML, JS, CSS, etc.) é o conteúdo ao qual você ainda deseja acesso aleatório (para ser servido por meio de, digamos, http.FileServer , que oferece suporte a solicitações de intervalo)

E olhando para o tamanho combinado do HTML / CSS / JS de Perkeep que incorporamos, é 48 KB descompactado. O binário do servidor Perkeep é de 49 MB. (Estou ignorando o tamanho das imagens incorporadas porque elas já estão compactadas.) Portanto, parece que não vale a pena, mas pode ser adicionado mais tarde.

Em uma discussão com @rsc , parece que poderíamos fazer uma combinação das abordagens acima:

No tempo de execução do pacote,

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(...) { ...}

Então, em seu código:

package yourcode

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

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

Em seguida, a ferramenta cmd / go iria analisar os comentários go:embed e glob esses padrões + hash desses arquivos e registrá-los com o tempo de execução, usando &website .

O tempo de execução teria efetivamente um mapa de cada endereço de Arquivos para saber qual é seu conteúdo e onde eles estão no arquivo executável (ou quais são seus nomes de seção ELF / etc). E talvez eles suportem ou não o acesso aleatório, se acabarmos fazendo alguma compressão.

@gdamore ,

Apenas mais uma questão aqui - eu tenho um projeto diferente que usa o código-fonte do DTrace (incorporado). Isso é sensível às diferenças entre n e rn.
...
Se várias munging não resolverem, isso é um argumento contra o uso desse novo recurso.

Você também pode munge em tempo de execução para remover qualquer retorno de carro que seja incorporado a partir de usuários do Windows executando go install. Eu escrevi esse filtro io.Reader algumas vezes.

Mas descobri que @rsc não tem compressão por causa de uma percepção que tive: o próprio conteúdo que é mais compactável (HTML, JS, CSS, etc.) é o conteúdo ao qual você ainda deseja acesso aleatório (para ser servido via, digamos, http.FileServer, que oferece suporte a solicitações de intervalo)

A compactação e o acesso aleatório não são totalmente mutuamente exclusivos. Veja, por exemplo, alguma discussão aqui: https://stackoverflow.com/questions/429987/compression-formats-with-good-support-for-random-access-within-archives

A compactação e o acesso aleatório não são totalmente mutuamente exclusivos

Sim, se quiséssemos busca granular com alguma sobrecarga para chegar à posição certa. Eu fiz alguns trabalhos neste espaço com o formato stargz do CRFS . Mas temo que a sobrecarga seja grande o suficiente para que não queiramos fazer isso automaticamente para as pessoas. Suponho que você também poderia inflá-lo preguiçosamente na memória (e ser capaz de soltá-lo em GCs, como um sync.Pool), mas simplesmente não parece valer a pena.

Temo que a sobrecarga seja grande o suficiente para que não queiramos fazer isso automaticamente para as pessoas.

É justo. A questão importante é se preferiríamos uma API que nos permitisse mudar de opinião sobre isso de forma barata, se as necessidades mudam ou se os experimentos mostram que a sobrecarga é aceitável.

@bradfitz bom ponto. E certamente posso fazer isso. FWIW, em meu repo eu também configurei git para ser menos tóxico ao ver arquivos .d. Ainda assim, considero útil a propriedade de strings embutidas com crases, já que é previsível e não está sujeita aos caprichos do git ou do sistema.

O que eu estava chegando com a ideia do Codec é que a compactação não é a única transformação que se pode desejar e que um tipo de Codec fornecido pelo usuário permite que o conjunto de ferramentas ignore outros sinalizadores além de "qual codec". Quaisquer níveis de compactação, ou o algoritmo, compactação ou não, teriam que ser específicos para o codec usado. Eu concordo totalmente que tentar "suportar compressão" no sentido de fornecer algum conjunto específico de formatos e botões seria uma perseguição louca com todas as variações que as pessoas poderiam querer. Na verdade, eu ficaria muito animado com os usos incomuns, como o pré-processamento de dados i18n, talvez, ou o processamento de conjuntos de dados como em latlong , então acho que ainda vale a pena considerar as opções em torno disso.

Pensei em outra maneira de fornecer a mesma flexibilidade que poderia ser mais agradável. A diretiva // go:embed poderia ser uma invocação de comando, assim como // go:generate é. Para o caso mais simples, algo como

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

A principal diferença é, é claro, que o stdout da invocação do comando está embutido no nome fornecido. O exemplo também usa um pacote fingido com go run para mostrar como provavelmente seria feito para tornar o comando OS independente, uma vez que cat pode não estar disponível em todos os lugares em que Go compila.

Isso cuida da etapa de "codificação" da transformação e, talvez, a tarefa da etapa de "decodificação" possa ser deixada para o usuário. O pacote runtime / embed pode apenas fornecer os bytes que o usuário pediu ao conjunto de ferramentas para incorporar, qualquer que seja a codificação. Isso é bom porque o usuário sabe qual deve ser o processo de decodificação.

Uma grande desvantagem disso é que não vejo uma boa maneira de incorporar um conjunto de arquivos múltiplos desta forma, além dos bytes incorporados serem um zip ou algo assim. Isso pode ser bom o suficiente, já que um glob ainda pode ser usado por um comando zip, e é no lado da definição que você realmente se preocupa com o glob. Mas também poderíamos ter dois recursos fora desta proposta, um para fazer um embed simples e outro para executar um embed de gerador.

Uma possível desvantagem que me ocorreu é que ele adiciona uma etapa aberta ao build, assumindo que as incorporações devem ser tratadas por go build e não requerem uma invocação de conjunto de ferramentas extra como go generate . Eu acho que está tudo bem, no entanto. Talvez seja esperado que a ferramenta gerencie seu próprio cache para evitar a repetição de operações caras, ou talvez ela possa se comunicar com a cadeia de ferramentas para usar o cache de Go. Isso parece um problema que pode ser resolvido e se encaixa no tema geral de go build fazer mais por nós (como buscar módulos).

Um dos objetivos deste projeto é garantir que as construções Go não exijam ferramentas externas e nem vão: gerar linhas?

Caso contrário, vale a pena manter as coisas simples e suportar apenas uma fatia de byte ou string porque se o usuário quiser compactar com muitos botões, ele pode fazê-lo em seu arquivo make (ou similar), ir gerar linha, etc. antes de construir de qualquer maneira, então não parece valer a pena adicioná-los a qualquer que seja o resultado desta proposta.

Se não exigir Make ou similar é um objetivo, então eu suponho que faça sentido usar compressão, mas pessoalmente eu prefiro usar Make, ir gerar, etc. para fazer a compressão e então manter a simplicidade e apenas alguns bytes .

@SamWhited ,

Um dos objetivos deste projeto é garantir que as construções Go não exijam ferramentas externas e nem vão: gerar linhas?

sim.

Se as pessoas quiserem usar go: generate ou Makefiles ou outras ferramentas, elas têm dezenas de opções hoje.

Queremos algo que seja portátil, seguro e correto que funcione por padrão. (e para ser claro: seguro significa que não podemos executar código arbitrário no momento de "ir para a instalação", pelo mesmo motivo que vai: gerar não é executado por padrão)

@ stephens2424

Acho que poderíamos evitar a necessidade de o conjunto de ferramentas precisar oferecer suporte ativo a compactação / transformações específicas se a interação incorporada puder opcionalmente fornecer um "codec".

Nenhuma execução arbitrária de código durante go build .

Nenhuma execução arbitrária de código durante o go build.

Sim, eu vejo isso agora. Suponho que não haja como reconciliar tendo apenas arquivos "fonte" comprometidos com um repo, querendo arquivos "processados" incorporados, para ter o pacote "go gettable," _and_ manter go build simples e seguro. Ainda estou a favor da padronização aqui, mas esperava ter meu bolo e comê-lo também, eu acho. Vale a pena experimentar! Obrigado por detectar o problema!

@flimzy

Isso impediria o uso de outros métodos por meio de reflexão.

Não há métodos no que mencionei, apenas funções. Eles não podem ser descobertos em tempo de execução e não há como referenciá-los sem mencioná-los pelo nome na fonte. E observe que os valores de interface retornados pelas diferentes funções não precisam ser do mesmo tipo - na verdade, eu esperaria que eles fossem tipos não exportados com exatamente o método necessário para implementar essa interface ou uma instância de *strings.Reader etc., o que quer que faça sentido no contexto.

Porém, indiscutivelmente, a ideia sofre ao passar as funções exportadas do pacote embed como valores. Embora isso provavelmente não seja um problema - a assinatura contém um tipo não exportado (veja abaixo), então você não pode declarar uma variável, argumento ou retorno de seu tipo. Você pode passá-los para reflect.ValueOf eles próprios, em teoria. Eu nem sei se isso permitiria que você realmente os chamasse (você ainda teria que construir um valor de seu tipo de parâmetro, que não foi exportado. Não sei se o reflexo permite isso).

Mas seja como for: ainda seria possível (e mais simples) simplesmente ser pessimista no caso de qualquer função de nível superior de embed ser usada como um valor e assumir as restrições que cria em todos os arquivos incorporados. O que significaria que se você decidir fazer coisas extremamente estranhas e inúteis com o pacote embed, você perderá algumas otimizações (sobre as quais não fazemos necessariamente nenhuma promessa). Parece justo.

Na verdade, acho que as implicações são mais amplas do que isso. Se o uso de FileReaderAt for uma indicação de que os dados devem ser descompactados, então o uso de FileReaderAt () com qualquer entrada não const implica que todos os arquivos devem ser armazenados descompactados.

Não faz sentido permitir entradas não constantes, pois o nome do arquivo precisa ser conhecido estaticamente para fazer a incorporação. No entanto, foi impreciso da minha parte usar string como o tipo de parâmetro de nome de arquivo: Eles deveriam ser realmente type filename string não exportados e não deveriam ser usados ​​como nada além de argumentos de função. Dessa forma, é impossível passar qualquer coisa que não seja uma constante de string não digitada.

@Merovius

Não faz sentido permitir entradas não constantes

Acho que estamos falando de coisas diferentes. Quero dizer entradas para as funções de acesso (ou seja, FileReaderAt() ). Tenho certeza de que você concordará que entradas não constantes fazem sentido aqui.

E meu ponto é: suponha que incorporamos 100 arquivos, mas temos uma chamada FileReaderAt(filename) , onde filename não é constante; não há como saber quais (se é que algum) dos arquivos embutidos serão acessados ​​dessa forma, portanto, todos devem ser armazenados descompactados.

@flimzy estávamos falando sobre a mesma coisa, só que seriamente não acho que nomes de arquivos não constantes fariam sentido :) O que, pensando bem, estava errado e foi um descuido. Desculpe por isso. Facilidades para globalizar ou incluir diretórios inteiros e, em seguida, iterar sobre eles, na verdade, são muito importantes, sim. Ainda acho que isso poderia ser resolvido - por exemplo, tomando a decisão por coleção (dir / glob) e permitindo apenas selecionar aqueles por nomes constantes - mas como eu disse: não é realmente uma API que eu consideraria super apropriada para a ferramenta Go por causa de como é mágico. Portanto, aprofundar-se dessa maneira provavelmente está dando ao conceito mais espaço na discussão do que ele merece :)

Outro caso que não vi nas mensagens anteriores e que me fez considerar a incorporação de um arquivo em um binário Go foi a impossibilidade de distribuir adequadamente um pacote wrapper de uma biblioteca compartilhada C usando go build / install regular (a biblioteca compartilhada permanece no fontes).

Eu não fiz isso no final, mas isso definitivamente me faria reconsiderar para este caso. A biblioteca C tem, de fato, muitas dependências que seriam mais fáceis de distribuir como uma biblioteca compartilhada. Essa biblioteca compartilhada pode ser incorporada pelas ligações Go.

Uau!!!

@ Julio-Guerra
Tenho certeza de que você ainda teria que extraí-los para o disco e, em seguida, usar dlopen e dlsym para chamar as funções C.

Edit: entendi mal sua postagem, acabei de perceber que você está falando sobre a criação de um binário para distribuição

Fora dos ativos estáticos http, para blobs incorporados que você precisa em um ponteiro para a memória, seria bom ter uma função que retornasse o ponteiro para a memória incorporada já em processo. Caso contrário, seria necessário alocar uma nova memória e fazer uma cópia do io.Reader. Isso consumiria o dobro da memória.

@glicerina , novamente, isso é string . A string é um ponteiro e um comprimento.

Não seria ótimo ter apenas uma maneira de marcar o código a ser executado em tempo de compilação e fornecer o resultado em tempo de execução. Dessa forma, você pode ler qualquer arquivo, compactá-lo se quiser em tempo de compilação e em tempo de execução você pode acessá-lo. Isso funcionaria para alguns cálculos como funcionaria para o pré-carregamento do conteúdo do arquivo.

@burka como dito antes no tópico, go build não executará código arbitrário.

@burka , isso está explicitamente fora do escopo. Essa decisão (sem execução de código em tempo de compilação) foi feita há muito tempo e este não é o bug para mudar essa política.

Um efeito colateral dessa proposta é que os proxies go nunca podem otimizar os arquivos que armazenam para serem apenas arquivos go. Um proxy deve armazenar um repositório inteiro porque ele não saberá se o código Go incorpora algum dos arquivos não Go.

Não sei se os proxies já otimizam para isso, mas é possível que algum dia eles queiram.

@leighmcculloch Mas também não acho que seja esse o caso hoje. Quaisquer arquivos não Go em um pacote Go devem ser incluídos nos arquivos do módulo, porque podem ser necessários para go test . Você também pode ter arquivos C para cgo, como outro exemplo.

Esta é uma direção interessante, definitivamente precisamos dela para nossos casos de uso.

Dito isso, sinto que existem diferentes casos de uso com diferentes requisitos, mas quase todos que estão comentando sobre _como_ eles acham que isso deve ser feito estão implicitamente visualizando seus próprios casos de uso, mas não os definindo explicitamente.

Pode ser útil - pelo menos realmente útil para mim - se pudéssemos delinear os diferentes casos de uso para uma solução de incorporação de arquivos e os desafios que cada caso de uso apresenta.

Por exemplo, nosso principal caso de uso é incorporar HTML + CSS + JS + JPG + etc para que, quando o aplicativo go for executado, ele possa gravar esses arquivos em um diretório de forma que possam ser servidos por um http.FileServer . Dado esse caso de uso, a maioria dos comentários que li sobre Leitores e Escritores foram estranhos para mim porque não precisamos acessar os arquivos do Go, apenas deixamos go-bindata copiá-los para o disco _ (embora talvez haja uma maneira de aproveitar técnicas melhores que simplesmente ainda não percebemos que deveríamos considerar.) _

Mas nossos desafios são os seguintes: Normalmente usamos GoLand com seu depurador e trabalharemos no aplicativo da web fazendo alterações contínuas. Portanto, durante o desenvolvimento, precisamos de http.FileServer para carregar os arquivos diretamente de nosso diretório de origem. Mas quando o aplicativo executa http.FileServer precisa ler esses arquivos do diretório onde os arquivos foram gravados pela solução de incorporação. O que significa que, quando compilamos, temos que executar go-bindata para atualizar os arquivos e, em seguida, verificá-los no Git. E isso geralmente é viável com go-bindata , embora certamente não seja uma ideia.

No entanto, em outras ocasiões, precisamos realmente executar um executável compilado, para que possamos anexar um depurador ao programa em execução e ainda assim fazer com que o programa carregue os arquivos do diretório de origem e não do diretório onde o arquivo incorporado foi escrito por go-bindata . Atualmente não temos uma boa solução para isso.

Então, esses são nossos casos de uso e desafios. Talvez outros possam definir explicitamente os outros casos de uso e o conjunto de desafios relacionados, de modo que essas discussões possam abordar explicitamente os vários espaços de problema e / ou denotar explicitamente que esse esforço não atenderá às necessidades específicas de um determinado espaço de problema.

Agradecemos antecipadamente por considerar.

Como não o vejo mencionado como um caso de uso, também nos beneficiaríamos disso para nosso diretório de modelos que acessamos por meio de template.ParseFiles.

Eu consideraria a abordagem mais limpa um método de aceitação por meio de go.mod . Isso garantiria que fosse compatível com as versões anteriores (já que os projetos existentes teriam que optar por usá-lo) e permitiria que ferramentas (como proxies go) determinassem quais arquivos são necessários. O comando go mod init pode ser atualizado para incluir uma versão padrão para novos projetos para torná-lo mais fácil de usar no futuro.

Posso ver os argumentos para ter o diretório como um nome padrão (se exigirmos adesão, então pode ser um nome mais limpo / mais simples) ou ter o nome do diretório definido no próprio go.mod e permitir que os usuários escolha o nome (mas tendo um padrão fornecido por go mod init .

Na minha opinião, uma solução como essa atinge um equilíbrio entre facilidade de uso e menos "mágica".

@jayconrod escreveu:

Um argumento a favor de um pragma de comentário (// go: embed) em vez de um nome de diretório especial (estático /): um comentário nos permite inserir um arquivo no arquivo de teste de um pacote (ou o arquivo xtest), mas não na biblioteca sob teste.

Esta é uma observação muito boa. Embora se quiséssemos ir com o nome do diretório especial, poderíamos usar um mecanismo familiar: static para todas as compilações, static_test para compilações de teste, static_amd64 para compilações amd64 e em breve. Não vejo uma maneira óbvia de fornecer suporte a tag de construção arbitrário, no entanto.

Pode haver um arquivo de manifesto no diretório estático (o padrão quando um manifesto vazio é incluir tudo, exceto o manifesto) que inclui globs e permite especificar tags de construção e talvez compressão posterior etc.

Uma vantagem é que, se a lista go atingir um diretório contendo um manifesto, ela poderá pular essa árvore à la # 30058

Uma desvantagem é que ele pode ter muitos recursos e não, obrigado

Um mecanismo simples de botão zero para agrupar arquivos em um pacote poderia ser um diretório especial go.files em um diretório de pacote (semelhante a go.mod em um módulo). O acesso seria limitado a esse pacote, a menos que ele opte por exportar um símbolo.

Editar: proposta de função ú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)
}

A abordagem import "C" já estabeleceu um precedente para caminhos de importação "mágicos". IMO funcionou muito bem.

Como não o vejo mencionado como um caso de uso, também nos beneficiaríamos disso para nosso diretório de modelos que acessamos por meio de template.ParseFiles.

Há outro desafio: embora o binário possa conter todos os arquivos necessários, esses mesmos arquivos seriam os padrões fornecidos pelo desenvolvedor. No entanto, modelos como, por exemplo, impressão ou políticas de privacidade, devem ser personalizáveis ​​pelo usuário final. Pelo que vejo, isso significa que deve haver alguma maneira de exportar meus arquivos padrão e então permitir que o binário use os arquivos customizados em tempo de execução, ou alguma forma de substituir as versões embutidas pelas customizadas.

Acho que isso poderia ser feito fornecendo uma API com funções para 'exportar' e 'substituir' um recurso incorporado. O desenvolvedor pode então fornecer algumas opções de linha de comando ao usuário final (usando internamente as chamadas API mencionadas).

Tudo isso, é claro, com base na suposição de que realmente haverá algum tipo de incorporação que definitivamente facilitaria a implantação.

Obrigado por abrir o problema. No trabalho, pensamos na mesma ideia de recurso, pois precisamos incorporar arquivos em praticamente todos os projetos Golang. As bibliotecas existentes funcionam bem, mas acho que esse é um recurso pelo qual Golang está clamando. É uma linguagem feita para se transformar em um único binário estático. Ele deve abranger isso, permitindo-nos carregar os arquivos de ativos necessários no binário, com uma API universal e amigável para o desenvolvedor.

Eu só quero fornecer rapidamente meus detalhes de implementação favoritos. Várias pessoas falaram sobre fornecer automaticamente uma API para ler os arquivos incorporados, em vez de precisar de outro sinal, como um comentário mágico. Acho que esse deve ser o caminho a percorrer, pois oferece uma sintaxe programática familiar para a abordagem. Buscar um pacote especial, possivelmente runtime/embed como mencionado anteriormente, satisfaria isso e permitiria uma fácil extensibilidade no futuro. Uma implementação como a seguinte faria mais sentido para mim:

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{}) 
}

Usar esse pacote em algum lugar do seu código deve acionar o compilador para fornecer esse arquivo para o tempo de execução, incorporando-o automaticamente.

// 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)

Além disso, acho que as questões de segurança e reprodutibilidade não devem ser um problema se restringirmos a importação de arquivos em ou possivelmente 1 nível de diretório abaixo do diretório go.mod, que também já foi mencionado anteriormente no tópico. Caminhos de incorporação absolutos resolveriam em relação a esse nível de diretório.

Falha ao acessar os arquivos durante o processo de compilação irá gerar um erro do compilador.

Também é possível criar um arquivo zip atrás de um binário, para que ele possa se tornar efetivamente um binário de extração automática. Talvez isso seja útil em alguns casos de uso? Fiz isso como um experimento aqui: https://github.com/sanderhahn/gozip

Go já tem „testdata“. Os testes de unidade usam IO regular para fazer o que quiserem. O escopo do teste significa que o conteúdo não é enviado. Isso é tudo que há para saber, sem frescuras, sem mágica, sem lógica de contêiner compactado, sem direcionamentos configuráveis, sem META-INF. Linda, simples, elegante. Por que não ter uma pasta de “dados” para dependências de escopo de tempo de execução agrupadas?

Podemos facilmente escanear projetos Go existentes no Github ea e chegar a uma série de projetos que já usam uma pasta de „dados“ e, portanto, requerem adaptação.

Outra coisa que não está clara para mim. Para a discussão de um diretório static , não está 100% claro para mim se estamos discutindo um diretório static _source_ ou um diretório static onde os arquivos serão disponibilizados _at runtime_ ?

E essa distinção é particularmente importante porque está relacionada ao processo de desenvolvimento e ao código de depuração que está em desenvolvimento.

@mikeschinkel está bem claro na postagem original que a incorporação aconteceria no momento da construção:

fazer go install / go build fazer a incorporação automaticamente

A postagem original e alguns dos comentários acima também discutem a existência de um modo "dev" para carregar arquivos em tempo de execução.

@mvdan Obrigado pela resposta. Então você está pensando que isso significa que o diretório /static/ proposto seria relativo à raiz do repo do aplicativo, do repo do pacote e / ou possivelmente ambos?

E a localização dos arquivos de tempo de execução dependeria totalmente de onde o desenvolvedor queria colocá-los?

Se tudo isso for verdade - e parece lógico - seria útil se os programas compilados com informações de depuração pudessem opcionalmente carregar arquivos de seu local de origem para facilitar a depuração sem muita lógica e código extra - e não padronizado.

Algumas pessoas mencionaram os proxies do módulo. Acho que é um grande teste de tornassol para um bom design desse recurso.

Parece possível hoje, sem executar o código do usuário, implementar um proxy de módulo viável que retira arquivos que não são usados ​​na construção. Alguns dos designs acima significam que os proxies de módulo devem executar o código do usuário para descobrir quais arquivos estáticos também devem ser incluídos.

As pessoas também mencionaram go.mod como opt-in.

Idéia: especificação no arquivo go.mod? Facilita a análise de outras ferramentas.

module github.com/foo/bar

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

Isso criaria um pacote em tempo de compilação com os dados do arquivo. A sintaxe Glob pode ser boa aqui, mas talvez simplificar e apenas incorporar diretórios seja bom o suficiente. (À parte: +1 para sintaxe de ** glob.)

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

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

Algo como StripPrefix pode ser bom aqui, mas não necessário. Fácil de criar um pacote de invólucro que usa os caminhos de arquivo que você deseja.

Poderia ser ainda mais simplificado:

module github.com/foo/bar

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

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

Mas não é nada intuitivo que o moddata tenha um comportamento diferente dependendo do pacote / módulo de chamada. Seria mais difícil escrever ajudantes (por exemplo, http.Conversor de sistema de arquivos)

Parece possível hoje, sem executar o código do usuário, implementar um proxy de módulo viável que retira arquivos que não são usados ​​na construção. Alguns dos designs acima significam que os proxies de módulo devem executar o código do usuário para descobrir quais arquivos estáticos também devem ser incluídos.

Não acho que haveria uma mudança significativa aqui. Em particular, o código C já poderia incluir qualquer arquivo na árvore, portanto, um proxy de módulo que desejasse fazer isso precisaria analisar C. Parece que, nesse ponto, quaisquer comentários mágicos ou API que apresentarmos serão um pequeno passo.

Alguns dos designs acima significam que os proxies de módulo devem executar o código do usuário para descobrir quais arquivos estáticos também devem ser incluídos.

Acho que está bem claro que "a ferramenta go não deve executar o código do usuário durante a construção" é uma linha desenhada na areia que não será cruzada aqui. E se a ferramenta go não pode executar o código do usuário, então deve ser possível dizer quais arquivos incluir sem ele.

Tenho tentado condensar minhas várias idéias sobre este caso de uso em algo convincente e, portanto, um grande +1 para o que @broady sugeriu. Acho que, na maior parte, resume o que venho pensando. No entanto, acho que a palavra-chave deve ser o verbo embed vez do substantivo data .

  1. Os arquivos incorporados parecem algo que deve ser importado em vez de apenas ter um comentário especial ou um pacote mágico. E em um projeto Go, o arquivo go.mod é onde um desenvolvedor pode especificar os módulos / arquivos que são necessários, então faz sentido estendê-lo para suportar a incorporação.

  2. Além disso, uma coleção de arquivos incorporados parece-me que eles seriam mais valiosos e reutilizáveis ​​se um pacote que pudesse ser incluído em vez de algo ad-hoc adicionado a um projeto Go usando uma sintaxe única. A ideia aqui é que, se os embutidos fossem implementados como pacotes, as pessoas poderiam desenvolvê-los e compartilhá-los via Github e outros poderiam usá-los em seus projetos. Imagine pacotes mantidos pela comunidade e gratuitos no GitHub contendo:

    uma. Arquivos para países onde cada arquivo contém todos os códigos postais daquele país,
    b. Um arquivo com todas as strings de user agent conhecidas para identificar navegadores,
    c. Imagens da bandeira de cada país do mundo,
    d. Informações de ajuda detalhadas que descrevem os erros mais comuns em um programa Go,
    e. e assim por diante...

  3. Um novo esquema de URL como goembed:// - ou talvez um existente - que pode ser usado para abrir e ler arquivos do pacote, permitindo assim que _ (todos?) _ APIs de manipulação de arquivos existentes sejam aproveitadas em vez de criar novos uns, algo como o seguinte, que seria relativo ao embed contido no pacote atual:

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

Com os conceitos acima, nada parece _ "mágico" _; tudo seria tratado com elegância por um mecanismo que parece ter sido feito para um propósito específico. Seria necessária muito pouca extensão; um novo verbo em go.mod e um novo esquema de URL que seria reconhecido internamente pelo Go. Todo o resto seria fornecido no estado em que se encontrava.

O que faço agora

Eu uso code.soquee.net/pkgzip para isso agora (é um fork de statik que muda a API para evitar o estado global e importar efeitos colaterais). Meu fluxo de trabalho normal (pelo menos em um aplicativo da web) é incorporar ativos agrupados em um arquivo ZIP e exibi-los usando golang.org/x/tools/godoc/vfs/zipfs e golang.org/x/tools/godoc/vfs/httpfs .

go: abordagem embed

Existem duas coisas que provavelmente me impediriam de adotar a abordagem go:embed :

  1. O código gerado não aparecerá na documentação
  2. Os ativos podem estar espalhados por toda a base de código (isso se aplica ao uso de ferramentas externas e go:generate também, e é por isso que geralmente prefiro usar um makefile para gerar vários conjuntos de ativos antes da construção, então eu pode ver todos no makefile)

Há também um problema que não incluí acima porque pode ser um recurso para alguns que ter ativos como parte de um pacote (em oposição a todo o módulo) significa que toda a complexidade das tags de construção, pacotes de teste, etc. .aplicar a eles, precisamos de uma maneira de especificar se são públicos ou privados para aquele pacote, etc. Isso parece uma grande complexidade de compilação extra.

O que eu gosto nisso é que as bibliotecas podem ser escritas apenas para tornar a importação de ativos mais fácil. Por exemplo. uma biblioteca com um único arquivo Go que apenas incorpora uma fonte, ou alguns ícones poderiam ser publicados e eu poderia importá-lo como qualquer outro pacote go. No futuro, eu poderia obter uma fonte de ícone simplesmente importando-a:

import "forkaweso.me/forkawesome/v2"

abordagem de pacote embutido

Embora eu goste da ideia de ter tudo explícito, um código Go normal, odeio a ideia de que este seria outro pacote mágico que não pode ser implementado fora da biblioteca padrão.

Esse pacote seria definido como parte da especificação do idioma? Caso contrário, é outro lugar onde o código Go quebraria entre diferentes implementações, o que também parece ruim. Provavelmente continuaria usando uma ferramenta externa para evitar essa quebra.

Além disso, como outros mencionaram, o fato de que isso está sendo feito em tempo de construção significa que este pacote só pode aceitar strings literais ou constantes como argumentos. Atualmente, não há como representar isso no sistema de tipos e suspeito que isso será um ponto de confusão. Isso poderia ser resolvido com a introdução de algo como funções constantes, mas agora estamos falando de mudanças importantes na linguagem, tornando-o um iniciante. Não vejo uma boa maneira de corrigir isso de outra forma.

Híbrido

Gosto da ideia de uma abordagem híbrida. Em vez de reutilizar os comentários (que acabam espalhados por todo o lugar e, em uma nota pessoal, parecem nojentos), gostaria de ver todos os recursos colocados em um só lugar, provavelmente o arquivo go.mod como outros disse:

module forkaweso.me/forkawesome/v2

go 1.15

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

Isso significa que os ativos não podem ser incluídos ou excluídos por tags de construção arbitrárias ou em pacotes arbitrários (por exemplo, o pacote _testing) sem a criação de um módulo separado. Acho que essa redução na complexidade pode ser desejável (não há tag de construção oculta em uma biblioteca que você está tentando importar, e você não consegue descobrir por que não tem o ativo certo porque a importação da biblioteca deveria tê-lo incorporado) , mas YMMV. Se isso for desejável, os comentários do tipo pragma ainda podem ser usados, exceto que eles não geram código e, em vez disso, usam a mesma abordagem que estou prestes a descrever para a versão go.mod .

Ao contrário da proposta original, isso não geraria nenhum código. Em vez disso, a funcionalidade para, por exemplo. ler a seção de dados do arquivo ELF (ou no entanto, isso acaba sendo armazenado em qualquer sistema operacional que você está usando) seria adicionado quando apropriado (por exemplo, os ou debug/elf , etc.) e então, opcionalmente, um novo pacote seria criado que se comportaria exatamente como o pacote descrito no OP, exceto que em vez de ser mágico e fazer a incorporação em si, ele meramente lê os arquivos incorporados (o que significa que pode ser implementado fora da biblioteca padrão se desejado).

Isso contorna questões como ter que restringir o pacote mágico para permitir apenas strings literais como argumentos, mas significa que é mais difícil verificar se os ativos embutidos são realmente usados ​​em qualquer lugar ou acabam sendo um peso morto. Também evita quaisquer novas dependências entre os pacotes de biblioteca padrão, porque o único pacote que precisa importar algo extra é o próprio pacote novo.

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

Como visto acima, colocar os recursos no módulo ainda pode direcioná-los a esse módulo específico, se desejado. A API real e como você seleciona um ativo podem precisar de algum trabalho.

Ainda outra ideia: em vez de adicionar um novo tipo de verbo embed em go.mod , poderíamos introduzir um novo tipo de pacote, um pacote de dados, que é importado e usado em go.mod no maneira usual. Aqui está um esboço de espantalho.

Se um pacote contém exatamente um arquivo .go , static.go , e esse arquivo contém apenas comentários e uma cláusula de pacote, então um pacote é um pacote de dados. Quando importado, cmd / go preenche o pacote com funções exportadas, fornecendo acesso aos arquivos contidos nele, que são incorporados ao binário resultante.

Se for um pacote real, isso significa que as regras internal se aplicam e podemos ter controles de acesso sem adicionar à API.

Que tal incluir automaticamente todos os arquivos e subpastas que não sejam .go (seguindo as regras de código não reais) no diretório?

Se um pacote contém exatamente um arquivo .go , static.go , e esse arquivo contém apenas comentários e uma cláusula de pacote, então um pacote é um pacote de dados.

Essa verificação seria feita antes da aplicação das tags de construção? Nesse caso, parece mais um caso especial, que pode ser evitado. Do contrário, é bem possível que um pacote seja visto como um pacote Go padrão para algumas tags de construção e como um pacote de dados para outras. Isso parece estranho, mas talvez seja desejável?

@flimzy
Isso meio que permitiria usar arquivos embutidos com uma tag e definir o mesmo fns / vars como o pacote gerado e servir os arquivos de outra maneira (talvez remota?) Com outra tag.

Seria bom se houvesse um sinalizador de construção para gerar as funções de invólucro, de forma que só fosse necessário preencher os espaços em branco.

@josharian

Se um pacote contém exatamente um arquivo .go , static.go , e esse arquivo contém apenas comentários e uma cláusula de pacote, então um pacote é um pacote de dados.

Posso imaginar os pacotes de "dados" como tendo sua própria funcionalidade específica de domínio, como uma consulta de código postal. A abordagem que você acabou de propor não permitiria nada além dos dados brutos e, portanto, anularia os benefícios de ser capaz de empacotar lógica com dados.

Posso imaginar os pacotes de "dados" como tendo sua própria funcionalidade específica de domínio, como uma consulta de código postal.

Você pode expor a funcionalidade em my.pkg / postalcode e colocar os dados em my.pkg / postalcode / data (ou my.pkg / postalcode / internal / data).

Vejo o apelo de fazer da maneira que você sugere, mas levanta um monte de questões: Como funciona a compatibilidade com versões anteriores? Como você marca um pacote de dados como tal? O que você faria se o pacote tivesse funções que entrariam em conflito com o cmd / go adicionaria? (Não estou dizendo que não haja respostas, apenas que é mais simples não ter que respondê-las.)

@josharian , considere o comentário de verificação de tipo acima (https://github.com/golang/go/issues/35950#issuecomment-561443566).

@bradfitz sim, isso seria uma mudança de idioma e exigiria suporte go / types.

Na verdade, há uma maneira de fazer isso sem que seja uma mudança de linguagem - requer static.go para conter funções sem corpo que correspondam exatamente ao que cmd / go preencheria.

requerem static.go para conter funções sem corpo que correspondam exatamente ao que cmd / go preencheria.

Se ele gerar funções por arquivo em vez de capturar todos os embed.File() , isso permitiria controles fáceis de exportação por ativo.

Assim, o material gerado ficaria assim:

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

Uma postagem no blog que escrevi sobre arquivos estáticos 4 meses atrás. Veja a última frase nas conclusões :-)

@josharian

Você pode expor a funcionalidade em my.pkg / postalcode e colocar os dados em my.pkg / postalcode / data (ou my.pkg / postalcode / internal / data).

Isso - embora deselegante - poderia resolver minhas preocupações.

Como funciona a compatibilidade com versões anteriores?

Não vejo como as preocupações de BC se aplicam aqui. Você pode elaborar?

Como você marca um pacote de dados como tal?

Com a instrução embed em go.mod ?

Talvez eu não siga o que você está pedindo.

Mas vou dar a volta por cima; como você marca um pacote apenas com dados como tal?

O que você faria se o pacote tivesse funções que entrariam em conflito com o cmd / go adicionaria?

  1. Usando a abordagem proposta, não acho que seria necessário cmd / go para adicionar quaisquer funções.

  2. Mesmo se cmd / go precisar adicionar funções, imagino que o comportamento dos conflitos em um pacote _existente_ seria _undefinado_.

    A proposta assume que o desenvolvedor segue o princípio de responsabilidade única e, portanto, deve apenas construir um pacote com dados para ser um pacote centrado em dados e não adicionar dados a um pacote centrado em lógica existente.

    É claro que um desenvolvedor _pode_ adicionar a um pacote existente, caso em que o comportamento seria indefinido. IOW, se um desenvolvedor ignorar o idioma, ele estará em um território desconhecido.

Não estou dizendo que não haja respostas, apenas que é mais simples não ter que respondê-las.

Exceto que acho que as respostas são simples. Pelo menos para aqueles que você posou até agora.

Acho que qualquer solução que adiciona símbolos ou valores para símbolos deve ter o escopo do pacote, não do módulo. Porque a unidade de compilação para Go é um pacote, não um módulo.

Portanto, isso exclui qualquer uso de go.mod para especificar a lista de arquivos a serem importados.

@dolmen se o resultado final estiver em seu próprio pacote, o próprio escopo seria um módulo.

@urandom Não, o escopo são os pacotes que importam o pacote gerado. Mas, de qualquer forma, não acho que um pacote gerado completo esteja no escopo desta proposta.

@urandom Não, o escopo são os pacotes que importam o pacote gerado. Mas, de qualquer forma, não acho que um pacote gerado completo esteja no escopo desta proposta.

independentemente de como essa proposta seria implementada, dado que vários pacotes de módulo usarão o resultado final, faz sentido que a definição do que é incorporado seja especificada em um nível de módulo. um precedente para isso também existe, no ecossistema java, onde os arquivos incorporados têm escopo de módulo e são adicionados a partir de um diretório mágico.

além disso, go.mod apresenta a maneira mais limpa de adicionar esse recurso (sem comentários mágicos ou diretórios mágicos) sem quebrar os programas existentes.

Aqui está algo que ainda não foi mencionado: a API para ferramentas de processamento de origem Go (compilador, analisadores estáticos) é tão importante quanto a API de tempo de execução. Este tipo de API é um valor central do Go que ajuda a desenvolver o ecossistema (como go/ast / go/format e go mod edit ).

Esta API pode ser usada por ferramentas de pré-processador (em go:generate etapas em particular) para obter a lista de arquivos que serão incorporados ou pode ser usada para gerar a referência.

No caso de um pacote especial, não vejo nada a mudar na análise go.mod ( go mod tools) ou go/ast parser.

@dolmen

_ "Eu acho que qualquer solução que adiciona símbolos ou valores para símbolos deve ter escopo de pacote, não de módulo. Porque a unidade de compilação para Go é pacote, não módulo. Portanto, isso deixa de fora qualquer uso de go.mod para especificar a lista de arquivos para importar. "_

O que é módulo? Módulos são _ " uma coleção de pacotes Go relacionados que são versionados juntos como uma única unidade ." _ Assim, um módulo pode consistir em um único pacote, e um único pacote pode ser a totalidade de um módulo.

Assim, go.mod é o local correto para especificar a lista de arquivos a serem importados, assumindo que a equipe Go adote go.mod vez de comentários especiais e pacotes mágicos. Isso é, a menos e até que a equipe Go decida adicionar um arquivo go.pkg .

Além disso, se a equipe de Go aceitasse go.mod como o local para especificar os arquivos incorporados, qualquer pessoa que desejasse incorporar os arquivos deveria fornecer go.mod com as instruções embed e o pacote que é representado pelo diretório no qual o arquivo go.mod reside seria o pacote que contém os arquivos incorporados.

Mas se não for isso que o desenvolvedor deseja, ele deve criar outro arquivo go.mod e colocá-lo no diretório do pacote que deseja que contenha seus arquivos incorporados.

Você imagina um cenário legítimo em que essas restrições não sejam viáveis?

@mikeschinkel , um módulo é uma coleção de pacotes _related_. No entanto, é possível (e razoável!) Usar um pacote de um módulo sem extrair as dependências transitivas (e dados!) De outros pacotes dentro desse módulo.

Os arquivos de dados são geralmente dependências por pacote, não por módulo, portanto, as informações sobre como localizar essas dependências devem ser colocadas junto com o pacote - não armazenadas como metadados em nível de módulo separados.

@bcmills

Parece que se pode substituir 'Arquivos de dados' em sua mensagem por 'módulos' e isso ainda será válido.
É bastante comum ter módulos específicos como dependências para pacotes específicos de sua preferência.
Ainda assim, colocamos todos eles dentro do go.mod.

@urandom , nem todos os pacotes nos módulos indicados no arquivo go.mod estão vinculados ao binário final. (Colocar uma dependência no arquivo go.mod _não_ é equivalente a vincular essa dependência ao programa.)

Meta-ponto

É claro que isso é algo com que muita gente se preocupa, e o comentário original de Brad no topo era menos uma proposta concluída do que um esboço inicial / ponto de partida / call to action. Acho que faria sentido neste ponto encerrar essa discussão específica, fazer com que Brad e talvez algumas outras pessoas colaborassem em um documento de design detalhado e, em seguida, iniciar uma nova edição para uma discussão desse documento específico (ainda a ser escrito) . Parece que isso ajudaria a enfocar o que se tornou uma conversa um tanto extensa.

Pensamentos?

Não tenho certeza se concordo em fechar este, mas podemos Proposta - Esperar até que haja um documento de design e bloquear os comentários um pouco. (Quase todos os comentários são redundantes neste ponto, pois há muitos comentários para as pessoas lerem para ver se o comentário deles é redundante ...)

Talvez quando houver um documento de design, este possa ser fechado.

Ou feche este e reabra o # 3035 (com os comentários congelados) para que pelo menos um problema aberto rastreie.

Desculpe por fazer isso logo depois de falar sobre o fechamento, mas a discussão acirrada saltou logo após o comentário de @bcmills e antes que eu pudesse esclarecer.

"_No entanto, é possível (e razoável!) Usar um pacote de um módulo sem extrair as dependências transitivas (e dados!) De outros pacotes dentro desse módulo." _

Sim, claramente é _possível._ Mas, assim como qualquer prática recomendada, uma prática recomendada para pacotes de dados poderia ser criar um único pacote para um módulo, o que resolve sua preocupação. Se isso significa um go.mod na raiz e outro go.mod no subdiretório contendo o pacote de dados, que seja.

Acho que estou defendendo que você não torna perfeito o inimigo do bom aqui, onde o bem aqui é identificado como go.mod sendo um lugar perfeito para especificar arquivos incorporados, visto que, por sua natureza, eles são uma lista de componentes de um módulo.

Desculpe, mas os pacotes são o conceito fundamental em Go, não os módulos.
Módulos são simplesmente grupos de pacotes que são versionados como uma unidade.
Os módulos não contribuem com semântica adicional além das dos pacotes individuais.
Isso é parte de sua simplicidade.
Tudo o que fazemos aqui deve estar vinculado a pacotes, não a módulos.

Onde quer que isso vá e como seja feito, deve haver uma maneira de obter uma lista de todos os ativos que serão incorporados usando a lista go (não apenas os padrões usados).

Colocando em espera até que Brad e outros elaborem um documento de design formal.

Bloquear certos arquivos não deve ser muito difícil, especialmente se você usar um diretório static ou embed . Os links simbólicos podem complicar isso um pouco, mas você pode simplesmente evitar que ele incorpore qualquer coisa fora do módulo atual ou, se você estiver no GOPATH, fora do pacote que contém o diretório.

Não sou particularmente fã de um comentário que compila em código, mas também acho um pseudo-pacote que afeta a compilação um pouco estranho. Se a abordagem de diretório não for usada, talvez faça um pouco mais sentido ter algum tipo de declaração de nível superior embed realmente embutida na linguagem. Funcionaria de forma semelhante a import , mas só suportaria caminhos locais e exigiria um nome para ser atribuído. Por exemplo,

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))
}

Edit: Você chegou antes de mim, @jayconrod.

Está claro e legível, mas não tenho certeza se a equipe de Go gostaria de introduzir uma nova palavra-chave

A ideia, pelo que me lembro, era apenas ter um nome de diretório especial como "estático" contendo os dados estáticos e torná-los automaticamente disponíveis por meio de uma API, sem a necessidade de anotações.

Usar static como um nome de diretório especial é um pouco confuso e prefiro assets .
Outra ideia que não vi no tópico é permitir a importação de assets como um pacote, por exemplo, import "example.com/internal/assets" . A API exposta ainda precisa de um design, mas pelo menos parece mais limpa do que comentários especiais ou novos pacotes no estilo runtime/files .

Outra ideia que não vi no tópico é permitir a importação de ativos como um pacote

Isso foi proposto aqui: https://github.com/golang/go/issues/35950#issuecomment -562966654

Uma complicação é que, para permitir que a verificação de tipo ocorra, você precisa fazer com que seja uma alteração de idioma ou fornecer funções sem corpo a serem preenchidas por cmd / go .

É uma ideia semelhante, mas o design de static.go permite transformar um caminho de importação arbitrário em um pacote de dados, enquanto o diretório assets funciona mais como testdata , internal ou vendor em termos de ser “especial”. Um dos possíveis requisitos que assets poderia ter é não conter pacotes Go (ou permitir apenas documentos), ou seja, para compatibilidade implícita com versões anteriores.

Isso também pode ser combinado com a API runtime/files -thingy para obter os arquivos. Isto é, usando import s brutos para embutir árvores de diretórios com arquivos e então usar algum pacote de tempo de execução para acessá-los. Pode ser até os.Open , mas é improvável que seja aceito.

O shurcooL/vfsgen juntos shurcooL/httpgzip tem um bom recurso onde o conteúdo pode ser servido sem descompressão.

por exemplo

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

Um recurso semelhante está sendo proposto para C ++: std::embed :

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

Pode ser útil como inspiração para um design e para coletar possíveis casos de uso.

Estou um pouco atrasado para a festa, mas tive uma ideia. Em vez de comentários especiais, um diretório especial fixo (estático) ou a abordagem aparentemente proibida de estender go.mod: que tal um novo arquivo de manifesto por pacote: go.res

  • Contém uma lista de arquivos. Sem caminhos ou globs, apenas nomes no diretório do pacote atual. Gere-o a partir de um glob antes de comprometer, se necessário.

    • __Edit__ pode precisar de uma única linha package mypackagename no topo, como um arquivo go teria. Como alternativa, você pode incluir o nome do pacote no nome do arquivo (por exemplo, mypackagename.go.res). Pessoalmente, gosto mais da linha de cabeçalho package .

  • Um novo pacote básico denominado “recurso” ou talvez “io / recurso”. Tem pelo menos uma função: func Read(name string) (io.Reader, bool) para ler recursos embutidos no pacote atual.

    • __Edit__ Não tenho certeza se os pacotes principais funcionam dessa maneira. Pode ter que ser uma função privada do pacote gerado (por exemplo, func readresource(name string) (io.Reader, bool) )

  • Se você quiser recursos em um subdiretório, torne o subdiretório um pacote adicionando um arquivo go.res e pelo menos um arquivo .go . O arquivo go exporta sua própria API pública para acessar recursos no pacote de subdiretório. O arquivo go e a API exportada são necessários, porque os recursos de outros pacotes não são exportados automaticamente (por design). Você também pode personalizar como eles são exportados dessa maneira.

    • __Edit__ alternativamente, se você precisar de uma estrutura de diretório e / ou compactação, use um recurso tar. Isso permite coisas como pacotes de webpack, que já requerem compilação (e podem se beneficiar da pré-compactação). Levá-los um passo adiante até o alcatrão é simples.

  • __Edit__ Precisa de um manifesto? Basta incluir o próprio arquivo go.res como um recurso. Não precisamos nem criar uma função listresources.

Extremamente simples. Uma nova função. Um novo arquivo. Sem caminhos. Sem compressão. Sem nova sintaxe. Sem mágica. Extensível. Acesso somente leitura via leitor (mas aberto a padrões de acesso alternativos no futuro). Possibilidade quase zero de quebrar os pacotes existentes. Continua com o pacote sendo a construção central em andamento.

__Edit__ Depois de uma pesquisa no github language:go filename:go.res extension:res , parece que go.res seria um nome de arquivo bastante seguro para usar. Não há correspondências em go repos e apenas algumas em non-go repos.

Gosto da ideia de @ chris.ackermanm. Mas eu preferiria uma combinação:

Um arquivo go.res especificando o namespace em um diretório.

Isso permite

  • múltiplo inclui, desde que o namespace seja diferente
  • não saber os arquivos antes e ter que gerar uma lista

Este último deve lidar com a saída do webpack e similares que podem mudar o layout devido a atualizações, opções diferentes, o que você puder imaginar.

Com relação à compressão: acho que é mais uma característica em termos de não ter os tamanhos binários explodindo e deve ser transparente para o código de uso.

Mais tarde, você pode permitir reescritas como

filename => stored-as.png

Apenas meus 2 centavos

@sascha-andres Parece que ultra simplicidade e magia zero é o tom deste tópico. Veja as edições que fiz em meu comentário sobre suas sugestões.

Não gosto do mapeamento. Não há necessidade. Isso é possível expondo sua própria função de leitura de um pacote separado de qualquer maneira, e agora precisamos de uma nova sintaxe de arquivo, ou algo mais complexo do que arquivo por linha.

Oi

Essa proposta é incrível!

E eu tenho minha abordagem para ativos emebed. não há necessidade de introduzir nenhuma ferramenta diferente do GNU bintools. É meio sujo, mas funciona bem para mim por enquanto. Só quero compartilhar e ver se ajuda.

minha abordagem é apenas incorporar meus recursos (compactados com tar & gz) em uma seção elf / pe32 com objcopy e lê-los via pacote debug / elf e debug / pe32 junto com zip quando necessário. tudo que eu preciso lembrar é não tocar em nenhuma seção existente. todos os ativos são imutáveis ​​e, em seguida, o código lê o conteúdo e o processa na memória.

eu sou bastante inexperiente em design de linguagem ou design de compilador. então, eu usaria apenas a abordagem descrita acima e usaria .goassets ou algo parecido como o nome da seção. e tornar a compressão opcional.

minha abordagem é apenas incorporar meus recursos (compactados com tar & gz) em uma seção elf / pe32 com objcopy e lê-los via pacote debug / elf e debug / pe32 junto com zip quando necessário. tudo que eu preciso lembrar é não tocar em nenhuma seção existente. todos os ativos são imutáveis ​​e, em seguida, o código lê o conteúdo e o processa na memória.

Parece que funciona em elf / pe32 mas e quanto a mach-o / plan9 ?

Outro problema é que ele depende da abertura de um identificador de arquivo no executável; se o executável foi sobrescrito / atualizado / excluído, isso retornará dados diferentes, sem certeza se isso é um problema legítimo ou um recurso inesperado.

Eu tentei um pouco (usando debug / macho ), mas não consigo ver uma maneira de fazer com que esta plataforma cruzada funcione, estou construindo no macOS e o GNU binutils instalado parece corromper o mach-o-x86-64 file (isso pode ser apenas minha falta de compreensão da estrutura de mach-o e muito tempo desde que eu sequer olhei para objcopy ).

Outro problema é que ele depende da abertura de um identificador de arquivo no executável

Tenho certeza de que o carregador do programa irá (ou poderia) carregar a seção de recursos na memória, portanto, não há necessidade de usar pacotes de depuração. Embora o acesso aos dados exija muito mais ajustes nos arquivos de objetos do que realmente vale a pena.

Por que não seguir o que funciona - por exemplo, como Java faz isso. Eu exigiria que as coisas fossem um grande go-ish, mas algo nas linhas:

  • crie um arquivo go.res ou modifique go.mod para apontar para o diretório onde os recursos estão
  • todos os arquivos deste diretório são incluídos automaticamente, sem exceções pelo compilador no executável final
  • language fornece uma API semelhante a um caminho para acessar esses recursos

A compactação, etc. deve estar fora do escopo deste pacote de recursos e pode ter até // go:generate scripts, se necessário.

Alguém já olhou para markbates / pkger ? É uma solução bastante simples de usar go.mod como o diretório de trabalho atual. Supondo que index.html seja incorporado, abri-lo seria pkger.Open("/index.html") . Acho que é uma ideia melhor do que codificar um diretório static/ no projeto.

Também vale a pena mencionar que Go não tem nenhum requisito de estrutura significativo para um projeto, tanto quanto eu pude ver. go.mod é apenas um arquivo e muitas pessoas nunca usam vendor/ . Eu pessoalmente não acho que um diretório static/ seria bom.

Como já temos uma maneira de injetar dados (embora limitados) em uma compilação por meio da sinalização de link ldflags -X importpath.name=value , esse caminho de código poderia ser ajustado para aceitar -X importpath.name=@filename para injetar dados externos arbitrários?

Sei que isso não cobre todos os objetivos declarados do problema original, mas como uma extensão da funcionalidade -X , isso parece um avanço razoável?

(E se isso funcionar, estender a sintaxe go.mod como uma maneira mais limpa de especificar valores de ldflags -X é um próximo passo razoável?)

É uma ideia muito interessante, mas estou preocupado com as implicações de segurança.

É muito comum fazer -X 'pkg.BuildVersion=$(git rev-parse HEAD)' , mas não queremos deixar que o.mod execute comandos arbitrários, não é? (Eu acho que go generate sim, mas isso não é algo que você normalmente executa para pacotes OSS baixados.) Se go.mod não puder lidar com isso, ele acaba perdendo um caso de uso principal, então ldflags ainda seriam muito comuns.

Depois, há o outro problema de garantir que @filename não seja um link simbólico para / etc / passwd ou qualquer outra coisa.

O uso do vinculador impede o suporte para WASM e, possivelmente, outros destinos que não usam um vinculador.

Com base na discussão aqui, @bradfitz e eu trabalhamos em um design que fica em algum lugar no meio das duas abordagens consideradas acima, levando o que parece ser o melhor de cada uma. Publiquei um rascunho do documento, vídeo e código de design (links abaixo). Em vez de comentários sobre este problema, use as perguntas e respostas do Reddit para comentários sobre este projeto de rascunho específico - o Reddit encadeia e dimensiona as discussões melhor do que o GitHub. Obrigado!

Vídeo: https://golang.org/s/draft-embed-video
Design: https://golang.org/s/draft-embed-design
Perguntas e respostas: https://golang.org/s/draft-embed-reddit
Código: https://golang.org/s/draft-embed-code

@rsc Na minha opinião, a proposta go: embed é inferior a fornecer execução de código Go em sandbox _universal_ em tempo de compilação, que incluiria a leitura de arquivos e a transformação de dados lidos em um _formato ideal_ mais adequado para consumo em tempo de execução.

@atomsymbol Isso soa como algo muito fora do escopo deste problema.

@atomsymbol Isso soa como algo muito fora do escopo deste problema.

Estou ciente disso.

Eu li a proposta e fiz a varredura do código, mas não consegui encontrar uma resposta para isto: Este esquema de incorporação conterá informações sobre o arquivo no disco (~ os.Stat)? Ou esses carimbos de data / hora serão redefinidos para o tempo de construção? De qualquer forma, essas informações são peças úteis que são usadas em vários lugares, por exemplo, podemos enviar um 304 para ativos inalterados com base nisso.

Obrigado!

Editar: encontrei no tópico do reddit.

O tempo de modificação para todos os arquivos incorporados é o tempo zero, exatamente para as questões de reprodutibilidade que você listou. (Os módulos nem mesmo registram os tempos de modificação, novamente pelo mesmo motivo.)

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

De qualquer forma, essas informações são peças úteis que são usadas em vários lugares, por exemplo, podemos enviar um 304 para ativos inalterados com base nisso.

Um cabeçalho ETag baseado no hash de dados do arquivo resolveria esse problema sem precisar saber nada sobre as datas. Mas isso teria que ser conhecido por http.HandlerFS ou algo assim para poder funcionar e não desperdiçar recursos teria que ser feito apenas uma vez por arquivo.

Mas isso teria que ser conhecido por http.HandlerFS ou algo assim para poder funcionar e não desperdiçar recursos teria que ser feito apenas uma vez por arquivo.

Como o http.HandlerFS saberia que o fs.FS era imutável? Deve haver uma interface opcional IsImmutable() bool ?

Como o http.HandlerFS saberia que o fs.FS era imutável? Deve haver uma interface opcional IsImmutable() bool ?

Não quero entrar em detalhes de implementação porque não sou o designer dessas coisas, mas http.HandlerFS poderia verificar se é um tipo embed.FS e agir sobre isso como um caso especial, não acho que alguém queira expanda a API FS agora. Também pode haver um argumento de opção para HandlerFS especificamente para instruí-lo a tratar um sistema de arquivos como imutável. Além disso, se isso for feito na inicialização do aplicativo e todos os ctime / mtime tiverem valor zero, o handlerFS pode usar essa informação para "saber" que o arquivo não foi alterado, mas também existem sistemas de arquivos que podem não ter mtime ou tê-lo desativado. pode haver problemas lá também.

Eu não estava assistindo os comentários sobre esse assunto.

@atomsymbol bem-vindo de volta! É ótimo ver você comentando aqui novamente.
Eu concordo em princípio que, se tivéssemos sandbox, muitas coisas seriam mais fáceis.
Por outro lado, muitas coisas podem ser mais difíceis - as compilações podem nunca terminar.
De qualquer forma, definitivamente não temos esse tipo de sandbox hoje. :-)

@kokes não tenho certeza sobre os detalhes,
mas garantiremos a veiculação de um embed. Arquivos sobre HTTP obtêm ETags corretas por padrão.

Eu fiz o pedido # 41191 por aceitar o rascunho do projeto postado em julho.
Vou encerrar esta edição substituída por outra.
Obrigado pela grande discussão preliminar aqui.

Esta página foi útil?
0 / 5 - 0 avaliações