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

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 .



Stanislav Afanasyev (en adelante - SA): - ¡Buenas tardes! Me llamo Stas Vengo de Minsk, de la compañía Juno. Gracias por venir en este día lluvioso, después de haber encontrado la fuerza para salir de la casa.

Hoy quiero hablar con ustedes sobre un tema como la construcción de tuberías Go basadas en las interfaces io.Reader / io.Writer. De lo que voy a hablar hoy es, en general, del concepto de interfaces io.Reader / io.Writer, por qué son necesarias, cómo usarlas correctamente y, lo más importante, cómo implementarlas correctamente.

También hablaremos sobre la construcción de tuberías basadas en diversas implementaciones de estas interfaces. Hablaremos sobre los métodos existentes, discutiremos sus ventajas y desventajas. Mencionaré varias trampas (esto será en abundancia).

Antes de comenzar, debemos responder la pregunta, ¿por qué se necesitan estas interfaces? Levanta las manos, que trabaja con Go con fuerza (todos los días, cada dos días) ...



¡Genial! Todavía tenemos una comunidad de Go. Creo que muchos de ustedes han trabajado con estas interfaces, al menos han oído hablar de ellas. Puede que ni siquiera sepas sobre ellos, pero ciertamente deberías haber escuchado algo sobre ellos.

En primer lugar, estas interfaces son una abstracción de la operación de entrada-salida en todas sus manifestaciones. En segundo lugar, es una API muy conveniente que le permite construir tuberías, como un constructor a partir de cubos, sin pensar realmente en los detalles internos de la implementación. Al menos eso fue originalmente pensado.

io.Reader


Esta es una interfaz muy simple. Consiste en un solo método: el método de lectura. Conceptualmente, la implementación de la interfaz io.Reader puede ser una conexión de red, por ejemplo, donde todavía no hay datos, pero pueden aparecer allí:



puede ser un búfer en la memoria donde los datos ya existen y se pueden leer por completo. También puede ser un descriptor de archivo: podemos leer este archivo en pedazos si es muy grande.

La implementación conceptual de la interfaz io.Reader es el acceso a algunos datos. Todos los casos que escribí son compatibles con el método Read. Solo tiene un argumento: este es el byte de corte.
Un punto para hacer aquí. Aquellos que vinieron a Go recientemente o vinieron de otra tecnología, donde no había una API similar (yo soy uno de esos), esta firma es un poco confusa. El método de lectura parece leer de alguna manera este segmento. De hecho, lo contrario es cierto: la implementación de la interfaz Reader lee los datos que contiene y llena este segmento con los datos que tiene esta implementación.

La cantidad máxima de datos que el método de lectura puede leer a pedido es igual a la longitud de este segmento. Una implementación regular devuelve tantos datos como puede devolver en el momento de la solicitud, o la cantidad máxima que cabe en este segmento. Esto sugiere que Reader puede leerse en partes: al menos por byte, al menos diez, como lo desee. Y el cliente que llama a Reader, de acuerdo con los valores de retorno del método Read, piensa cómo vivir.

El método Read devuelve dos valores:

  • número de bytes restados;
  • un error si ocurrió

Estos valores influyen en el comportamiento posterior del cliente. Hay un gif en la diapositiva que muestra, muestra este proceso, que acabo de describir:





Io.Reader - ¿Cómo hacerlo?


Hay exactamente dos formas para que sus datos satisfagan la interfaz del lector.



El primero es el más simple. Si tiene algún tipo de byte de corte y desea que satisfaga la interfaz del lector, puede implementar una biblioteca estándar que ya satisfaga esta interfaz. Por ejemplo, Reader del paquete de bytes. En la diapositiva anterior, puede ver la firma de cómo se crea este Reader.

Hay una forma más complicada: implementar la interfaz del lector usted mismo. Hay aproximadamente 30 líneas en la documentación con reglas difíciles, restricciones que deben seguirse. Antes de hablar sobre todos ellos, me resultó interesante: “¿Y en qué casos no hay suficientes implementaciones estándar (biblioteca estándar)? ¿Cuándo es el momento en que necesitamos implementar la interfaz del lector nosotros mismos?

