Stas Afanasyev. Juno Tuberías basadas en io.Reader / io.Writer. Parte 2

En la charla, hablaremos sobre el concepto de io.Reader / io.Writer, por qué son necesarios, cómo implementarlos correctamente y qué inconvenientes existen a este respecto, así como sobre la construcción de tuberías basadas en implementaciones estándar y personalizadas de io.Reader / io.Writer .



Stas Afanasyev. Juno Tuberías basadas en io.Reader / io.Writer. Parte 1

Error "en confianza"


Otro matiz: en esta implementación hay un "bagul". Este error es confirmado por los desarrolladores (les escribí al respecto). ¿Quizás alguien sabe qué es este "bagul"? En la diapositiva está la penúltima línea:



está asociada con demasiada confianza en el Reader envuelto: si Reader devuelve un número negativo de bytes, entonces aumenta el límite que nos gustaría obtener por el número de bytes restados. Y en algunos casos, este es un error bastante grave que no puedes entender de inmediato.

Escribí en el número: ¡hagamos algo, arreglemoslo! Y luego se reveló una capa de problemas ... Primero, me dijeron que si agrega este cheque ahora aquí, tendrá que agregar este cheque en todas partes, y hay una docena de estos lugares. Si queremos cambiar esto al lado del cliente, entonces necesitamos determinar una serie de reglas mediante las cuales el cliente validará los datos (y también puede haber cinco o dos de ellos). Resulta que todo esto necesita ser copiado.

Estoy de acuerdo en que esto no es óptimo. ¡Entonces veamos una versión consistente! ¿Por qué tenemos una implementación de la biblioteca estándar que no confía en nada, mientras que otras confían absolutamente en todo?

En general, mientras escribía mi opinión cívica, pensándolo bien, cerramos el tema con comentarios: “No haremos nada. Adiós"! Me hicieron parecer una especie de tonto ... Cortésmente, por supuesto, no puedes encontrar la culpa.

En general, ahora tenemos un problema. Consiste en el hecho de que no está claro quién debe validar los datos del Reader envuelto. O el cliente, o confiamos completamente en el contrato ... ¡Tenemos una solución! Si queda tiempo, lo contaré.

Pasemos al siguiente caso.

Teereader


Observamos un ejemplo de cómo envolver datos de Reader. El siguiente ejemplo de canalización es superar los datos de Reader en Writer. Hay dos situaciones

Primera situacion. Necesitamos leer los datos de Reader, copiarlos de alguna manera en Writer (de manera transparente) y trabajar con ellos como con Reader. Hay una implementación de TeeReader para esto. Se presenta en el fragmento de implementación superior:



funciona como el equipo Tee en Unix. Creo que muchos de ustedes han escuchado sobre esto.
Tenga en cuenta que esta implementación verifica el número de bytes que lee del Reader envuelto. Ver las condiciones en la segunda línea? Porque cuando escribe tal implementación, es intuitivamente clara: en caso de un número negativo, se asustará. ¡Y este es otro lugar donde confiamos en el lector envuelto! Les recuerdo que estas son todas las bibliotecas estándar.

Pasemos a un caso, por ejemplo, cómo usarlo. ¿Qué haremos en el fragmento inferior? Descargaremos el archivo robot.txt de golang.org usando el cliente http estándar.

Como sabe, el cliente http nos devuelve una estructura de respuesta, en la que el campo Cuerpo es una implementación de la interfaz del Lector. Debe aclararse diciendo que esta es una implementación de la interfaz ReadCloser. Pero ReadCloser es solo una interfaz construida desde Reader y Closer. Es decir, este es un Reader, que, en general, puede cerrarse.

En este ejemplo (en el fragmento inferior) recopilamos TeeReader, que leerá los datos de este Cuerpo y los escribirá en un archivo. La creación del archivo hoy, desafortunadamente, se mantuvo detrás de escena, porque no todo encajaba. Pero, de nuevo, si observa el dendrograma, el tipo de archivo implementa la interfaz de Writer, es decir, podemos escribir en él. Es obvio.

Armamos nuestro TeeReader y lo leímos usando ReadAll. Todo funciona como se esperaba: restamos el Cuerpo resultante, lo escribimos en un archivo y lo vemos en Assad.

Manera principiante


La segunda situación. Solo necesitamos leer los datos de Reader y escribirlos en Writer. La solución es obvia ...

Cuando recién comencé a trabajar con Go, resolví problemas como en una diapositiva:



