Stas Afanasyev. Juno. Pipelines baseados em io.Reader / io.Writer. Parte 1

No relatório, falaremos sobre o conceito de io.Reader / io.Writer, por que eles são necessários, como implementá-los corretamente e que armadilhas existem a esse respeito, bem como sobre a construção de pipelines com base em implementações padrão e personalizadas de io.Reader / io.Writer .



Stanislav Afanasyev (a seguir - SA): - Boa tarde! Meu nome é Stas. Eu vim de Minsk, da empresa Juno. Obrigado por ter vindo neste dia chuvoso, tendo encontrado forças para sair de casa.

Hoje, quero falar com você sobre um tópico como a construção de pipelines, com base nas interfaces io.Reader / io.Writer. O que falarei hoje é, em geral, o conceito de interfaces io.Reader / io.Writer, por que elas são necessárias, como usá-las corretamente e, mais importante, como implementá-las corretamente.

Também falaremos sobre a construção de pipelines com base em várias implementações dessas interfaces. Vamos falar sobre métodos existentes, discutir seus prós e contras. Vou mencionar várias armadilhas (isso será em abundância).

Antes de começarmos, devemos responder à pergunta: por que essas interfaces são necessárias? Levante as mãos, que trabalha com o Go com força (todos os dias, todos os dias) ...



Ótimo! Ainda temos uma comunidade Go. Eu acho que muitos de vocês trabalharam com essas interfaces, pelo menos já ouviram falar delas. Você pode nem saber sobre eles, mas certamente deveria ter ouvido algo sobre eles.

Antes de tudo, essas interfaces são uma abstração da operação de entrada e saída em todas as suas manifestações. Em segundo lugar, é uma API muito conveniente que permite criar pipelines, como um construtor a partir de cubos, sem realmente pensar nos detalhes internos da implementação. Pelo menos isso foi originalmente planejado.

io.Reader


Esta é uma interface muito simples. Consiste em apenas um método - o método Read. Conceitualmente, a implementação da interface io.Reader pode ser uma conexão de rede - por exemplo, onde ainda não há dados, mas eles podem aparecer lá:



pode ser um buffer na memória onde os dados já existem e podem ser lidos inteiramente. Também pode ser um descritor de arquivo - podemos ler esse arquivo em partes, se for muito grande.

A implementação conceitual da interface io.Reader é o acesso a alguns dados. Todos os casos que escrevi são suportados pelo método Read. Ele tem apenas um argumento - este é um byte de fatia.
Um ponto a ser destacado aqui. Aqueles que vieram recentemente ao Go ou vieram de alguma outra tecnologia em que não havia API semelhante (eu sou um deles), essa assinatura é um pouco confusa. O método Read parece, de alguma forma, ler esta fatia. De fato, o oposto é verdadeiro: a implementação da interface Reader lê os dados internos e preenche essa fatia com os dados que essa implementação possui.

A quantidade máxima de dados que podem ser lidos mediante solicitação pelo método Read é igual ao tamanho dessa fatia. Uma implementação regular retorna o máximo de dados possível no momento da solicitação ou a quantidade máxima que se encaixa nessa fatia. Isso sugere que o Reader pode ser lido em pedaços: pelo menos, byte, pelo menos dez - como você quiser. E o cliente que chama o Reader, de acordo com os valores de retorno do método Read, pensa em como viver.

O método Read retorna dois valores:

  • número de bytes subtraídos;
  • um erro se ocorreu.

Esses valores influenciam o comportamento adicional do cliente. Há um gif no slide que mostra, exibe esse processo, que acabei de descrever:





Io.Reader - Como?


Existem exatamente duas maneiras de seus dados satisfazerem a interface do Reader.



O primeiro é o mais simples. Se você possui algum tipo de fatia de byte e deseja satisfazê-la com a interface do Reader, pode implementar a biblioteca padrão que já satisfaz essa interface. Por exemplo, Reader do pacote de bytes. No slide acima, você pode ver a assinatura de como este leitor é criado.

Existe uma maneira mais complicada - de implementar você mesmo a interface do Reader. Existem aproximadamente 30 linhas na documentação com regras complicadas, restrições que devem ser seguidas. Antes de falarmos sobre todos eles, ficou interessante para mim: “E em que casos não há implementações padrão suficientes (biblioteca padrão)? Quando é o momento em que precisamos implementar a interface do Reader?