Para responder a esta pregunta, tomé los miles de repositorios más populares en Github (por el número de estrellas), los agregué y encontré todas las implementaciones de la interfaz Reader allí. En la diapositiva, tengo algunas estadísticas (categorizadas) de cuándo las personas implementan esta interfaz.

  • La categoría más popular son las conexiones. Esta es una implementación de protocolos y envoltorios patentados para los tipos existentes. Entonces, Brad Fitzpatrick tiene un proyecto Camlistore: hay un ejemplo en forma de statTrackingConn, que, en general, es un Wrapper ordinario sobre el tipo de estafa del paquete neto (agrega métricas a este tipo).
  • La segunda categoría más popular son los buffers personalizados. Aquí me gustó el único ejemplo: dataBuffer del paquete x / net. Su peculiaridad es que almacena datos cortados en fragmentos, y al restarlos pasa a través de estos fragmentos. Si los datos en el fragmento han terminado, pasarán al siguiente fragmento. Al mismo tiempo, tiene en cuenta la longitud, el lugar en el que puede llenar el segmento transmitido.
  • Otra categoría es todo tipo de barras de progreso, contando el número de bytes restados con el envío de métricas ...

En base a estos datos, podemos decir que la necesidad de implementar la interfaz io.Reader ocurre con bastante frecuencia. Entonces comencemos a hablar sobre las reglas que están en la documentación.

Reglas de documentación


Como dije, la lista de reglas, y en general la documentación es bastante grande, masiva. 30 líneas son suficientes para una interfaz que consta de solo tres líneas.

La primera regla, la más importante, se refiere al número de bytes devueltos. Debe ser estrictamente mayor o igual que cero y menor o igual que la longitud del segmento enviado. ¿Por qué es importante?



Dado que este es un contrato bastante estricto, el cliente puede confiar en la cantidad que proviene de la implementación. Hay Wrappers en la biblioteca estándar (por ejemplo, bytes.Buffer y bufio). Hay un momento así en la biblioteca estándar: algunas implementaciones confían en los lectores envueltos, otros no confían (hablaremos de esto más adelante).

Bufio no confía en nada, verifica absolutamente todo. Bytes.Buffer confía absolutamente en todo lo que le llega. Ahora demostraré lo que está sucediendo en relación con esto ...

Ahora consideraremos tres casos posibles: estos son tres lectores implementados. Son bastante sintéticos, útiles para la comprensión. Leeremos todos estos lectores usando el ayudante ReadAll. Su firma se presenta en la parte superior de la diapositiva:



io.Reader # 1. Ejemplo 1


ReadAll es un ayudante que toma algún tipo de implementación de la interfaz de Reader, la lee y devuelve los datos que leyó, así como un error.

Nuestro primer ejemplo es Reader, que siempre devolverá -1 y cero como un error, es decir, un NegativeReader. Ejecútelo y veamos qué sucede:



como sabe, el pánico sin ninguna razón es una señal de tontería. Pero quién en este caso es tonto, yo o byte. Amortiguador, depende del punto de vista. Quienes escriben este paquete y lo siguen tienen diferentes puntos de vista.

¿Que pasó aquí? Bytes.Buffer aceptó un número negativo de bytes, no verificó que fuera negativo e intentó cortar el búfer interno a lo largo del límite superior, que recibió, y salimos de los límites del segmento.

Hay dos problemas en este ejemplo. La primera es que la firma no tiene prohibido devolver números negativos, y la documentación está prohibida. Si la firma tuviera Uint, obtendríamos un desbordamiento clásico (cuando un número con signo se interpreta como sin signo). Y este es un error muy complicado, que seguramente ocurrirá el viernes por la noche, cuando ya estés en casa. Por lo tanto, el pánico en este caso es la opción preferida.

El segundo "punto" es que el seguimiento de la pila no comprende lo que sucedió en absoluto. Está claro que hemos ido más allá de los límites del sector, ¿y qué? Cuando tiene una tubería multicapa y se produce un error de este tipo, no está claro de inmediato qué sucedió. Por lo tanto, el bufio de la biblioteca estándar también "entra en pánico" en esta situación, pero lo hace más bellamente. Inmediatamente escribe: “Resté un número negativo de bytes. No haré nada más, no sé qué hacer con él ".

Y bytes. Buffer está entrando en pánico lo mejor que puede. Publiqué un problema en Golang pidiéndome que agregue un error humano. El tercer día, discutimos las perspectivas de esta decisión. La razón es esta: históricamente sucedió que diferentes personas en diferentes momentos tomaron diferentes decisiones descoordinadas. Y ahora tenemos lo siguiente: en un caso no confiamos en absoluto en la implementación (verificamos todo), y en el otro confiamos completamente, no obtenemos lo que proviene de allí. Este es un problema no resuelto, y hablaremos más sobre esto.