localicé el búfer, lo llené con datos de Reader y transfirí el segmento lleno a Writer. Todo es simple

Dos puntos. En primer lugar, no hay garantía de que todo el Reader se reste en una llamada al método de lectura, ya que pueden quedar datos (en el buen sentido, esto debe hacerse en un bucle).

El segundo punto es que este camino no es óptimo. Aquí hay un código bastante repetitivo escrito antes que nosotros.

Para esto, hay una familia especial de ayudantes en la biblioteca estándar: estos son Copy, CopyN y CopyBuffer.

io.Copia. WriterTo y ReaderFrom


io.Copy básicamente hace lo que estaba en la diapositiva anterior: asigna un búfer predeterminado de 32 KB y escribe datos del lector al escritor (la firma de esta copia se muestra en el fragmento superior):



además de esta rutina de plantilla, también contiene Una serie de optimizaciones difíciles. Y antes de hablar sobre estas optimizaciones, necesitamos familiarizarnos con dos interfaces más:

  • WriterTo;
  • Leer de.

Situación hipotética. Su lector funciona con un búfer de memoria. Ya lo ha reubicado, escribe, lee algo desde allí, es decir, un lugar debajo ya ha sido reubicado. Desea leer este lector en algún lugar desde el exterior.

Ya hemos visto cómo sucede esto: se crea un búfer, se pasa el búfer, que se pasa al método de lectura; El lector, que funciona con memoria, lo tira de la pieza replicada ... Pero esto ya no es óptimo: el lugar ha sido reposicionado. ¿Por qué hacerlo de nuevo?



En algún lugar hace 5-6 años (hay un enlace a la lista de cambios) se hicieron dos interfaces: WriteTo y ReadFrom, que se implementan localmente. Reader implementa WriteTo, y Writer implementa ReadFrom. Resulta que Reader, que tiene un segmento con datos ya replicados, puede evitar una ubicación adicional y aceptar los métodos Write To Writer y pasar un búfer disponible dentro.

Así es como funciona la implementación de bytes.Buffer y bufio. Y si vuelve a mirar el dendrograma, verá que estas dos interfaces no son muy populares. Solo se implementan para aquellos tipos que funcionan con el búfer interno, donde la memoria ya está reubicada. Esto no lo ayudará a evitar la elocuencia cada vez, pero solo si ya está trabajando con una pieza reubicada.

ReaderFrom funciona de manera similar (solo lo implementa Writer). ReaderFrom lee todo el Reader, lo que se presenta como un argumento (antes de EOF) y escribe en algún lugar de la implementación interna de Writer.

Implementación de CopyBuffer


Este fragmento muestra la implementación del ayudante copyBuffer. Este copyBuffer no exportable se utiliza bajo el capó de io.Copy, CopyN y CopyBuffer.

Y aquí hay un pequeño matiz que vale la pena mencionar. CopyN ha sido optimizado recientemente, desatado de esta lógica. Esta es exactamente la optimización de la que hablé antes: antes de crear un búfer adicional de 32 KB, se realiza una comprobación, ¿tal vez la fuente de datos implementa la interfaz WriterTo, y este búfer adicional no es necesario?

Si esto no sucede, verificamos: ¿quizás Writer implementa ReaderFrom para conectarlos sin este intermediario? Si esto no sucede, la última esperanza permanece: ¿tal vez nos dieron algún tipo de búfer reubicado que podríamos usar?



Así es como funciona io.Copy.

Hay un problema, que es una semi-propuesta, un semi-error: no está claro qué. Lleva colgado un año y medio. Suena así: CopyBuffer es semánticamente incorrecto.

Desafortunadamente, no hay firma para este copyBuffer, pero se ve exactamente como este método no exportable.

Cuando llame a copyBuffer con la esperanza de evitar una ubicación adicional, pase un byte de rebanada reubicado allí, la siguiente lógica funciona: si Reader o Writer tienen las interfaces WriterTo y ReaderFrom, entonces no hay garantía de que pueda evitar esta ubicación. Esto fue aceptado como una propuesta y prometió pensarlo en Go 2.0. Por ahora, solo necesitas saber.

Trabaja con io.Pipe. PipeReader y pipeWriter


Otro caso: necesita obtener datos de Writer de alguna manera en Reader. Bonito caso de vida.