Para responder a essa pergunta, peguei os milhares de repositórios mais populares no Github (pelo número de estrelas), os adicionei e encontrei todas as implementações da interface do Reader lá. No slide, tenho algumas estatísticas (categorizadas) de quando as pessoas implementam essa interface.

  • A categoria mais popular são as conexões. Esta é uma implementação de protocolos proprietários e wrappers para tipos existentes. Portanto, Brad Fitzpatrick tem um projeto Camlistore - há um exemplo na forma de statTrackingConn, que, em geral, é um Wrapper comum sobre o tipo con do pacote net (adiciona métricas a esse tipo).
  • A segunda categoria mais popular são os buffers personalizados. Aqui gostei do primeiro exemplo: dataBuffer do pacote x / net. Sua peculiaridade é que ele armazena dados cortados em pedaços e, ao subtraí-los, passa por esses pedaços. Se os dados no pedaço terminarem, eles passarão para o próximo pedaço. Ao mesmo tempo, ele leva em consideração o comprimento, o local em que pode preencher a fatia transmitida.
  • Outra categoria são todos os tipos de barras de progresso, contando o número de bytes subtraídos com o envio de métricas ...

Com base nesses dados, podemos dizer que a necessidade de implementar a interface io.Reader ocorre com bastante frequência. Vamos então começar a falar sobre as regras que estão na documentação.

Regras de documentação


Como eu disse, a lista de regras e, em geral, a documentação é bastante grande, massiva. 30 linhas é suficiente para uma interface que consiste em apenas três linhas.

A primeira regra mais importante diz respeito ao número de bytes retornados. Ele deve ser estritamente maior ou igual a zero e menor ou igual ao comprimento da fatia enviada. Por que isso é importante?



Como esse é um contrato bastante rigoroso, o cliente pode confiar na quantia proveniente da implementação. Existem Wrappers na biblioteca padrão (por exemplo, bytes.Buffer e bufio). Existe um momento na biblioteca padrão: algumas implementações confiam em leitores embrulhados, outras não (nós falaremos sobre isso mais tarde).

Bufio não confia em nada - verifica absolutamente tudo. Bytes.Buffer confia absolutamente tudo o que chega até ele. Agora vou demonstrar o que está acontecendo em conexão com isso ...

Vamos agora considerar três casos possíveis - esses são três leitores implementados. Eles são bastante sintéticos, úteis para a compreensão. Leremos todos esses leitores usando o auxiliar ReadAll. Sua assinatura é apresentada na parte superior do slide:



io.Leitor # 1. Exemplo 1


O ReadAll é um auxiliar que utiliza algum tipo de implementação da interface do Reader, lê tudo e retorna os dados que leu, além de um erro.

Nosso primeiro exemplo é o Reader, que sempre retornará -1 e nulo como um erro, ou seja, um NegativeReader. Vamos executá-lo e ver o que acontece:



como você sabe, o pânico sem motivo é um sinal de tolice. Mas quem neste caso é um tolo - eu ou byte.Buffer - depende do ponto de vista. Quem escreve este pacote e o segue tem pontos de vista diferentes.

O que aconteceu aqui? O Bytes.Buffer aceitou um número negativo de bytes, não verificou se era negativo e tentou cortar o buffer interno ao longo do limite superior que ele recebeu - e saímos dos limites da fatia.

Existem dois problemas neste exemplo. A primeira é que a assinatura não é proibida para retornar números negativos e a documentação é proibida. Se a assinatura tivesse Uint, obteríamos um estouro clássico (quando um número assinado for interpretado como não assinado). E esse é um bug muito complicado, que certamente acontecerá na sexta à noite, quando você já estiver em casa. Portanto, o pânico neste caso é a opção preferida.

O segundo "ponto" é que o rastreamento da pilha não entende o que aconteceu. É claro que fomos além dos limites da fatia - e daí? Quando você possui um canal com várias camadas e ocorre um erro, não fica claro imediatamente o que aconteceu. Portanto, o bufio da biblioteca padrão também entra em pânico nessa situação, mas o faz de maneira mais bonita. Ele imediatamente escreve: “Subtraí um número negativo de bytes. Não farei mais nada - não sei o que fazer com isso. "

E bytes.Buffer está em pânico o melhor que pode. Publiquei um problema no Golang pedindo que eu adicionasse um erro humano. No terceiro dia, discutimos as perspectivas dessa decisão. A razão é a seguinte: historicamente aconteceu que pessoas diferentes, em momentos diferentes, tomaram decisões descoordenadas diferentes. E agora temos o seguinte: em um caso, não confiamos na implementação (verificamos tudo) e no outro, confiamos completamente, não obtemos o que vem daí. Este é um problema não resolvido, e falaremos mais sobre isso.

io.Leitor # 1. Exemplo 2