io.Reader # 1. Ejemplo 2


La siguiente situación: nuestro lector siempre devolverá 0 y cero como resultado. Desde el punto de vista de los contratos, todo es legal aquí, no hay problemas. La única advertencia: la documentación dice que no se recomienda que las implementaciones devuelvan los valores 0 y nulo, además del caso, cuando la longitud del segmento enviado es cero.

En la vida real, tal lector puede causar muchos problemas. Entonces, volvemos a la pregunta, ¿debemos confiar en Reader? Por ejemplo, una verificación está integrada en el bufio: lee secuencialmente Reader exactamente 100 veces; si dicho par de valores se devuelve 100 veces, simplemente devuelve NoProgress.

No hay nada como esto en bytes. Si ejecutamos este ejemplo, obtenemos solo un bucle sin fin (ReadAll usa bytes.Buffer debajo del capó, no Reader en sí):



io.Reader # 1. Ejemplo 2


Un ejemplo mas. También es bastante sintético, pero útil para comprender:



aquí siempre devolvemos 1 y cero. Parece que tampoco hay problemas aquí: todo es legal desde el punto de vista del contrato. Hay un matiz: si ejecuto este ejemplo en mi computadora, se congelará después de 30 segundos ...

Esto se debe al hecho de que el cliente que lee este Reader (es decir, bytes.Buffer) nunca recibe una señal del final de los datos: se lee, resta ... Además, obtiene un byte restado cada vez. Para él, esto significa que en algún momento, el búfer reposicionado finaliza, todavía se ejecuta: la situación se repite y se ejecuta hasta el infinito hasta que explota.

io.Reader # 2. Error de retorno


Llegamos a la segunda regla importante para implementar la interfaz de Reader: este es un retorno de error. La documentación establece tres errores que la implementación debería devolver. El más importante de ellos es EOF.

EOF es el signo mismo del fin de los datos, que la implementación debería devolver siempre que se quede sin datos. Conceptualmente, esto no es, en general, un error, sino un error.

Hay otro error llamado UnexpectedEOF. Si de repente mientras lee Reader ya no puede leer los datos, se pensó que devolvería UnexpectedEOF. Pero, de hecho, este error se usa solo en un lugar de la biblioteca estándar: en la función ReadAtLeast.



Otro error es NoProgress, del que ya hablamos. La documentación lo dice: esta es una señal de que la interfaz está implementada es una mierda.

Io.Reader # 3


La documentación estipula un conjunto de casos sobre cómo devolver correctamente el error. A continuación puede ver tres casos posibles:



podemos devolver un error tanto con el número de bytes restados como por separado. Pero si de repente sus datos se agotan en su Reader, y no puede devolver el EOF [signo final] en este momento (muchas implementaciones de biblioteca estándar funcionan de esa manera), entonces se supone que devolverá EOF a la próxima llamada consecutiva (es decir, debe dejar ir cliente).

Para el cliente, esto significa que no hay más datos, ya no acudes a mí. Si devuelve nulo y el cliente necesita datos, entonces debe volver a usted.

io.Reader. Errores


En general, según Reader, estas fueron las principales reglas importantes. Todavía hay un conjunto de pequeños, pero no son tan importantes y no conducen a tal situación:



antes de pasar por todo lo relacionado con Reader, debemos responder a la pregunta: ¿es importante, ocurren errores a menudo en implementaciones personalizadas? Para responder a esta pregunta, recurrí a mi spool para obtener 1000 repositorios (y allí obtuvimos alrededor de 550 implementaciones personalizadas). Miré a través de los primeros cien con los ojos. Por supuesto, esto no es un superanálisis, sino lo que es ...

Identifiqué los dos errores más populares:
  • nunca devuelve EOF;
  • demasiada confianza en el lector envuelto.

Nuevamente, este es un problema desde mi punto de vista. Y de aquellos que están viendo el paquete io, esto no es un problema. Hablaremos de esto nuevamente.

Me gustaría volver a un matiz. Ver:



El cliente nunca debe interpretar el par 0 y cero como EOF. Esto es un error! Para Reader, este valor es solo una oportunidad para dejar ir al cliente. Por lo tanto, los dos errores que mencioné parecen insignificantes, pero es suficiente imaginar que tiene una tubería de varias capas en el producto y un pequeño y astuto "bagul" en el medio, ¡entonces el "golpe subterráneo" no tardará mucho, garantizado!

Según Reader, básicamente todo. Estas fueron las reglas básicas de implementación.

io.Writer


En el otro extremo de las tuberías, tenemos io.Writer, que es donde usualmente escribimos datos. Una interfaz muy similar: también consta de un método (escritura), su firma es similar. Desde el punto de vista de la semántica, la interfaz de Writer es más comprensible: diría que tal como se escucha, se escribe.



El método Write toma un byte de segmento y lo escribe en su totalidad. También tiene un conjunto de reglas que deben seguirse.

  1. El primero de estos se refiere al número devuelto de bytes escritos. Yo diría que no es tan estricto, porque no encontré un solo ejemplo cuando esto llevaría a algunas consecuencias críticas, por ejemplo, entrar en pánico. Esto no es muy estricto porque existe la siguiente regla ...
  2. La implementación de Writer es necesaria para devolver un error siempre que la cantidad de datos escritos sea inferior a la que se envió. Es decir, la grabación parcial no es compatible. Esto significa que no es muy importante cuántos bytes se escribieron.
  3. Una regla más: el escritor no debe modificar el segmento enviado, porque el cliente seguirá trabajando con este segmento.
  4. El escritor no debe contener esta porción (el lector tiene la misma regla). Si necesita datos en su implementación para algunas operaciones, solo necesita copiar esta diapositiva, y eso es todo.



Por lector y escritor, eso es todo.

Dendrograma


Especialmente para este informe, generé un gráfico de implementación y lo diseñé en forma de dendrograma. Aquellos que quieran ahora pueden seguir este código QR:



este dendrograma tiene todas las implementaciones de todas las interfaces del paquete io. Este dendrograma es necesario para comprender simplemente: qué y con qué se puede unir en las tuberías, dónde y qué puede leer, dónde puede escribir. Todavía lo consultaré en el curso de mi informe, así que por favor consulte el código QR.

Tuberías


Hablamos sobre qué es Reader, io.Writer. Ahora hablemos de la API que existe en la biblioteca estándar para construir tuberías. Empecemos con lo básico. Tal vez ni siquiera sea interesante para nadie. Sin embargo, esto es muy importante.

Leeremos los datos del flujo de entrada estándar (de Stdin):



Stdin está representado en Go por una variable global de tipo archivo del paquete os. Si observa el dendrograma, notará que el tipo de archivo también implementa las interfaces Reader y Writer.

En este momento estamos interesados ​​en Reader. Leeremos Stdin usando el mismo ayudante ReadAll que ya usamos.

Vale la pena señalar un matiz con respecto a este ayudante: ReadAll lee Reader hasta el final, pero determina el final por EOF, de acuerdo con el signo del final del que hablamos.
Ahora limitaremos la cantidad de datos que leemos de Stdin. Para hacer esto, hay una implementación de LimitedReader en la biblioteca estándar:



me gustaría que preste atención a cómo LimitedReader limita el número de bytes a leer. Uno podría pensar que esta implementación, este Wrapper, resta todo lo que está en el Reader, que envuelve, y luego da todo lo que queremos. Pero todo funciona un poco diferente ...

LimitedReader recorta el segmento dado como argumento a lo largo del límite superior. Y le pasa esta rebanada recortada a Reader, que la envuelve. Esta es una demostración clara de cómo se regula la longitud de los datos leídos en las implementaciones de la interfaz io.Reader.

Error al devolver el final del archivo


Otro punto interesante: ¡observe cómo esta implementación devuelve un error EOF! Los valores nombrados devueltos se usan aquí, y se asignan por los valores que obtenemos del Reader envuelto.

Y si sucede que hay más datos en el Reader envuelto de los que necesitamos, asignamos los valores del Reader envuelto, por ejemplo, 10 bytes y cero, porque todavía hay datos en el Reader envuelto. Pero la variable n, que disminuye (en la penúltima línea), dice que hemos llegado al "fondo", el final de lo que necesitamos.

En la próxima iteración, el cliente debe volver de nuevo; en la primera condición, recibirá EOF. Este es el caso que mencioné.

Continuará muy pronto ...


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