Imagine que ya tiene algunos datos, implementan la interfaz Reader, todo está claro con esto. Debe comprimir estos datos, "ajustarlos" y enviarlos a S3. ¿Cuál es el matiz? ..
Quien trabajó con el tipo gzip en el paquete compess sabe que el gzip'er en sí mismo es solo un proxy: toma datos en sí mismo, implementa la interfaz del Escritor, escribe los datos, algo les hará y entonces tengo que dejarlos caer en alguna parte. En el constructor, se necesita una implementación de la interfaz de Writer.

En consecuencia, aquí necesitamos algún tipo de Escritor intermedio, donde eliminaremos los datos ya comprimidos que se archivan en la primera etapa. Nuestro siguiente paso es subir estos datos a S3. Y el cliente estándar de AWS acepta la interfaz io.Reader como fuente de datos.



La diapositiva muestra la canalización: muestra cómo se ve: necesitamos superar el adelanto de datos de Reader a Writer, de Writer a Reader. ¿Cómo hacerlo?

La biblioteca estándar tiene una característica genial: io.Pipe. Devuelve dos valores: pipeReader y pipeWriter. Este par está inextricablemente vinculado. Imagine un "teléfono para bebés" en tazas con cuerdas: no tiene sentido hablar en una taza mientras nadie está escuchando en el otro extremo ...



¿Qué hace este io.Pipe? No se leerá hasta que nadie escriba los datos. Y viceversa, no escribirá nada hasta que nadie lea estos datos en el otro extremo. Aquí hay un ejemplo de implementación:



haremos lo mismo aquí. Leeremos el archivo robot.txt, que se leyó antes, lo comprimiremos usando nuestro gzip y lo enviaremos a S3.

  • En la primera línea, se crea un par: pipeReader, pipeWriter. A continuación, debemos ejecutar al menos una rutina, que leerá los datos de un extremo (una especie de tubería). En este gorutin, ejecute el cargador con una fuente de datos (source - pipeReader).
  • En el siguiente paso, necesitamos comprimir los datos. Comprimimos los datos y los escribimos en pipeWriter (será el otro extremo de la tubería), y ya ejecutándose goroutine recibe los datos en el otro extremo de la tubería y los lee. Cuando todo este sándwich esté listo, todo lo que queda es prender fuego a la mecha ...
  • Ver: io.Copy en la última línea escribe datos del Cuerpo en el gzip que creamos (es decir, de Lector a Escritor). Todo esto funciona como se esperaba.

Este ejemplo se puede resolver de otra manera. Si usa alguna implementación que implemente tanto Reader como Writer. Primero escribirá datos en él y luego los leerá.
Fue una demostración clara de cómo trabajar con io.Pipe.

Otras implementaciones


Eso es básicamente todo para mí. Llegamos a implementaciones interesantes de las que me gustaría hablar.



No dije nada sobre MultiReader, ni sobre MultiWriter. Y esta es otra implementación genial de la biblioteca estándar, que le permite conectar diferentes implementaciones. Por ejemplo, MultiWriter escribe en todos los escritores simultáneamente, y MultiReader lee los lectores secuencialmente.

Otra implementación se llama limio. Le permite establecer un límite para la resta. Puede establecer la velocidad en bytes por segundo a la que debe leer su Reader.

Otra implementación interesante es solo una visualización del progreso de la lectura: la barra de progreso (de algún tipo). Se llama ioprogreso.

¿Por qué dije todo esto? ¿Qué quise decir con eso?



  • Si de repente necesita implementar las interfaces Reader y Writer, hágalo bien. Todavía no hay una decisión única sobre quién es responsable de la implementación: asumiremos que todos confían en el contrato. Por lo tanto, debe cumplir impecablemente.
  • Si su caso funciona con un búfer reposicionado, no se olvide de las interfaces ReaderFrom y WriterTo.
  • Si está en un callejón sin salida y necesita ejemplos: consulte la biblioteca estándar, hay muchas implementaciones interesantes en las que puede confiar. Hay documentación allí.
  • Si algo es completamente incomprensible para usted, no dude en escribir sus problemas. Los chicos allí son adecuados, responden rápidamente, muy amablemente y le ayudan de manera competente.



Eso es todo para mí. ¡Gracias por venir!

Preguntas


Pregunta de la audiencia (B): Tengo una pregunta simple, supongo. Cuéntenos sobre algunos casos de uso de la vida: ¿cuáles fueron utilizados y por qué? Dijiste que Reader / Writer devuelve la longitud que leyó. ¿Alguna vez has tenido algún problema con esto? ¿Cuándo exigiste leer (no solo ReadAll existe), pero algo no funcionó?