A seguinte situação: nosso Reader sempre retornará 0 e nulo como resultados. Do ponto de vista dos contratos, tudo é legal aqui - não há problemas. A única ressalva: a documentação diz que as implementações não são recomendadas para retornar os valores 0 e nulo, além do caso, quando o comprimento da fatia enviada for zero.

Na vida real, esse leitor pode causar muitos problemas. Então, voltando à pergunta, devemos confiar no Reader? Por exemplo, uma verificação é incorporada ao bufio: ele lê sequencialmente o Reader exatamente 100 vezes - se esse par de valores é retornado 100 vezes, ele simplesmente retorna NoProgress.

Não há nada como isso em bytes.Buffer. Se executarmos este exemplo, obteremos apenas um loop infinito (ReadAll usa bytes.Buffer sob o capô, não o próprio Reader):



io.Leitor # 1. Exemplo 2


Mais um exemplo. Também é bastante sintético, mas útil para a compreensão:



aqui sempre retornamos 1 e zero. Parece que também não há problemas aqui - tudo é legal do ponto de vista do contrato. Há uma nuance: se eu executar este exemplo no meu computador, ele congelará após 30 segundos ...

Isso ocorre porque o cliente que lê este Reader (ou seja, bytes.Buffer) nunca recebe um sinal do final dos dados - ele lê, subtrai ... Além disso, ele recebe um byte subtraído a cada vez. Para ele, isso significa que, em algum momento, o buffer reposicionado termina, ele ainda corre - a situação se repete e corre para o infinito até explodir.

io.Leitor # 2. Retorno de erro


Chegamos à segunda regra importante para implementar a interface do Reader - este é um retorno de erro. A documentação indica três erros que a implementação deve retornar. O mais importante deles é EOF.

EOF é o próprio sinal do final dos dados, que a implementação deve retornar sempre que os dados acabarem. Conceitualmente, isso geralmente não é um erro, mas é um erro.

Há outro erro chamado UnexpectedEOF. Se de repente, ao ler o Reader, não puder mais ler os dados, pensou-se que retornaria UnexpectedEOF. Mas, na verdade, esse erro é usado apenas em um local da biblioteca padrão - na função ReadAtLeast.



Outro erro é o NoProgress, sobre o qual já conversamos. A documentação diz o seguinte: este é um sinal de que a interface está implementada é uma porcaria.

Io.Reader # 3


A documentação estipula um conjunto de casos sobre como retornar corretamente o erro. Abaixo, você pode ver três casos possíveis:



Podemos retornar um erro com o número de bytes subtraídos e separadamente. Mas, de repente, seus dados acabam no Reader, e você não pode retornar o EOF [sinal final] agora (muitas implementações da biblioteca padrão funcionam assim), pressupõe-se que você retornará o EOF para a próxima chamada consecutiva (ou seja, você deve liberar) cliente).

Para o cliente, isso significa que não há mais dados - não me procure mais. Se você retornar nulo e o cliente precisar de dados, ele deverá procurá-lo novamente.

io.Reader. Erros


Em geral, segundo o Reader, essas eram as principais regras importantes. Ainda existe um conjunto de pequenos, mas eles não são tão importantes e não levam a uma situação assim:



antes de analisarmos tudo relacionado ao Reader, precisamos responder à pergunta: é importante, geralmente ocorrem erros em implementações personalizadas? Para responder a essa pergunta, eu virei para o meu spool para 1000 repositórios (e lá temos cerca de 550 implementações personalizadas). Eu olhei através dos primeiros cem com meus olhos. Claro, isso não é superanálise, mas o que é ...

identifiquei os dois erros mais populares:
  • nunca retorna EOF;
  • muita confiança no leitor embrulhado.

Novamente, este é um problema do meu ponto de vista. E daqueles que estão assistindo o pacote io, isso não é um problema. Vamos falar sobre isso novamente.

Gostaria de voltar a uma nuance. Consulte:



O cliente nunca deve interpretar o par 0 e zero como EOF. Isso é erro! Para o Reader, esse valor é apenas uma oportunidade de liberar o cliente. Então, os dois erros que mencionei parecem insignificantes, mas é suficiente imaginar que você tem um pipeline de várias camadas no produto e um pequeno e astuto "bagul" apareceu no meio, então a "batida subterrânea" não vai demorar muito - garantida!

Segundo o Reader, basicamente tudo. Essas eram as regras básicas de implementação.

io.Writer


No outro extremo dos pipelines, temos o io.Writer, que é onde costumamos escrever dados. Uma interface muito semelhante: também consiste em um método (Write), sua assinatura é semelhante. Do ponto de vista da semântica, a interface do Writer é mais compreensível: eu diria que, ao ser ouvida, está escrita.