SA: - Debo admitir honestamente que nunca tuve tales casos, porque siempre trabajé con implementaciones de la biblioteca estándar. Pero hipotéticamente, tal situación, por supuesto, es posible. En cuanto a casos específicos, a menudo recolectamos tuberías multicapa, y si hipotéticamente permite tal error, toda la tubería se vendrá abajo ...

P:- Esto no es del todo un error. Entonces, te cuento sobre mi pequeña experiencia. Tuve un problema con Booking.com: utilizaron el controlador que escribí y tuvieron un problema, algo no funcionaba. Hay un protocolo binario estándar que hicimos; localmente, todo funciona bien, todos están bien, pero resultó que tienen una red muy mala con un centro de datos. Entonces, Reader realmente no devolvió todo (tarjetas de red defectuosas, algo más).

CA: - Pero si no devolvió todo, entonces no debería haber devuelto el signo del final (final), y el cliente debería haber regresado. Según el contrato que se describe, Reader no debería ... Digamos que Reader, por supuesto, decide cuándo quiere venir, cuándo no quiere, sin embargo, si quiere leer todo, debe esperar a EOF.

A:"Pero eso es precisamente por la conexión". Este es exactamente el problema que ocurrió en el paquete neto estándar.

CA: - ¿Y devolvió el EOF?

P: - No devolvió todo, simplemente no lo leyó todo. Le dije: "Lee los siguientes 20 bytes". El Lee. Y no leo todo.

SA: - Hipotéticamente, esto es posible, porque es solo una interfaz que describe un protocolo de comunicación. Es necesario mirar y desmontar específicamente el caso. Aquí solo puedo responderte que el cliente, en teoría, debería haber regresado si no recibió todo lo que quería. Le pediste una porción de 20 bytes, te restó 15, pero EOF no vino - deberías ir de nuevo ...

P: - Hay io.ReadFull para esta situación. Está especialmente diseñado para leer el segmento hasta el final.

CALIFORNIA:- Si. No dije nada sobre ReadFull.

P: - Esta es una situación completamente normal cuando Leer no llena todo el segmento. Necesitas estar preparado para esto.

SA: - ¡Este es un caso muy esperado!

P: Gracias por el informe, fue interesante. Utilizo lectores en un proxy pequeño y simple que lee http y escribe al revés. Utilizo Close Reader para resolver un problema: cerrar lo que leo todo el tiempo. ¿Necesito confiar ciegamente en un contrato? Dijiste que podría haber problemas. ¿O agregar cheques adicionales? Es teóricamente posible que algo no venga completamente en este sitio. ¿Necesito hacer estos controles adicionales y no confiar en el contrato?

CALIFORNIA:- Diría esto: si su aplicación tolera estos errores (por ejemplo, si confía plenamente en el contrato), entonces tal vez no. Pero si no desea obtener un "pánico" en usted (como lo mostré en la lectura negativa en byte.Buffer), aún así lo comprobaría.
Pero esto depende de usted. ¿Qué puedo recomendarle? Creo que sopesar los pros y los contras. ¿Qué sucede si de repente obtienes un número negativo de bytes?

P: Gracias por el informe. Lamentablemente, no sé nada en Go. Si se ha producido un "pánico", ¿hay alguna forma de interceptar esta información y obtener información sobre qué, dónde, cómo ser parcial, para evitar problemas el viernes por la noche?

CA: Sí. El mecanismo de recuperación le permite "atrapar" un pánico y sacarlo sin caerse, en términos relativos.



A:- ¿Cómo sus recomendaciones para usar implementaciones de Writer y Reader son consistentes con los errores que se devuelven al implementar sockets web? No daré un ejemplo concreto, pero ¿siempre se usa el final del archivo allí? Por lo que recuerdo, el mensaje termina con algunos otros significados ...

SA: - Esta es una buena pregunta, porque no tengo nada que responder. Debo mirar! Si EOF no viene, entonces el cliente, si quiere obtener todo, debe volver a ir.

P: - ¿Cuánto tiempo pudo ensamblar la tubería? ¿Hay alguna creencia interna de que no vale la pena reunir a más de cinco participantes, o con ramas? ¿Cuánto tiempo logró construir un árbol a partir de estas tuberías (lectura, escritura)?

CALIFORNIA:- En mi práctica, unas cinco llamadas consecutivas son óptimas, porque es más difícil de depurar, tenga en cuenta lo que fluye y hacia dónde va. Se obtiene una estructura bastante ramificada. Pero yo diría que en algún lugar 5-7 máximo.

P: - 5-7 - ¿en este caso?

SA: - Esto es leer, por ejemplo, algunos datos. Debe prometer, y lo que inicia sesión, debe recortar. Prometido, luego lee estos datos, debe enviarlo de vuelta a algún almacenamiento (bueno, hipotéticamente). En cualquier almacenamiento que implemente la interfaz de Writer. Con esta tubería, se producen 5-6 pasos, aunque en uno de los pasos todavía se ramifica hacia un lado, y continúa trabajando con Reader.

A:- Según el modo Principiante, tenías una diapositiva interesante. ¿Puede indicar otros 2-3 puntos interesantes que estaban allí, pero ahora es mejor no hacerlo, sino hacerlo de manera diferente ahora?

SA: - Con esa diapositiva, quería mostrar exactamente cómo hacerlo sin la necesidad de leer Reader. Nunca se me pasó por la cabeza que algo así como la forma Principiante ... Este es probablemente el principal error, el patrón principal que debe evitarse al trabajar con lectores.
Presentador: - Agregaría por mi cuenta que es muy importante para un principiante leer toda la documentación del paquete io, en todas las interfaces que están allí, y comprenderlas. Porque, de hecho, hay muchos de ellos, y a menudo comienzas a hacer algo por tu cuenta, aunque ya existe allí y está implementado correctamente ("correcto", teniendo en cuenta todas las características).
Pregunta del líder: - ¿Cómo vivir más?

CA: - Buena pregunta! Prometí decir si tenemos tiempo. Como resultado de la discusión sobre el error, LimitedReader tomó la siguiente decisión: hacer un condón Reader en algún sentido, que proteja contra amenazas externas, envolver un Reader en el que no confía, para evitar que ingrese cualquier infección en su sistema.

Y en este lector, implementa todas las comprobaciones que no puede hacer: por ejemplo, lectura negativa, experimentos con el número de bytes (digamos que envió una porción de 10 bytes y obtuvo 15 de vuelta, ¿cómo reaccionar ante esto?) ... En este Reader y usted pueden implementar un conjunto de tales controles. Dije: "¿Quizás agreguemos a la biblioteca estándar, porque sería útil para que todos la usen"?

Me dieron la respuesta de que no parece tener sentido en esto: esto es algo simple que puede implementar usted mismo. Todas. Vivimos en Confiamos en el contrato chicos. Pero no confiaría.



P: - Cuando trabajamos con lectores, escritores y existe la oportunidad de encontrarnos con una "bomba" de gzip ... ¿Cuánto confiamos en ReadAll y WriteAll? ¿O, sin embargo, implementar la lectura del búfer y trabajar solo con el búfer?

CALIFORNIA:- ReadAll en sí solo utiliza bytes. Amortiguador bajo el capó. Cuando quiera usar esto o aquello, es recomendable que entre y vea cómo se implementan estas "tripas". De nuevo, depende de sus requisitos: si es intolerante con los errores que le mostré, debe ver si está marcado lo que proviene del Reader envuelto. Si no está marcado, use, por ejemplo, bufio (allí está todo marcado). O haga lo que acabo de decir: un cierto Lector proxy, que de acuerdo con su lista de requisitos verificará estos datos y los devolverá al cliente o los devolverá al cliente.




Un poco de publicidad :)


Gracias por estar con nosotros. ¿Te gustan nuestros artículos? ¿Quieres ver más materiales interesantes? Apóyenos haciendo un pedido o recomendando a sus amigos, VPS en la nube para desarrolladores desde $ 4.99 , un análogo único de servidores de nivel básico que inventamos para usted: toda la verdad sobre VPS (KVM) E5-2697 v3 (6 núcleos) 10GB DDR4 480GB SSD 1Gbps desde $ 19 o cómo dividir el servidor? (las opciones están disponibles con RAID1 y RAID10, hasta 24 núcleos y hasta 40GB DDR4).

Dell R730xd 2 veces más barato en el centro de datos Equinix Tier IV en Amsterdam? Solo tenemos 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TV desde $ 199 en los Países Bajos.Dell R420 - 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB - ¡desde $ 99! Lea sobre Cómo construir un edificio de infraestructura. clase c con servidores Dell R730xd E5-2650 v4 que cuestan 9,000 euros por un centavo?

All Articles