O método Write pega um byte de fatia e o grava por inteiro. Ele também tem um conjunto de regras que devem ser seguidas.

  1. O primeiro deles diz respeito ao número retornado de bytes gravados. Eu diria que não é tão rigoroso, porque não encontrei um único exemplo quando isso levaria a algumas consequências críticas - por exemplo, entrar em pânico. Isso não é muito rigoroso porque existe a seguinte regra ...
  2. A implementação do Writer é necessária para retornar um erro sempre que a quantidade de dados gravados for menor que o que foi enviado. Ou seja, a gravação parcial não é suportada. Isso significa que não é muito importante quantos bytes foram gravados.
  3. Mais uma regra: o Writer não deve modificar a fatia enviada, porque o cliente ainda trabalhará com essa fatia.
  4. O Writer não deve conter essa fatia (o Reader tem a mesma regra). Se você precisar de dados em sua implementação para algumas operações, basta copiar este slide e pronto.



Por Reader e Writer, é isso.

Dendrograma


Especialmente para este relatório, gerei um gráfico de implementação e o projetei na forma de um dendograma. Quem quiser agora pode seguir este código QR:



Este dendrograma tem todas as implementações de todas as interfaces do pacote io. Esse dendrograma é necessário para simplesmente entender: o que e com o que você pode colar nos pipelines, onde e o que você pode ler, onde pode escrever. Ainda vou fazer referência a ele no decorrer do meu relatório, portanto, consulte o código QR.

Tubulações


Falamos sobre o que é o Reader, io.Writer. Agora vamos falar sobre a API que existe na biblioteca padrão para construção de pipelines. Vamos começar com o básico. Talvez nem seja interessante para ninguém. No entanto, isso é muito importante.

Leremos os dados do fluxo de entrada padrão (da Stdin):



Stdin é representado em Go por uma variável global do tipo file do pacote os. Se você der uma olhada no dendograma, notará que o tipo de arquivo também implementa as interfaces Reader e Writer.

No momento, estamos interessados ​​no Reader. Estaremos lendo o Stdin usando o mesmo auxiliar ReadAll que já usamos.

Uma nuance em relação a esse auxiliar é digna de nota: ReadAll lê o Reader até o fim, mas determina o final pelo EOF, de acordo com o sinal do final do qual falamos.
Agora, limitaremos a quantidade de dados que lemos do Stdin. Para fazer isso, há uma implementação do LimitedReader na biblioteca padrão:



gostaria que você prestasse atenção em como o LimitedReader limita o número de bytes a serem lidos. Alguém poderia pensar que esta implementação, este Wrapper, subtrai tudo o que está no Reader, o qual envolve, e depois fornece o quanto queremos. Mas tudo funciona um pouco diferente ...

LimitedReader corta a fatia fornecida como um argumento ao longo do limite superior. E ele passa essa fatia cortada para o Reader, que a envolve. Esta é uma demonstração clara de como o comprimento dos dados lidos é regulado nas implementações da interface io.Reader.

Erro ao retornar final do arquivo


Outro ponto interessante: observe como essa implementação retorna um erro de EOF! Os valores nomeados retornados são usados ​​aqui e são atribuídos pelos valores que obtemos do Reader empacotado.

E se houver mais dados no Reader empacotado do que o necessário, atribuímos os valores do Reader empacotado - por exemplo, 10 bytes e nulo - porque ainda existem dados no Reader empacotado. Mas a variável n, que diminui (na penúltima linha), diz que chegamos ao "fundo" - o fim do que precisamos.

Na próxima iteração, o cliente deve voltar novamente - na primeira condição, ele receberá EOF. Este é o caso que eu mencionei.

Para continuar em breve ...


Um pouco de publicidade :)


Obrigado por ficar com a gente. Você gosta dos nossos artigos? Deseja ver materiais mais interessantes? Ajude-nos fazendo um pedido ou recomendando aos seus amigos o VPS na nuvem para desenvolvedores a partir de US $ 4,99 , um analógico exclusivo de servidores de nível básico que foi inventado por nós para você: Toda a verdade sobre o VPS (KVM) E5-2697 v3 (6 núcleos) 10 GB DDR4 480 GB SSD 1 Gbps de US $ 19 ou como dividir o servidor? (as opções estão disponíveis com RAID1 e RAID10, até 24 núcleos e até 40GB DDR4).

Dell R730xd 2 vezes mais barato no data center Equinix Tier IV em Amsterdã? Somente nós temos 2 TVs Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TV a partir de US $ 199 na Holanda!Dell R420 - 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB - a partir de $ 99! Leia sobre Como criar um prédio de infraestrutura. classe c usando servidores Dell R730xd E5-2650 v4 que custam 9.000 euros por um centavo?

All Articles