<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Corentin GS&apos;s Blog</title><description>I code. I write. I explore.</description><link>https://corentings.dev/</link><language>en</language><atom:link href="https://corentings.dev/atom.xml" rel="self" type="application/atom+xml"/><item><title>Tokyo in the Browser: City, Cognition, and Japanese Visual Culture</title><link>https://corentings.dev/blog/ux-japan-2/</link><guid isPermaLink="true">https://corentings.dev/blog/ux-japan-2/</guid><description>Why do Japanese websites feel dense? A cautious UX essay connecting Tokyo signage, visual culture, cognition research, and Japanese web design.</description><pubDate>Wed, 03 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Tokyo in the Browser: City, Cognition, and Japanese Visual Culture&lt;/h1&gt;
&lt;p&gt;Walk through Shinjuku Station at rush hour and the first impression is almost impossible to separate from the information surrounding you. Platform numbers, exit codes, train lines, arrows, warnings, shop signs, restaurant boards, campaign posters, ticket machines, and convenience-store shelves all compete for attention. And yet the system works. People scan, filter, compare, and move.&lt;/p&gt;
&lt;p&gt;Open a Japanese portal website and you&apos;ll see the same logic at work. The page behaves like a city compressed into a browser: a field of routes, signals, services, promotions, warnings, and contextual cues.&lt;/p&gt;
&lt;p&gt;In the previous essay, I argued that many Japanese websites are better understood as high-density context environments than as simple design failures. This article asks where that logic comes from. The answer lies outside the browser: in the streets, stations, shelves, magazines, and visual habits of Japan.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;A Japanese Website Is Not a Poster&lt;/h2&gt;
&lt;p&gt;Western landing pages often behave like advertisements. One dominant message. One focal object. One call to action. The design assumes the user arrives with a single intent, and the page&apos;s job is to guide that intent toward conversion.&lt;/p&gt;
&lt;p&gt;Many Japanese portals, service pages, and retail sites behave differently. They behave like maps, directories, counters, or flyers. Their job is to orient, not to persuade. They assume users arrive with questions, comparisons, and multiple possible next steps.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Poster-like page&lt;/th&gt;
&lt;th&gt;Map-like page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One dominant message&lt;/td&gt;
&lt;td&gt;Many visible destinations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linear persuasion&lt;/td&gt;
&lt;td&gt;Nonlinear navigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One primary CTA&lt;/td&gt;
&lt;td&gt;Multiple task paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low visual density&lt;/td&gt;
&lt;td&gt;Managed visual density&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brand mood first&lt;/td&gt;
&lt;td&gt;Context and service information first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you judge a map by poster rules, it will look like clutter. But a map follows different rules than a poster.&lt;/p&gt;
&lt;p&gt;Japanese web density is a different design logic, shaped by different environmental training.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The City as Interface&lt;/h2&gt;
&lt;p&gt;Tokyo functions like an interface. The city guides movement through layered navigation systems: color-coded train lines, numbered exits, floor maps, directional arrows, multilingual warnings, and retail prompts stacked at every decision point.&lt;/p&gt;
&lt;p&gt;Take Shinjuku Station. It is the world&apos;s busiest transport hub, handling over three million passengers daily. The station compresses dozens of train lines, hundreds of exits, shopping malls, offices, and restaurants into a single navigable structure. The density is not random. It is organized through redundancy: the same information appears in multiple formats (signs, maps, digital displays, audio announcements) so that travelers with different needs, languages, and attention states can all find their way.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-2-1.jpg&quot; alt=&quot;Layered signage and commercial density on a Tokyo street at night&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Japanese urban density is often vertical, layered, and sign-rich. A domestic portal page can feel less strange when read through this spatial logic.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Or walk through Akihabara at night. Vertical signage stacks ten stories high. Storefronts display product categories, price ranges, and promotional campaigns on every surface. The street is not chaotic; it is a browsing environment. People scan, compare, and filter before entering any single store.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-2-2.jpg&quot; alt=&quot;Japanese train station wayfinding signage with color-coded lines and multiple information layers&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Dense does not necessarily mean random. Japanese stations compress routes, exits, platforms, warnings, and services into navigable systems.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The city teaches people to read dense visual fields. Not because they are naturally better at it, but because the environment demands it. Daily commuting, shopping, and wayfinding in Tokyo provide thousands of hours of practice in extracting meaning from layered, overlapping, context-rich surfaces.&lt;/p&gt;
&lt;p&gt;Tokyo did not cause Japanese web design — that would be too simple. Tokyo is one visible example of a broader visual environment in which dense, navigable displays of information are common. Japanese web interfaces may reflect this environment because the users who navigate them have been trained by it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;From Street Signs to Screen Modules&lt;/h2&gt;
&lt;p&gt;The physical-to-digital analogy has limits, but it is useful. Consider how specific urban environments map to interface patterns:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical environment&lt;/th&gt;
&lt;th&gt;Digital equivalent&lt;/th&gt;
&lt;th&gt;Shared logic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Train-station wayfinding&lt;/td&gt;
&lt;td&gt;Dense navigation menus&lt;/td&gt;
&lt;td&gt;Many routes visible at once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Department-store floor guide&lt;/td&gt;
&lt;td&gt;Category mega-menu&lt;/td&gt;
&lt;td&gt;Orientation through labels and hierarchy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Konbini shelf&lt;/td&gt;
&lt;td&gt;Product grid&lt;/td&gt;
&lt;td&gt;Dense comparison, price cues, promotions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chirashi flyer&lt;/td&gt;
&lt;td&gt;Campaign modules&lt;/td&gt;
&lt;td&gt;Seasonal offers, urgency, abundance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restaurant street sign&lt;/td&gt;
&lt;td&gt;Promotional banner&lt;/td&gt;
&lt;td&gt;Immediate decision cues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public notice board&lt;/td&gt;
&lt;td&gt;Announcements / updates&lt;/td&gt;
&lt;td&gt;Completeness, procedural clarity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ticket machine&lt;/td&gt;
&lt;td&gt;Multi-step service UI&lt;/td&gt;
&lt;td&gt;Guided task completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manga page&lt;/td&gt;
&lt;td&gt;Modular visual sequencing&lt;/td&gt;
&lt;td&gt;Panel-based scanning, nonlinear reading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Don Quijote shelf&lt;/td&gt;
&lt;td&gt;Dense retail discovery&lt;/td&gt;
&lt;td&gt;Abundance as browsing mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A Japanese e-commerce page resembles a digital Don Quijote shelf: hundreds of products, price comparisons, campaign tags, and category cues compressed into a single scannable surface. The shopper is not being guided toward a single purchase. They are being invited to browse, compare, and discover.&lt;/p&gt;
&lt;p&gt;A municipal website functions like a public notice board: every procedure, every document, every contact method visible at once, because the institution cannot assume which specific question the citizen arrived with.&lt;/p&gt;
&lt;p&gt;These interfaces are digital versions of familiar physical and media environments.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Attention Is Culturally Trained&lt;/h2&gt;
&lt;p&gt;The science behind this intuition comes from cultural cognition research. In a series of studies, Richard Nisbett and Takahiko Masuda explored differences in visual attention between East Asian and Western participants. Their work contrasts two modes of attention:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Analytic attention&lt;/strong&gt;: Focus on discrete objects, categories, rules, and focal entities. The background is secondary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contextual attention&lt;/strong&gt;: Focus on fields, relationships, context, and change. The background carries meaning.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In Masuda and Nisbett&apos;s 2001 study, Japanese and American participants watched animated underwater scenes and described what they saw. Japanese participants made significantly more statements about background elements and relationships between objects. American participants tended to describe focal objects first. When researchers later tested recognition, Japanese participants&apos; ability to identify focal objects was more disrupted when the original background was changed.&lt;/p&gt;
&lt;p&gt;What does this mean for web design? The study did not involve websites, so it does not show that Japanese users prefer clutter. But it does suggest that attention to context and background has been observed experimentally, which makes it plausible that surrounding interface cues carry more meaning than Western minimalist designers assume.&lt;/p&gt;
&lt;p&gt;What looks like background noise to one audience may function as contextual information to another.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Physical Environment Trains the Eye&lt;/h2&gt;
&lt;p&gt;If attention patterns differ, where do they come from? One answer comes from Miyamoto, Nisbett, and Masuda&apos;s 2006 study, which compared Japanese and American physical environments. They found that Japanese scenes tended to contain more elements, more ambiguity, and more overlapping visual relationships than American scenes.&lt;/p&gt;
&lt;p&gt;The built environment is not just scenery — it trains perception. If everyday scenes contain more signs, objects, boundaries, and ambiguous relationships, users become practiced at extracting meaning from dense visual fields.&lt;/p&gt;
&lt;p&gt;The commuter who navigates Shinjuku Station every morning learns to scan layered signage, filter irrelevant signals, and locate meaningful cues within dense fields. The shopper who browses konbini shelves learns to compare products across multiple dimensions (price, promotion, category, novelty) at a glance. The reader who consumes manga learns to navigate nonlinear visual sequences using hierarchy, rhythm, and framing.&lt;/p&gt;
&lt;p&gt;These are skills people learn through repeated exposure to specific environmental conditions. And they may transfer to digital interfaces that present similar information structures.&lt;/p&gt;
&lt;p&gt;This shifts the question from &quot;Why do Japanese users like clutter?&quot; to &quot;What environments trained these scanning habits?&quot; — a hypothesis, not a proof.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Manga, Magazines, Flyers, and Dense Surfaces&lt;/h2&gt;
&lt;p&gt;The visual environment extends beyond urban space into media culture. Japanese print and commercial media have long traditions of dense information display:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Manga pages&lt;/strong&gt; use panel rhythm, speech bubbles, motion lines, and layered reading paths. The reader does not move linearly through the page but scans across multiple visual entry points, guided by hierarchy and spatial relationships.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-2-4.jpg&quot; alt=&quot;A manga page showing nonlinear panel sequencing and dense visual storytelling&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Manga and magazine layouts train readers to move through dense visual fields using hierarchy, rhythm, framing, and typographic signals.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Magazine covers&lt;/strong&gt; often contain dozens of headlines, teasers, and callouts competing for attention. The design assumes readers will scan, not read sequentially.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chirashi flyers&lt;/strong&gt; from supermarkets and department stores compress prices, product images, campaign periods, and promotional bursts into single sheets designed for rapid scanning and comparison.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-2-3.avif&quot; alt=&quot;A Japanese chirashi flyer showing dense promotional content with prices and campaign tags&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The chirashi flyer is a useful analogue for campaign-heavy websites: dense, comparative, seasonal, price-oriented, and designed for scanning.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Train advertisements&lt;/strong&gt; must capture attention in seconds, often using dense visual fields with multiple focal points and contextual cues.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Product packaging&lt;/strong&gt; in Japanese retail frequently displays ingredients, origin, preparation methods, and promotional information simultaneously, designed for the informed comparison shopper.&lt;/p&gt;
&lt;p&gt;These surfaces are not meant to be read linearly. They are meant to be scanned, compared, and entered from multiple points. They belong to the same visual environment as the dense web interfaces they resemble.&lt;/p&gt;
&lt;p&gt;These are analogies, not causes. Manga does not determine portal layouts, and magazine covers do not dictate web design. They are parallel visual literacies that coexist within the same cultural environment.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What This Explains About Japanese Websites&lt;/h2&gt;
&lt;p&gt;What does this mean for actual Japanese web interfaces?&lt;/p&gt;
&lt;p&gt;A Western minimalist site often assumes: &lt;strong&gt;One message, one focal object, one CTA.&lt;/strong&gt; The user arrives with an intent, and the page&apos;s job is to focus that intent and remove distraction.&lt;/p&gt;
&lt;p&gt;A Japanese dense site often behaves more like: &lt;strong&gt;A map of options, conditions, campaigns, rankings, and service paths.&lt;/strong&gt; The user arrives with a context, and the page&apos;s job is to make that context visible and navigable.&lt;/p&gt;
&lt;p&gt;This explains several observable patterns in Japanese web design:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Visible breadth over guided narrative&lt;/strong&gt;: Category pages show many products. Portal pages show many services. The assumption is that users want to see options before choosing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Contextual cues over isolated hero messaging&lt;/strong&gt;: Price comparisons, campaign tags, trust badges, and support links appear alongside primary content, not hidden behind clicks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Category density over progressive disclosure&lt;/strong&gt;: Information that Western designers might tuck into dropdowns or secondary pages remains visible, because the cost of clicking may exceed the cost of scanning.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Trust through completeness&lt;/strong&gt;: Municipal pages, banking sites, and service portals surface every document, procedure, and contact method, because incompleteness creates anxiety.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Navigability through labels and clusters&lt;/strong&gt;: Dense pages rely on clear typographic hierarchy, color coding, and spatial grouping to make scanning possible.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Multiple user intents on one screen&lt;/strong&gt;: A single page may serve informational, transactional, comparative, and support needs simultaneously.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Service-counter logic rather than conversion-funnel logic&lt;/strong&gt;: The interface behaves like a staffed counter where the user can ask multiple questions, not like a billboard that demands a single action.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These patterns are rational responses to different assumptions about user behavior, shaped by different environmental training.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;But Density Is Not Automatically Good UX&lt;/h2&gt;
&lt;p&gt;Cultural plausibility explains tolerance, not efficiency. Familiarity with dense environments may increase tolerance for dense interfaces, but tolerance is not the same as efficiency.&lt;/p&gt;
&lt;p&gt;Baughan et al.&apos;s 2021 CHI study tested this directly. Eighty-four U.S. participants and sixty-five Japanese participants searched for information on website screenshots of varying complexity. The researchers hypothesized that Japanese participants might be faster at finding contextual information due to different visual attention patterns.&lt;/p&gt;
&lt;p&gt;The results did not support this. Japanese participants took longer overall, and the performance gap increased as website complexity increased. The authors suggest that Japanese participants may be taking in the whole page at once before focusing on the primary task — a broader initial scan, a delay while they orient themselves to the page.&lt;/p&gt;
&lt;p&gt;The study recommends: highlight related content areas, use consistent layouts, and reduce unnecessary complexity for both groups.&lt;/p&gt;
&lt;p&gt;Japanese users may take in more of the field before acting, but this can slow task completion. Dense interfaces can be culturally familiar and still impose usability costs. Familiarity may increase tolerance for density, but it does not eliminate cognitive load.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Good Density Needs Hierarchy&lt;/h2&gt;
&lt;p&gt;If density is not automatically good, what makes it usable? The answer is hierarchy.&lt;/p&gt;
&lt;p&gt;Tokyo is dense, but it is not random. Shinjuku Station works because redundancy is organized: colors code lines, numbers code exits, maps show spatial relationships, and signs appear at decision points. The density is managed through systems.&lt;/p&gt;
&lt;p&gt;Japanese web interfaces that succeed do the same. They use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Clear typographic hierarchy&lt;/strong&gt;: Headlines, subheads, and body text create scanning paths.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Color coding&lt;/strong&gt;: Campaign tags, category labels, and status indicators use consistent color systems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spatial grouping&lt;/strong&gt;: Related information clusters together, separated by whitespace or borders.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redundant cues&lt;/strong&gt;: Important information appears in multiple formats (icon, text, color) for different scanning modes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistent layouts&lt;/strong&gt;: Users learn where to look for specific information types.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The distinction that matters is between &lt;strong&gt;useful density and costly density&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Useful density&lt;/th&gt;
&lt;th&gt;Costly density&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supports comparison&lt;/td&gt;
&lt;td&gt;Hides hierarchy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shows routes&lt;/td&gt;
&lt;td&gt;Forces exhaustive search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Builds reassurance&lt;/td&gt;
&lt;td&gt;Creates anxiety&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uses stable clusters&lt;/td&gt;
&lt;td&gt;Accumulates randomly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matches multi-intent pages&lt;/td&gt;
&lt;td&gt;Overloads single-task flows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Good Japanese web design should organize density, not just preserve it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;From City to Trust&lt;/h2&gt;
&lt;p&gt;Japanese web density extends beyond the screen into a wider visual environment: stations, streets, shelves, flyers, magazines, public notices, and commercial signs. That does not make every dense interface good. The evidence suggests that complexity can still slow users down. But it changes the question.&lt;/p&gt;
&lt;p&gt;Instead of asking why Japanese websites look like clutter, we can ask why they resemble navigable environments. Instead of assuming Western minimalism is the universal standard, we can recognize that different visual environments produce different interface logics.&lt;/p&gt;
&lt;p&gt;The city explains part of the pattern. Repeated exposure to dense physical and media environments shapes scanning habits, expectations, and tolerance for contextual information. But there is another layer. If density feels familiar because it echoes the city, it can also feel reassuring because it answers questions before the user has to ask them.&lt;/p&gt;
&lt;p&gt;That trust is the subject of the next essay.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Sources and Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Baughan, S., et al. &quot;Do Cross-Cultural Differences in Visual Attention Patterns Affect Search Efficiency on Websites?&quot; &lt;em&gt;Proceedings of the 2021 CHI Conference on Human Factors in Computing Systems&lt;/em&gt;, 2021. &lt;a href=&quot;https://naomi-yamashita.net/wp-content/uploads/2021/06/2021jp-02.pdf&quot;&gt;PDF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Masuda, T., &amp;amp; Nisbett, R. E. &quot;Attending Holistically Versus Analytically: Comparing the Context Sensitivity of Japanese and Americans.&quot; &lt;em&gt;Journal of Personality and Social Psychology&lt;/em&gt;, 81(5), 922–934, 2001.&lt;/li&gt;
&lt;li&gt;Miyamoto, Y., Nisbett, R. E., &amp;amp; Masuda, T. &quot;Culture and the Physical Environment: Holistic Versus Analytic Perceptual Affordances.&quot; &lt;em&gt;Psychological Science&lt;/em&gt;, 17(2), 113–119, 2006.&lt;/li&gt;
&lt;li&gt;Nisbett, R. E., &amp;amp; Masuda, T. &quot;Culture and Point of View.&quot; &lt;em&gt;Proceedings of the National Academy of Sciences&lt;/em&gt;, 100(19), 11163–11170, 2003.&lt;/li&gt;
&lt;li&gt;Würtz, E. &quot;Intercultural Communication on Web Sites: A Cross-Cultural Analysis of Web Sites from High-Context Cultures and Low-Context Cultures.&quot; &lt;em&gt;Journal of Computer-Mediated Communication&lt;/em&gt;, 11(1), 2005.&lt;/li&gt;
&lt;li&gt;Cyr, D., &amp;amp; Trevor-Smith, H. &quot;Localization of Web Design: An Empirical Comparison of German, Japanese, and United States Web Site Characteristics.&quot; &lt;em&gt;Journal of the American Society for Information Science and Technology&lt;/em&gt;, 55(13), 1199–1208, 2004.&lt;/li&gt;
&lt;li&gt;W3C. &quot;Requirements for Japanese Text Layout.&quot; &lt;a href=&quot;https://www.w3.org/TR/jlreq/&quot;&gt;JLREQ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Why Japanese Websites Look So Overloaded, and What the Density Is Doing</title><link>https://corentings.dev/blog/ux-japan-1/</link><guid isPermaLink="true">https://corentings.dev/blog/ux-japan-1/</guid><description>Why do Japanese websites look overloaded? A UX look at density, trust cues, cultural context, and why &apos;clutter&apos; is often the wrong word.</description><pubDate>Sun, 31 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Why Japanese Websites Look So Overloaded, and What the Density Is Doing&lt;/h1&gt;
&lt;p&gt;Open Yahoo! Japan after browsing a Western startup website and you feel the difference immediately. The Western page gives you a hero image, a sentence, and a button. The Japanese page gives you news, weather, shopping, finance, auctions, login, campaigns, rankings, emergency notices, and half a dozen ways to go somewhere else.&lt;/p&gt;
&lt;p&gt;To a Western designer, it may look like clutter. But what if the page is not failing at minimalism? What if it is solving a different design problem?&lt;/p&gt;
&lt;p&gt;Many Japanese domestic websites use high-density interfaces to show context, reassurance, comparison options, promotions, navigation, and trust cues all at once. To users trained by minimalist design norms, that density reads as overload. But the better UX question is not &quot;why is this bloated?&quot; It is &quot;what job is this density doing?&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-1-1.jpg&quot; alt=&quot;A typical Japanese domestic portal page showing dense navigation, campaign banners, multiple content modules, and trust cues visible on a single screen&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A Japanese portal homepage showing high-density UI: news, weather, campaigns, rankings, and navigation modules are all visible at once. To a Western minimalist eye, it may register as clutter — but the density is doing specific jobs.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;&quot;Bloated&quot; Is the Wrong Word&lt;/h2&gt;
&lt;p&gt;Western critiques of Japanese web design often write it all off as bloat. But that word hides more than it reveals. Before we can ask why these pages look the way they do, we need a sharper vocabulary.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Is it always bad?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Information density&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Many facts, explanations, specs, labels, notices, comparisons visible at once&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visual density&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Many modules, colors, banners, borders, type sizes, icons&lt;/td&gt;
&lt;td&gt;Sometimes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Navigational density&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Many menus, links, category paths, sidebars, breadcrumbs&lt;/td&gt;
&lt;td&gt;Sometimes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Promotional density&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Coupons, rankings, campaigns, limited-time banners, points&lt;/td&gt;
&lt;td&gt;Depends on business model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trust density&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Security badges, company info, shipping, returns, support, official notices&lt;/td&gt;
&lt;td&gt;Often useful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Technical bloat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heavy JS, slow assets, unoptimized tracking, excessive DOM&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Only technical bloat is inherently bad. The rest may be poorly executed, but it may also serve a function. A page with many trust badges is not the same as a page with unoptimized JavaScript. A page with visible campaign information is not the same as a page with broken visual hierarchy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-1-4.png&quot; alt=&quot;Diagram of six density types: information, visual, navigational, promotional, trust, and technical bloat&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Separating density types lets us diagnose what each part of the page is doing, rather than collapsing everything into &quot;clutter.&quot; Technical bloat is the only inherently harmful type.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When we call everything &quot;clutter,&quot; we stop being able to diagnose it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Western Minimalism Is a Design Lens, Not a Universal Standard&lt;/h2&gt;
&lt;p&gt;Western digital design, especially in SaaS, startups, and luxury, tends to value:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Whitespace&lt;/li&gt;
&lt;li&gt;One primary call to action&lt;/li&gt;
&lt;li&gt;Few choices&lt;/li&gt;
&lt;li&gt;Emotional branding&lt;/li&gt;
&lt;li&gt;Reduced copy&lt;/li&gt;
&lt;li&gt;Progressive disclosure&lt;/li&gt;
&lt;li&gt;Polished typography&lt;/li&gt;
&lt;li&gt;Simple conversion funnels&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not neutral. It is a design ideology shaped by specific cultural and commercial assumptions: that the user arrives with a single intent, that persuasion happens through restraint, that confidence is signaled through polish, and that the page&apos;s job is to remove friction toward one action.&lt;/p&gt;
&lt;p&gt;Many Japanese domestic sites operate on different assumptions. They often value:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Visible categories&lt;/li&gt;
&lt;li&gt;Multiple routes&lt;/li&gt;
&lt;li&gt;Campaign visibility&lt;/li&gt;
&lt;li&gt;Rankings and social proof&lt;/li&gt;
&lt;li&gt;Detailed conditions&lt;/li&gt;
&lt;li&gt;Support information&lt;/li&gt;
&lt;li&gt;Comparison tools&lt;/li&gt;
&lt;li&gt;Trust cues&lt;/li&gt;
&lt;li&gt;Seasonal updates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not a failure of minimalism. It is a different answer to a different question. Where Western minimalism often asks &quot;what can we remove?&quot;, Japanese information-rich design often asks &quot;what context does the user need to feel confident?&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-1-2.jpg&quot; alt=&quot;Side-by-side comparison of a Western minimalist website and a Japanese high-density website showing different design approaches&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Western minimalist design (left) vs Japanese high-density design (right). One strips down to one action. The other shows context for many possible user intents.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Japanese Homepage as Service Counter&lt;/h2&gt;
&lt;p&gt;A Japanese homepage often functions as a digital service counter. It must answer many user questions upfront:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What is new?&lt;/li&gt;
&lt;li&gt;What campaign is active?&lt;/li&gt;
&lt;li&gt;Where do I log in?&lt;/li&gt;
&lt;li&gt;What are today&apos;s offers?&lt;/li&gt;
&lt;li&gt;Where is support?&lt;/li&gt;
&lt;li&gt;What are the rankings?&lt;/li&gt;
&lt;li&gt;What is popular?&lt;/li&gt;
&lt;li&gt;What happens after I click?&lt;/li&gt;
&lt;li&gt;What are the conditions?&lt;/li&gt;
&lt;li&gt;Can I compare?&lt;/li&gt;
&lt;li&gt;Is this official?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this model, the page is a hybrid of storefront, catalogue, flyer, customer-service desk, trust document, comparison table, and seasonal promotion board. The density serves a function.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-1-3.jpg&quot; alt=&quot;Japanese homepage annotated with service-counter zones: information, campaigns, navigation, trust cues, and announcements&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A Japanese homepage annotated to show its service-counter zones. Each cluster of elements answers a different user question before it needs to be asked.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This does not mean every dense page is well-designed. A service counter can be chaotic. A flyer can be unreadable. The point is that the density is doing work, and the work is different from what a minimalist landing page is trying to do.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What Research Says About Culture, Attention, and Web Design&lt;/h2&gt;
&lt;p&gt;The science here is suggestive but not conclusive. It does not prove that Japanese users prefer dense websites. But it does show that attention is not culturally neutral.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nisbett and Masuda&apos;s work on analytic versus holistic attention&lt;/strong&gt; found that Western participants tend to focus more on focal objects, categories, and formal properties, while East Asian participants tend to attend more to background, relationships, change, and context. In their well-known underwater-scene experiments, Japanese participants reported more contextual and relational information and were more affected by background changes during recognition tasks than American participants.&lt;/p&gt;
&lt;p&gt;If a web page is a visual field, then a culture more accustomed to context-sensitive perception may assign more meaning to surrounding cues: rankings, sidebars, labels, badges, notices, seasonal banners, and related options. What one user treats as irrelevant background, another may process as part of the meaningful field.&lt;/p&gt;
&lt;p&gt;Context sensitivity does not mean users prefer clutter. Familiarity with dense environments is different from efficiency within them. The research raises the question without settling it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Empirical Web-Design Evidence&lt;/h2&gt;
&lt;p&gt;Cross-cultural web-design research has measured these differences at scale. Nordhoff, August, Oliveira, and Reinecke analyzed &lt;strong&gt;80,901 website designs across 44 countries&lt;/strong&gt; and found measurable cross-country differences in visual design metrics. Japan, along with China and South Korea, clustered toward higher visual complexity and text density patterns.&lt;/p&gt;
&lt;p&gt;Baughan and colleagues studied &lt;strong&gt;84 U.S. American and 65 Japanese participants&lt;/strong&gt; searching for information on website screenshots of varying complexity. Japanese participants took longer overall, and the negative effect of complexity was stronger with increased website complexity — contrary to the hypothesis that they might perform better on dense sites.&lt;/p&gt;
&lt;p&gt;Dense websites may be locally familiar and commercially meaningful, but familiarity is not the same as efficiency. The future of Japanese web design is &lt;strong&gt;structured density&lt;/strong&gt;: context-rich design that does not sacrifice usability.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What Japanese Layout Standards Reveal&lt;/h2&gt;
&lt;p&gt;There is also a technical layer to this story. Japanese text composition involves unique requirements: mixed writing systems (kanji, hiragana, katakana, Latin characters, numerals), vertical and horizontal composition modes, complex line-breaking rules, ruby annotations, emphasis marks, and specific spacing conventions.&lt;/p&gt;
&lt;p&gt;The W3C&apos;s &lt;em&gt;Requirements for Japanese Text Layout&lt;/em&gt; documents these constraints in detail. Japanese script does not cause clutter, but it changes what compact information display can look like. A kanji label can convey dense semantic information in a compact spatial form in ways that do not map directly to English or French.&lt;/p&gt;
&lt;p&gt;Localization is the adaptation of hierarchy, trust, pacing, navigation, density, and proof — not translation plus flags.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;So Are Japanese Websites Actually Cluttered?&lt;/h2&gt;
&lt;p&gt;Sometimes, yes. Density can become noise. Visual hierarchy can collapse. Promotional banners can multiply until they obscure the content they are meant to promote. Technical bloat can slow a page to a crawl. These are real problems, and they deserve real critique.&lt;/p&gt;
&lt;p&gt;But sometimes, no. What looks like clutter from one design ideology may be context, reassurance, comparison, and transparency from another. A Western landing page says &quot;here is the action.&quot; A Japanese high-density page says &quot;here is the situation.&quot;&lt;/p&gt;
&lt;p&gt;The UX test is not whether the page looks busy. It is what happens if you remove an element. Does the user lose information they needed? Do they lose confidence? Do they lose a path they wanted to take? If the answer is yes, it was not clutter. It was structure they relied on.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;A Better Question for Designers&lt;/h2&gt;
&lt;p&gt;Instead of asking why Japanese websites are overloaded, ask what that density is doing. Is it clutter, or is it context? Is it noise, or is it reassurance? Is it poor design, or is it a design solving a different trust problem?&lt;/p&gt;
&lt;p&gt;Japanese websites look dense because many domestic sites are built as service environments, not statements. They must reassure, compare, announce, explain, promote, and guide the user at the same time.&lt;/p&gt;
&lt;p&gt;Some of that density is clutter. Some of it is context. What matters is what work the overload is doing.&lt;/p&gt;
&lt;p&gt;Article 2 will take that question outside the browser — into Tokyo&apos;s train stations, retail shelves, convenience stores, and urban signage — to see where Japanese visual density comes from.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Sources and Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Baughan, A., Oliveira, N., August, T., Yamashita, N., &amp;amp; Reinecke, K. (2021). &quot;Do Cross-Cultural Differences in Visual Attention Patterns Affect Search Efficiency on Websites?&quot; &lt;em&gt;Proceedings of the 2021 CHI Conference on Human Factors in Computing Systems&lt;/em&gt;. &lt;a href=&quot;https://naomi-yamashita.net/wp-content/uploads/2021/06/2021jp-02.pdf&quot;&gt;PDF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Masuda, T., &amp;amp; Nisbett, R. E. (2001). &quot;Attending Holistically Versus Analytically: Comparing the Context Sensitivity of Japanese and Americans.&quot; &lt;em&gt;Journal of Personality and Social Psychology&lt;/em&gt;, 81(5), 922–934.&lt;/li&gt;
&lt;li&gt;Nisbett, R. E., &amp;amp; Masuda, T. (2003). &quot;Culture and Point of View.&quot; &lt;em&gt;Proceedings of the National Academy of Sciences&lt;/em&gt;, 100(19), 11163–11170.&lt;/li&gt;
&lt;li&gt;Nordhoff, M., August, T., Oliveira, N. A., &amp;amp; Reinecke, K. (2018). &quot;A Case for Design Localization: Diversity of Website Aesthetics in 44 Countries.&quot; &lt;em&gt;Proceedings of the 2018 CHI Conference on Human Factors in Computing Systems&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;W3C. (2020). &quot;Requirements for Japanese Text Layout.&quot; &lt;a href=&quot;https://www.w3.org/TR/jlreq/&quot;&gt;JLREQ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>TDD Isn&apos;t About Bugs — It&apos;s Your Permission to Refactor</title><link>https://corentings.dev/blog/tdd-permission-slip/</link><guid isPermaLink="true">https://corentings.dev/blog/tdd-permission-slip/</guid><description>Learn why test-driven development is really about permission to refactor, not catching bugs. With TypeScript examples, Result&lt;T, E&gt; patterns, and behavior-based testing from 3 years in production.</description><pubDate>Sun, 24 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;TDD Isn&apos;t About Bugs — It&apos;s Your Permission to Refactor&lt;/h1&gt;
&lt;h2&gt;The Moment It Clicked&lt;/h2&gt;
&lt;p&gt;Three years ago, I had a crisis of confidence with test-driven development.&lt;/p&gt;
&lt;p&gt;I&apos;d built a user registration system with 80% test coverage, all green, CI passing. Then I decided to refactor the email validation logic: just move it to a separate module. A tiny change.&lt;/p&gt;
&lt;p&gt;Thirty minutes later, everything was broken.&lt;/p&gt;
&lt;p&gt;My tests tested &lt;strong&gt;how&lt;/strong&gt; the code worked, not &lt;strong&gt;what&lt;/strong&gt; it did. The moment the structure changed, they failed. I spent more time fixing tests than doing the refactoring itself.&lt;/p&gt;
&lt;p&gt;I&apos;ll show you what I was doing wrong. It might look familiar.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Trap: Testing Structure&lt;/h2&gt;
&lt;p&gt;Here&apos;s the TypeScript code I had, a &lt;code&gt;User&lt;/code&gt; class with inline validation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class User {
	email: string;
	age: number;

	constructor(email: string, age: number) {
		if (!email.includes(&quot;@&quot;)) {
			throw new Error(&quot;Invalid email&quot;);
		}
		if (age &amp;lt; 18) {
			throw new Error(&quot;Must be at least 18 years old&quot;);
		}
		this.email = email;
		this.age = age;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the test:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;describe(&quot;User&quot;, () =&amp;gt; {
	it(&quot;should create a user&quot;, () =&amp;gt; {
		const user = new User(&quot;alice@example.com&quot;, 25);
		expect(user.email).toBe(&quot;alice@example.com&quot;);
		expect(user.age).toBe(25);
	});
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What am I testing? Structure. I&apos;m reaching into the object and checking its properties directly.&lt;/p&gt;
&lt;p&gt;Now watch what happens when I hide the internals:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class User {
	private email: string;
	private age: number;

	constructor(email: string, age: number) {
		if (!email.includes(&quot;@&quot;)) {
			throw new Error(&quot;Invalid email&quot;);
		}
		if (age &amp;lt; 18) {
			throw new Error(&quot;Must be at least 18 years old&quot;);
		}
		this.email = email;
		this.age = age;
	}

	get email(): string {
		return this.email;
	}

	get age(): number {
		return this.age;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test breaks. The behavior hasn&apos;t changed (the same user is created with the same rules), but I changed the structure.&lt;/p&gt;
&lt;p&gt;I&apos;m now rewriting tests for a refactoring that shouldn&apos;t require any test changes.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Fix: Test Behavior&lt;/h2&gt;
&lt;p&gt;The solution: test what the code &lt;strong&gt;does&lt;/strong&gt;, not how it&apos;s &lt;strong&gt;organized&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But first, I need to fix another problem. Throwing exceptions makes testing awkward: you need &lt;code&gt;expect(() =&amp;gt; fn()).toThrow()&lt;/code&gt;, which hides the error behind a callback. The fix is treating errors as &lt;strong&gt;first-class values&lt;/strong&gt; instead of hidden control flow.&lt;/p&gt;
&lt;p&gt;Here&apos;s the pattern that made this click, the &lt;code&gt;Result&lt;/code&gt; type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Result&amp;lt;T, E&amp;gt; =
	| { readonly ok: true; readonly value: T }
	| { readonly ok: false; readonly error: E };

function ok&amp;lt;T&amp;gt;(value: T): Result&amp;lt;T, never&amp;gt; {
	return { ok: true, value };
}

function err&amp;lt;E&amp;gt;(error: E): Result&amp;lt;never, E&amp;gt; {
	return { ok: false, error };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of throwing, functions return a &lt;code&gt;Result&lt;/code&gt; that&apos;s either a success or a failure. Both paths are explicit. No try/catch. Errors are data you can inspect, test, and chain. (In production code, you&apos;d use a library like &lt;a href=&quot;https://github.com/supermacro/neverthrow&quot;&gt;neverthrow&lt;/a&gt; which adds &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;flatMap&lt;/code&gt;, and &lt;code&gt;match&lt;/code&gt; for composing results.)&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;Result&lt;/code&gt; in place, I can rewrite the &lt;code&gt;User&lt;/code&gt; class with a static factory method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class UserCreationError {
	constructor(
		public readonly message: string,
		public readonly field: string
	) {}
}

class User {
	private constructor(
		private readonly _email: string,
		private readonly _age: number
	) {}

	static create(email: string, age: number): Result&amp;lt;User, UserCreationError&amp;gt; {
		if (!email.includes(&quot;@&quot;)) {
			return err(new UserCreationError(&quot;Invalid email format&quot;, &quot;email&quot;));
		}
		if (age &amp;lt; 18) {
			return err(new UserCreationError(&quot;Must be at least 18 years old&quot;, &quot;age&quot;));
		}
		return ok(new User(email, age));
	}

	get email(): string {
		return this._email;
	}

	get age(): number {
		return this._age;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the tests verify &lt;strong&gt;behavior&lt;/strong&gt;, the observable contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;describe(&quot;User Registration&quot;, () =&amp;gt; {
	it(&quot;should create a user with valid email and age&quot;, () =&amp;gt; {
		const result = User.create(&quot;alice@example.com&quot;, 25);

		expect(result.ok).toBe(true);
		if (result.ok) {
			expect(result.value.email).toBe(&quot;alice@example.com&quot;);
			expect(result.value.age).toBe(25);
		}
	});

	it(&quot;should reject invalid emails&quot;, () =&amp;gt; {
		const result = User.create(&quot;not-an-email&quot;, 25);

		expect(result.ok).toBe(false);
		if (!result.ok) {
			expect(result.error.field).toBe(&quot;email&quot;);
			expect(result.error.message).toMatch(/email/i);
		}
	});

	it(&quot;should reject users under 18&quot;, () =&amp;gt; {
		const result = User.create(&quot;alice@example.com&quot;, 17);

		expect(result.ok).toBe(false);
		if (!result.ok) {
			expect(result.error.message).toMatch(/18/);
		}
	});
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When I refactor the internals now (make &lt;code&gt;_email&lt;/code&gt; private, change the data structure, extract a class), these tests keep passing. They test the contract: valid inputs succeed, invalid inputs fail with the right error. The internal structure is irrelevant.&lt;/p&gt;
&lt;p&gt;This is the red-green-refactor cycle that defines test-driven development: write a failing test (red), make it pass with the simplest code (green), then improve the design while keeping tests green (refactor). The tests give you permission to refactor aggressively because they catch real problems, not structural changes.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Going Deeper: Value Objects&lt;/h2&gt;
&lt;p&gt;Once I started testing behavior, I noticed something. My validation logic was scattered. The same email check appeared in &lt;code&gt;registerUser&lt;/code&gt;, &lt;code&gt;changeEmail&lt;/code&gt;, and &lt;code&gt;resetPassword&lt;/code&gt;. Three places, same rule, same potential for bugs.&lt;/p&gt;
&lt;p&gt;The fix is a &lt;strong&gt;Value Object&lt;/strong&gt;: a small, self-validating type that represents a single concept. If you have an &lt;code&gt;Email&lt;/code&gt; instance, it&apos;s valid by construction:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class InvalidEmailError {
	readonly message = &quot;Invalid email format&quot;;
}

class Email {
	private constructor(private readonly _value: string) {}

	static create(raw: string): Result&amp;lt;Email, InvalidEmailError&amp;gt; {
		const value = raw.trim().toLowerCase();
		if (!value.includes(&quot;@&quot;)) {
			return err(new InvalidEmailError());
		}
		return ok(new Email(value));
	}

	get value(): string {
		return this._value;
	}

	equals(other: Email): boolean {
		return this._value === other._value;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the normalization (&lt;code&gt;trim().toLowerCase()&lt;/code&gt;) lives inside the Value Object. Every &lt;code&gt;Email&lt;/code&gt; instance is guaranteed lowercase and trimmed. The validation rule exists in one place. If the rule changes, you change one class.&lt;/p&gt;
&lt;p&gt;Similarly for &lt;code&gt;Age&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class InvalidAgeError {
	readonly message = &quot;Must be at least 18 years old&quot;;
}

class Age {
	private constructor(private readonly _value: number) {}

	static create(value: number): Result&amp;lt;Age, InvalidAgeError&amp;gt; {
		if (!Number.isInteger(value) || value &amp;lt; 18) {
			return err(new InvalidAgeError());
		}
		return ok(new Age(value));
	}

	get value(): number {
		return this._value;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;User&lt;/code&gt; class now composes these Value Objects instead of accepting raw primitives:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class User {
	private constructor(
		private readonly _email: Email,
		private readonly _age: Age
	) {}

	static create(email: string, age: number): Result&amp;lt;User, UserCreationError&amp;gt; {
		const emailResult = Email.create(email);
		if (!emailResult.ok)
			return err(new UserCreationError(emailResult.error.message, &quot;email&quot;));

		const ageResult = Age.create(age);
		if (!ageResult.ok)
			return err(new UserCreationError(ageResult.error.message, &quot;age&quot;));

		return ok(new User(emailResult.value, ageResult.value));
	}

	get email(): string {
		return this._email.value;
	}

	get age(): number {
		return this._age.value;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same tests from before still pass. They test &lt;code&gt;User.create()&lt;/code&gt; returning success or failure, which hasn&apos;t changed. The internal composition with Value Objects is invisible to the tests.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Payment System&lt;/h2&gt;
&lt;p&gt;Last year, I refactored a production payment processing module: about 2,400 lines of conditional logic scattered across &lt;code&gt;processPayment&lt;/code&gt;, &lt;code&gt;validatePayment&lt;/code&gt;, and &lt;code&gt;handleRefund&lt;/code&gt;. Validation rules were duplicated in all three functions.&lt;/p&gt;
&lt;p&gt;The old tests looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;it(&quot;processes payment&quot;, () =&amp;gt; {
	const processor = new PaymentProcessor(mockGateway, mockDB);
	processor.validateAmount(100);
	processor.validateCurrency(&quot;USD&quot;);
	const result = processor.process({ amount: 100, currency: &quot;USD&quot; });
	expect(result.status).toBe(&quot;success&quot;);
	expect(mockGateway.charge).toHaveBeenCalledTimes(1);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those tests called internal methods directly. When I extracted validation into a &lt;code&gt;Payment&lt;/code&gt; Value Object, every test broke because &lt;code&gt;validateAmount&lt;/code&gt; and &lt;code&gt;validateCurrency&lt;/code&gt; no longer existed as separate methods.&lt;/p&gt;
&lt;p&gt;I rewrote the tests to focus on the contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;it(&quot;accepts valid payments&quot;, () =&amp;gt; {
	const result = Payment.create({ amount: 100, currency: &quot;USD&quot; });
	expect(result.ok).toBe(true);
});

it(&quot;rejects negative amounts&quot;, () =&amp;gt; {
	const result = Payment.create({ amount: -50, currency: &quot;USD&quot; });
	expect(result.ok).toBe(false);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I refactored aggressively. Extracted &lt;code&gt;Currency&lt;/code&gt; as its own Value Object. Moved validation into the &lt;code&gt;Payment&lt;/code&gt; factory. Deleted 800 lines of duplicated logic. Tests never broke once, because they tested what happened, not how.&lt;/p&gt;
&lt;p&gt;The whole refactoring took a day. With the old tests, it would have taken a week: three days of refactoring, two days of fixing tests.&lt;/p&gt;
&lt;p&gt;The same idea shows up in concurrency code too. When you refactor a &lt;a href=&quot;/blog/go-pattern-pipeline/&quot;&gt;pipeline&lt;/a&gt; or a &lt;a href=&quot;/blog/go-pattern-worker/&quot;&gt;worker pool&lt;/a&gt;, the contract stays the same while the internals change. The tests that survive are the ones that verify behavior, not goroutine counts.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The &quot;TDD Is Slow&quot; Myth&lt;/h2&gt;
&lt;p&gt;I used to think test-driven development slowed me down. Studies tell a more nuanced story: TDD has a modest upfront cost of 10–30% more development time, but reduces defects by 40–90% (Nagappan et al., Microsoft/IBM, 2008). George &amp;amp; Williams (2003) found TDD pairs took 16% more time but passed 18% more black-box tests.&lt;/p&gt;
&lt;p&gt;The upfront cost is real. The payoff is also real. And it compounds: every safe refactoring makes the next one easier.&lt;/p&gt;
&lt;p&gt;When I couldn&apos;t refactor safely, I coded defensively. I avoided changes. I left bad code in place because changing it was risky. Refactoring without tests is what&apos;s actually slow. It compounds with every month.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;When TDD Doesn&apos;t Help&lt;/h2&gt;
&lt;p&gt;Test-driven development isn&apos;t always the right tool. Spikes and prototypes (code you&apos;ll throw away) don&apos;t need tests. Exploratory work where you don&apos;t know the shape of the solution yet. UI code where the behavior is visual, not logical. One-off scripts. Code with genuinely unstable requirements that change every sprint.&lt;/p&gt;
&lt;p&gt;TDD pays off when you&apos;re building something you&apos;ll maintain and refactor over time. If the code won&apos;t survive the week, write the minimum.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Monday Morning&lt;/h2&gt;
&lt;p&gt;Take one function you wrote recently. Rewrite its tests to verify behavior (what it does) instead of implementation (how it&apos;s organized).&lt;/p&gt;
&lt;p&gt;Watch what happens to your code.&lt;/p&gt;
&lt;p&gt;Three years ago, I was afraid to refactor. Now I do it aggressively, because my tests give me permission. Test-driven development gave me a permission slip I didn&apos;t know I needed.&lt;/p&gt;
&lt;p&gt;Next time, I&apos;ll show you how to build Value Objects that make invalid states impossible, and how testing them first makes the design emerge naturally.&lt;/p&gt;
&lt;p&gt;If you found this useful, the next article in the series shows how to build Value Objects that make invalid states impossible. It comes out in a couple of weeks. You can subscribe via &lt;a href=&quot;/rss.xml&quot;&gt;RSS&lt;/a&gt; or &lt;a href=&quot;/atom.xml&quot;&gt;Atom&lt;/a&gt; to catch it when it lands.&lt;/p&gt;
&lt;p&gt;— Corentin&lt;/p&gt;
</content:encoded></item><item><title>Docker 29 Broke Traefik — Here&apos;s the Fix (and Why It Happened)</title><link>https://corentings.dev/blog/docker-29-traefik-fix/</link><guid isPermaLink="true">https://corentings.dev/blog/docker-29-traefik-fix/</guid><description>Docker Engine v29 raised the minimum API version to 1.44, breaking Traefik and a dozen other tools. Here&apos;s the daemon.json fix that takes 30 seconds, and the real solution.</description><pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Docker 29 Broke Traefik — Here&apos;s the Fix (and Why It Happened)&lt;/h1&gt;
&lt;h2&gt;The &quot;Everything Is Down&quot; Moment&lt;/h2&gt;
&lt;p&gt;You ran &lt;code&gt;apt upgrade&lt;/code&gt; on a Sunday evening on your Linux server. Seemed harmless. Docker updated to v29.0.0. You rebooted because why not.&lt;/p&gt;
&lt;p&gt;Now every service behind your Traefik reverse proxy returns a 404. Or a 502. Or just… nothing. Your monitoring is screaming. Your personal projects are unreachable. Your CI pipeline is stuck. And if you&apos;re running Coolify, Dokploy, or any platform that bundles Traefik, you can&apos;t even access the management UI to fix it.&lt;/p&gt;
&lt;p&gt;You check the Traefik logs and see this, repeating every few seconds:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ERR Failed to retrieve information of the docker client and server host
  error=&quot;Error response from daemon: client version 1.24 is too old.
  Minimum supported API version is 1.44, please upgrade your client
  to a newer version&quot; providerName=docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your first thought: &quot;I didn&apos;t change anything in Traefik.&quot;&lt;/p&gt;
&lt;p&gt;You&apos;re right. You didn&apos;t. Docker changed something under you.&lt;/p&gt;
&lt;p&gt;Let me save you the debugging. Here&apos;s the fix.&lt;/p&gt;
&lt;h2&gt;The 30-Second Fix&lt;/h2&gt;
&lt;p&gt;This applies to &lt;strong&gt;Linux hosts running Docker Engine directly&lt;/strong&gt;. If you&apos;re using Docker Desktop (macOS or Windows), you&apos;re not affected — updates are bundled automatically.&lt;/p&gt;
&lt;p&gt;If your services are down right now and you need them back immediately, do this:&lt;/p&gt;
&lt;p&gt;Edit (or create) &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/docker/daemon.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;min-api-version&quot;: &quot;1.24&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you already have content in &lt;code&gt;daemon.json&lt;/code&gt; (storage driver, log driver, etc.), merge the key into the existing object. Don&apos;t overwrite your whole config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;log-driver&quot;: &quot;json-file&quot;,
	&quot;log-opts&quot;: {
		&quot;max-size&quot;: &quot;10m&quot;,
		&quot;max-file&quot;: &quot;3&quot;
	},
	&quot;min-api-version&quot;: &quot;1.24&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The most common mistake is a missing or trailing comma. JSON is strict about this.&lt;/p&gt;
&lt;p&gt;Then restart Docker:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Restarting Docker stops all running containers. If your containers have a restart policy (&lt;code&gt;restart: unless-stopped&lt;/code&gt; or &lt;code&gt;restart: always&lt;/code&gt;), they&apos;ll come back on their own. If not, you&apos;ll need to start them manually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Docker Swarm:&lt;/strong&gt; This fix works the same on Swarm nodes. Apply it to all managers and workers. Swarm services will reschedule automatically after the daemon restarts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rootless Docker?&lt;/strong&gt; The config file lives at &lt;code&gt;~/.config/docker/daemon.json&lt;/code&gt; and you restart with &lt;code&gt;systemctl --user restart docker&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That&apos;s it. Your services should come back within seconds. Traefik will reconnect to the Docker daemon, discover your containers, and routes will light up again.&lt;/p&gt;
&lt;h3&gt;Verifying it worked&lt;/h3&gt;
&lt;p&gt;Check Traefik&apos;s logs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker logs traefik --tail 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should &lt;strong&gt;no longer&lt;/strong&gt; see the &lt;code&gt;client version 1.24 is too old&lt;/code&gt; error. If you see &lt;code&gt;Configuration received from provider docker&lt;/code&gt;, that&apos;s a positive confirmation (this message only appears at INFO log level, which may not be your default).&lt;/p&gt;
&lt;p&gt;The strongest verification is the API version check below.&lt;/p&gt;
&lt;p&gt;Confirm your Docker API versions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker version --format &apos;{{.Server.MinAPIVersion}}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should now return &lt;code&gt;1.24&lt;/code&gt; instead of &lt;code&gt;1.44&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt; The &lt;code&gt;min-api-version&lt;/code&gt; setting overrides Docker&apos;s built-in floor. It tells the daemon to accept connections from clients using API version 1.24 and above, restoring the backward compatibility that Docker 29 removed. This config change keeps Docker 29 fully operational while re-enabling communication with older API clients. You&apos;re still running Docker 29 with all its features — you&apos;ve just told it to accept a wider range of client versions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is this safe?&lt;/strong&gt; For now, yes. This setting only changes how Docker negotiates API versions with clients. The API version is a negotiation protocol that controls feature detection between client and server. It doesn&apos;t touch encryption or authentication. The risk is that older API clients might miss newer fields or features, but Traefik only needs to read container labels and inspect networks. It doesn&apos;t need API 1.44 for that.&lt;/p&gt;
&lt;h2&gt;The Real Fix: Update Traefik&lt;/h2&gt;
&lt;p&gt;The daemon.json workaround is a bandage. The actual fix is updating Traefik to &lt;strong&gt;v3.6.1 or later&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Traefik v3.6.1, released shortly after this incident, added automatic Docker API version negotiation. Instead of hardcoded v1.24, Traefik now queries the daemon for its supported version range and picks the highest mutually compatible version. This is how the Docker SDK is supposed to work.&lt;/p&gt;
&lt;p&gt;If you manage Traefik directly (docker-compose, Dockerfile, etc.), change your image tag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  traefik:
    image: traefik:v3.6.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or pull the latest:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull traefik:v3.6.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then restart:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d traefik
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re on the older standalone &lt;code&gt;docker-compose&lt;/code&gt;, the command is &lt;code&gt;docker-compose up -d traefik&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Once Traefik is on 3.6.1+, you can remove the &lt;code&gt;&quot;min-api-version&quot;&lt;/code&gt; workaround from &lt;code&gt;daemon.json&lt;/code&gt; and restart Docker again. Everything will keep working because Traefik now negotiates the API version correctly on its own.&lt;/p&gt;
&lt;h3&gt;If you can&apos;t update Traefik directly&lt;/h3&gt;
&lt;p&gt;If you&apos;re running a platform that bundles Traefik (Coolify, Dokploy, Appwrite, CapRover), you might not control the Traefik image version. In that case:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check if your platform has an update.&lt;/strong&gt; Most of them shipped patches within days.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use the daemon.json workaround&lt;/strong&gt; until the platform updates its bundled Traefik.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check the platform&apos;s GitHub issues.&lt;/strong&gt; This broke so many deployments that every affected platform has a thread about it.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;What Actually Happened&lt;/h2&gt;
&lt;p&gt;Let me explain the technical chain of events, because understanding it helps you prevent similar breakages.&lt;/p&gt;
&lt;h3&gt;Docker&apos;s API version floor&lt;/h3&gt;
&lt;p&gt;Docker Engine exposes a versioned API. Clients (CLI, SDK, third-party tools) negotiate which version to use during the initial handshake. Before Docker 29, the daemon supported API versions going all the way back to v1.24, which was introduced with Docker 1.12 in 2016. Nearly a decade of backward compatibility.&lt;/p&gt;
&lt;p&gt;With Docker Engine v29.0.0 (released November 10, 2025), Docker raised the minimum supported API version to &lt;strong&gt;v1.44&lt;/strong&gt;. API v1.44 corresponds to Docker Engine v25, released in late 2023.&lt;/p&gt;
&lt;p&gt;This was deliberate and documented. Docker&apos;s deprecation policy states that API versions are supported for a limited number of major releases. API 1.24 had been deprecated for years. Docker 29 finally enforced the deprecation.&lt;/p&gt;
&lt;p&gt;The result: any tool compiled against an older Docker SDK that defaults to API v1.24 as its negotiation floor gets rejected immediately. No fallback. No graceful degradation. A hard error: &lt;code&gt;client version 1.24 is too old&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Why Traefik specifically exploded&lt;/h3&gt;
&lt;p&gt;Traefik uses Docker&apos;s official Go SDK (&lt;code&gt;github.com/docker/docker/client&lt;/code&gt;) for container discovery. It reads container labels, figures out routing rules, and dynamically updates its configuration.&lt;/p&gt;
&lt;p&gt;The problem is in how the Go Docker SDK initializes. When you create a new client with &lt;code&gt;client.NewClientWithOpts()&lt;/code&gt;, the SDK defaults to negotiating from API v1.24. This has been the SDK&apos;s default since it was written. It was never a problem because Docker always accepted v1.24.&lt;/p&gt;
&lt;p&gt;Docker 29 stopped accepting it. The negotiation fails before Traefik can even list containers. No containers discovered means no routes configured means every request hits a 404 or 502.&lt;/p&gt;
&lt;p&gt;This affected &lt;strong&gt;all&lt;/strong&gt; Traefik versions through v3.6.0, including v2.x. Traefik was using an SDK default (API v1.24) that Docker 29 no longer accepts.&lt;/p&gt;
&lt;h2&gt;It&apos;s Not Just Traefik&lt;/h2&gt;
&lt;p&gt;Traefik got the most attention because a broken reverse proxy takes down everything behind it. But Docker 29&apos;s API floor broke a wide range of tools:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What Broke&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traefik&lt;/strong&gt; (v3.6.0 and earlier)&lt;/td&gt;
&lt;td&gt;Container discovery, all routing&lt;/td&gt;
&lt;td&gt;Update to v3.6.1+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Portainer&lt;/strong&gt; (before v2.33.5)&lt;/td&gt;
&lt;td&gt;Can&apos;t connect to Docker environments&lt;/td&gt;
&lt;td&gt;Update to v2.33.5+ (LTS) or v2.36.0+ (STS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Watchtower&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Can&apos;t query containers for updates&lt;/td&gt;
&lt;td&gt;Switch to &lt;code&gt;nickfedor/watchtower&lt;/code&gt; (original &lt;code&gt;containrrr/watchtower&lt;/code&gt; is unmaintained)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LazyDocker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker connection fails&lt;/td&gt;
&lt;td&gt;Update to v0.24.2+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dokploy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bundled Traefik fails&lt;/td&gt;
&lt;td&gt;Update Traefik image to v3.6.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coolify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bundled Traefik fails&lt;/td&gt;
&lt;td&gt;Check for Coolify update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Appwrite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bundled Traefik v2.11 fails&lt;/td&gt;
&lt;td&gt;Update to Appwrite version using Traefik v3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JetBrains IDEs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker plugin throws 400 errors&lt;/td&gt;
&lt;td&gt;Update IDE / Docker plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spring Boot&lt;/strong&gt; (Testcontainers)&lt;/td&gt;
&lt;td&gt;docker-java defaults to API v1.32&lt;/td&gt;
&lt;td&gt;Update to Spring Boot 3.5.8+ (includes workaround), or set &lt;code&gt;api.version=1.44&lt;/code&gt; in &lt;code&gt;docker-java.properties&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swarmpit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker API calls fail&lt;/td&gt;
&lt;td&gt;No fix: project is archived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CapRover&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker API connectivity&lt;/td&gt;
&lt;td&gt;Check for update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CasaOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker API connectivity&lt;/td&gt;
&lt;td&gt;Check for update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cAdvisor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Metrics collection stops&lt;/td&gt;
&lt;td&gt;Update to v0.53.0+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The pattern repeats across every entry: the tool uses a Docker SDK with a hardcoded minimum version below 1.44, Docker 29 rejects the connection, and the tool can&apos;t communicate with the daemon.&lt;/p&gt;
&lt;p&gt;Swarmpit is the saddest entry on that list. The project is archived. No fix is coming. Docker 29 finished what abandonment started.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;p&gt;For the record, here&apos;s how this played out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nov 10, 2025&lt;/strong&gt; — Docker Engine v29.0.0 released. Minimum API version raised to v1.44. Traefik immediately breaks for everyone who auto-updates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nov 10-11, 2025&lt;/strong&gt; — Reports flood in. Traefik GitHub issue &lt;a href=&quot;https://github.com/traefik/traefik/issues/12253&quot;&gt;#12253&lt;/a&gt; is filed. The Docker Community Forums &lt;a href=&quot;https://forums.docker.com/t/docker-29-increased-minimum-api-version-breaks-traefik-reverse-proxy/150384&quot;&gt;thread&lt;/a&gt; explodes. The Traefik community forum fills up. Coolify, Dokploy, and Appwrite all see the same issue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nov 11, 2025&lt;/strong&gt; — Community identifies the &lt;code&gt;daemon.json&lt;/code&gt; workaround and the &lt;code&gt;DOCKER_MIN_API_VERSION&lt;/code&gt; environment variable as immediate workarounds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nov 12, 2025&lt;/strong&gt; — Traefik maintainer &lt;a href=&quot;https://github.com/traefik/traefik/pull/12256&quot;&gt;PR #12256&lt;/a&gt; adds automatic API version negotiation. Community test builds appear on Docker Hub.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nov 13-14, 2025&lt;/strong&gt; — Traefik v3.6.1 released with the fix. Portainer ships v2.33.5 and v2.36.0 with updated API support. LazyDocker ships v0.24.2.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nov 2025 onward&lt;/strong&gt; — The rest of the tooling world catches up. Spring Boot, JetBrains, and others ship fixes over the following weeks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From &quot;everything is broken&quot; to &quot;official fix available&quot;: roughly &lt;strong&gt;48 hours&lt;/strong&gt;. Solid open-source incident response. But if you were one of the people whose services went down on a Sunday night, it probably felt a lot longer.&lt;/p&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;h3&gt;For your infrastructure&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Pin your Docker version in production.&lt;/strong&gt; Docker 29&apos;s release notes documented the API version change. But if Docker auto-updates via your package manager (as it does on Ubuntu with the official repository), you get the breaking change whether you read the notes or not.&lt;/p&gt;
&lt;p&gt;On Ubuntu/Debian:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-mark hold docker-ce docker-ce-cli containerd.io
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you&apos;re ready to upgrade, unpin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-mark unhold docker-ce docker-ce-cli containerd.io
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pin your Traefik version too.&lt;/strong&gt; Using &lt;code&gt;traefik:latest&lt;/code&gt; means you get updates automatically, which is usually fine, but it also means you can&apos;t predict when behavior changes. Use explicit version tags like &lt;code&gt;traefik:v3.6.1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep a daemon.json backup.&lt;/strong&gt; This file is tiny but critical. If you have it backed up or version-controlled, you can apply the workaround in under a minute instead of googling for it.&lt;/p&gt;
&lt;h3&gt;For the broader picture&lt;/h3&gt;
&lt;p&gt;A huge number of Docker-adjacent tools depended on an API version Docker deprecated years ago. The SDK default of v1.24 was never updated because Docker was always backward-compatible. Tools that were actively maintained shipped fixes within days. Tools that weren&apos;t (Swarmpit, older CasaOS setups) are now permanently broken on Docker 29+.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;min-api-version&lt;/code&gt; escape hatch is Docker acknowledging the pain with a temporary lever. Eventually, even that setting may go away.&lt;/p&gt;
&lt;p&gt;If you maintain a tool that talks to Docker, check your SDK&apos;s default API version negotiation. If it&apos;s hardcoded to a minimum, make it dynamic. The Traefik PR that fixed this was a few dozen lines of code.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Update Traefik. Pin your Docker version. The order is up to you.&lt;/p&gt;
</content:encoded></item><item><title>Why I Built ZaString</title><link>https://corentings.dev/blog/why-i-built-zastring/</link><guid isPermaLink="true">https://corentings.dev/blog/why-i-built-zastring/</guid><description>On zero allocations, Span&lt;T&gt;, and the pursuit of performance without sacrifice.</description><pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ZaString started with a frustration. I was working on a high-throughput system in C#, and every profiler run showed the same thing: strings everywhere. Allocations piling up. The garbage collector working overtime. Not because the code was wrong, but because strings in C# are immutable and every operation creates a new one.&lt;/p&gt;
&lt;h2&gt;The Problem With Strings&lt;/h2&gt;
&lt;p&gt;Here&apos;s what I mean. Take something innocent-looking:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var label = $&quot;User {name} ({age}) logged in at {DateTime.Now:t}&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One line. Clean. Readable. And underneath, it allocates. The interpolated string creates a &lt;code&gt;string&lt;/code&gt; on the heap. The &lt;code&gt;DateTime.Now:t&lt;/code&gt; formatting creates another. The &lt;code&gt;name&lt;/code&gt; substring reference adds overhead. Even something as simple as &lt;code&gt;str1 + str2&lt;/code&gt; doesn&apos;t modify in place. It allocates a brand new string with the combined content.&lt;/p&gt;
&lt;p&gt;In C#, strings are immutable. That&apos;s not a flaw. It&apos;s a deliberate design choice that makes them thread-safe and predictable. But immutability has a cost: every operation that &quot;changes&quot; a string actually creates a new one. Concatenation, formatting, trimming, splitting, they all allocate.&lt;/p&gt;
&lt;p&gt;For most code, this is fine. The garbage collector handles it. You never notice. But in hot paths like game loops, UI rendering, or request handlers processing thousands per second, those allocations add up. I was working with ImGui, where every frame builds strings for labels, tooltips, and debug output. Frame after frame, sixty times a second. The profiler wasn&apos;t subtle about it: strings dominated the allocation graph.&lt;/p&gt;
&lt;p&gt;I tried the usual fixes. &lt;code&gt;StringBuilder&lt;/code&gt; for concatenation. &lt;code&gt;string.Create()&lt;/code&gt; for pre-sized allocations. &lt;code&gt;ArrayPool&amp;lt;char&amp;gt;&lt;/code&gt; for reuse. They helped, but they all still allocated on the heap eventually. The fundamental issue was that the output was always a &lt;code&gt;string&lt;/code&gt;, and strings live on the heap.&lt;/p&gt;
&lt;p&gt;I didn&apos;t want to optimize allocation. I wanted to eliminate it.&lt;/p&gt;
&lt;h2&gt;Enter Span&amp;lt;T&amp;gt;&lt;/h2&gt;
&lt;p&gt;.NET 2.1 introduced &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt;, and it changed what was possible.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt; is a view over a contiguous region of memory. It can wrap a managed array, a stack-allocated buffer, or unmanaged memory. Crucially, it&apos;s a &lt;code&gt;ref struct&lt;/code&gt;. That means it lives on the stack. It can&apos;t be boxed, can&apos;t be stored in a field, can&apos;t be used across async boundaries. The runtime enforces this at compile time.&lt;/p&gt;
&lt;p&gt;What you get in exchange is zero-allocation memory access. Slicing a span doesn&apos;t copy. It just creates a new view with an adjusted offset and length. Writing into a span modifies the underlying memory directly. No GC pressure. No hidden allocations.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Span&amp;lt;char&amp;gt; buffer = stackalloc char[64];
var written = &quot;Hello&quot;.AsSpan().CopyTo(buffer);
var slice = buffer[..5]; // zero-copy view
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was the insight: instead of building strings and letting the GC clean up, write directly into a buffer you already own. The buffer can be on the stack for small strings, or pooled for larger ones. When you&apos;re done, you have a &lt;code&gt;ReadOnlySpan&amp;lt;char&amp;gt;&lt;/code&gt;, which is just a view over the result with no heap allocation at all.&lt;/p&gt;
&lt;p&gt;The catch is that working with raw spans is verbose. There&apos;s no fluent API, no formatting helpers, no interpolation. You&apos;re manually tracking offsets and calling &lt;code&gt;TryFormat&lt;/code&gt; on every value. It works, but it doesn&apos;t feel like writing C#. It feels like assembly with extra steps.&lt;/p&gt;
&lt;p&gt;I wanted the ergonomics of &lt;code&gt;StringBuilder&lt;/code&gt; with the allocation profile of &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt;. That&apos;s where ZaString came from.&lt;/p&gt;
&lt;h2&gt;The Design Philosophy&lt;/h2&gt;
&lt;p&gt;The core idea behind ZaString is simple: &lt;strong&gt;zero-allocation should feel normal.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Compare the two:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// StringBuilder — 146 ns, 480 B allocated
var sb = new StringBuilder();
sb.Append(&quot;Name: &quot;).Append(&quot;John&quot;).Append(&quot;, Age: &quot;).Append(25);
var result = sb.ToString();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// ZaSpanStringBuilder — 115 ns, 0 B allocated
Span&amp;lt;char&amp;gt; buffer = stackalloc char[50];
var builder = ZaSpanStringBuilder.Create(buffer);
builder.Append(&quot;Name: &quot;).Append(&quot;John&quot;).Append(&quot;, Age: &quot;).Append(25);
var result = builder.AsSpan();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same shape. Same chaining. Same mental model. But the second version never touches the heap. The buffer is stack-allocated, the builder is a &lt;code&gt;ref struct&lt;/code&gt;, and &lt;code&gt;AsSpan()&lt;/code&gt; returns a view, not a copy.&lt;/p&gt;
&lt;p&gt;The fluent API was non-negotiable. Every &lt;code&gt;Append&lt;/code&gt; returns &lt;code&gt;ref this&lt;/code&gt;, so you can chain calls without creating intermediate references. It supports all the types you&apos;d expect: strings, chars, numbers, booleans, dates, with formatting and culture providers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder.Append(&quot;Pi: &quot;).Append(Math.PI, &quot;F2&quot;)
       .Append(&quot;, Date: &quot;).Append(DateTime.Now, &quot;yyyy-MM-dd&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C# 10&apos;s interpolated string handlers make it even cleaner:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder.Append($&quot;User: {name}, Age: {age}, Pi: {Math.PI:F2}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The handler intercepts the interpolation at compile time and routes each placeholder directly into the span buffer. No intermediate string. No &lt;code&gt;Format()&lt;/code&gt; call. Just writes.&lt;/p&gt;
&lt;p&gt;I also wanted safety. Raw spans are dangerous. Write past the end and you corrupt memory. ZaString provides &lt;code&gt;TryAppend&lt;/code&gt; variants that return &lt;code&gt;false&lt;/code&gt; instead of throwing when the buffer is full. &lt;code&gt;TryAppendLine&lt;/code&gt; is atomic: if there isn&apos;t room for both the content and the newline, nothing gets written. No partial state. No corruption.&lt;/p&gt;
&lt;p&gt;The library grew from there. Escape helpers for JSON, HTML, URLs, CSV. Path and query parameter builders. A pooled builder for cases where the stack isn&apos;t enough. A UTF-8 writer for scenarios that need bytes instead of chars. Each piece follows the same principle: write into a buffer you control, return a view, allocate nothing.&lt;/p&gt;
&lt;h2&gt;What I Learned&lt;/h2&gt;
&lt;p&gt;The biggest lesson was about tradeoffs. Zero-allocation is not free. It&apos;s a constraint, and constraints shape design. &lt;code&gt;ref struct&lt;/code&gt; limitations mean you can&apos;t store a builder in a field, pass it to async methods, or return it from a function that escapes the stack. You have to think about buffer sizes upfront. You lose the convenience of &lt;code&gt;string&lt;/code&gt; being everywhere in the .NET ecosystem.&lt;/p&gt;
&lt;p&gt;But those constraints also force clarity. When you have to declare your buffer size, you think about how much data you actually handle. When you can&apos;t allocate, you design around reuse. The code becomes more intentional.&lt;/p&gt;
&lt;p&gt;I also learned that zero-allocation isn&apos;t always the answer. For a one-off log message, &lt;code&gt;string interpolation&lt;/code&gt; is fine. The GC won&apos;t notice. ZaString is for the hot paths, the code that runs thousands of times per frame, per request, per second. The profiler tells you where those paths are. Trust the profiler.&lt;/p&gt;
&lt;p&gt;On API design, I found that the best abstraction is the one you forget is there. If someone can read ZaString code and not realize it&apos;s zero-allocation, that&apos;s a win. The ergonomics should be invisible. The performance should be the default, not something you opt into with ceremony.&lt;/p&gt;
&lt;p&gt;And honestly? There&apos;s a specific kind of joy in running a benchmark and seeing the allocation column read &lt;strong&gt;0 B&lt;/strong&gt;. Not &quot;reduced.&quot; Not &quot;acceptable.&quot; Zero. The garbage collector has nothing to do. The memory doesn&apos;t move. The result is just... there, in the buffer, where you put it.&lt;/p&gt;
&lt;p&gt;Sometimes the best code is the code that doesn&apos;t happen.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;ZaString is available on &lt;a href=&quot;https://github.com/corentings/zastring&quot;&gt;GitHub&lt;/a&gt; and NuGet.&lt;/p&gt;
</content:encoded></item><item><title>Generic Methods Coming to Go</title><link>https://corentings.dev/blog/generic-methods-coming-to-go/</link><guid isPermaLink="true">https://corentings.dev/blog/generic-methods-coming-to-go/</guid><description>Go just accepted the proposal for generic methods on concrete types. Here&apos;s what changes, what doesn&apos;t, and why it matters.</description><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Generic Methods Coming to Go&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&quot;We do not anticipate that Go will ever add generic methods.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;— Go FAQ, for roughly a decade.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That line sat in the FAQ like a monument. It wasn&apos;t a &quot;not yet.&quot; It was a &quot;never.&quot; And for years, every time someone opened an issue asking for type parameters on methods, that quote was the mic drop that ended the conversation.&lt;/p&gt;
&lt;p&gt;Then, in January 2026, Robert Griesemer opened &lt;a href=&quot;https://github.com/golang/go/issues/77273&quot;&gt;proposal #77273&lt;/a&gt;: &quot;Generic methods for concrete types.&quot; The label was added: &lt;strong&gt;Proposal-Accepted&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before you get too excited: this is an accepted proposal, not a shipped feature. You cannot use this in any Go release today.&lt;/strong&gt; That said, the path forward is clear, and understanding what&apos;s coming, and what isn&apos;t, is worth your time now.&lt;/p&gt;
&lt;h2&gt;The Status Quo: The Anti-Pattern We All Live With&lt;/h2&gt;
&lt;p&gt;Since Go 1.18 gave us generics, there&apos;s been a frustrating gap. You can write generic functions and generic types, but you can&apos;t combine them into generic &lt;em&gt;methods&lt;/em&gt;. The method set of a generic type can only contain non-generic methods.&lt;/p&gt;
&lt;p&gt;This means many post-1.18 Go codebases have functions like these scattered around:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Cache[K comparable, V any] struct {
    items map[K]V
}

// This is fine: non-generic method on a generic type.
func (c *Cache[K, V]) Get(key K) (V, bool) {
    v, ok := c.items[key]
    return v, ok
}

// But you can&apos;t do this:
// func (c *Cache[K, V]) Transform[T any](fn func(V) T) []T { ... }

// So you end up with this:
func Transform[K comparable, V any, T any](c *Cache[K, V], fn func(V) T) []T {
    result := make([]T, 0, len(c.items))
    for _, v := range c.items {
        result = append(result, fn(v))
    }
    return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That last function takes the receiver as its first argument. It&apos;s not a method. It doesn&apos;t show up on autocomplete, can&apos;t be chained, and breaks the left-to-right reading flow that methods provide. &lt;code&gt;cache.Transform(...)&lt;/code&gt; reads naturally; &lt;code&gt;Transform(cache, ...)&lt;/code&gt; reads like inside-out code. This isn&apos;t just aesthetics: it makes APIs harder to discover, document, and compose.&lt;/p&gt;
&lt;h2&gt;Why This Became Possible&lt;/h2&gt;
&lt;p&gt;The reason generic methods were blocked for so long wasn&apos;t technical laziness. It was a genuine design problem.&lt;/p&gt;
&lt;p&gt;The original Type Parameters proposal discussed generic methods and rejected them. The reasoning went like this: if methods can have type parameters, then interface methods should too. And generic interface methods are genuinely hard. They break type erasure, create dynamic dispatch problems, and don&apos;t play well with reflection. So the whole idea was shelved.&lt;/p&gt;
&lt;p&gt;What changed? Griesemer&apos;s insight, captured in the proposal, is disarmingly simple:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&quot;Concrete methods are a language feature that is useful in itself, irrespective of interfaces.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Methods do two things: they implement interfaces, &lt;em&gt;and&lt;/em&gt; they organize code on a type. The Go team had been conflating these two roles. Once you separate them, the problem dissolves:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// This is what&apos;s being added (concrete type, generic method):
func (s *MySlice[E]) Map[T any](fn func(E) T) []T

// This is NOT being added (interface method with type parameters):
type Reader[E any] interface {
    Read[E]()  // Still impossible
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first one is straightforward. The compiler knows the concrete type at compile time, so it can statically resolve the method using the existing type argument mechanism. No dynamic dispatch, no reflection, no runtime complexity.&lt;/p&gt;
&lt;p&gt;The second one remains unsolved because &lt;code&gt;Reader[E]&lt;/code&gt; doesn&apos;t name a single method. It names a family of methods, and Go&apos;s interface satisfaction model can&apos;t handle that.&lt;/p&gt;
&lt;p&gt;This decoupling is the key insight. Generic methods on concrete types were always implementable. They were just held hostage to the harder problem of generic interface methods.&lt;/p&gt;
&lt;p&gt;The history matters here. &lt;a href=&quot;https://github.com/golang/go/issues/49085&quot;&gt;Issue #49085&lt;/a&gt;, opened in October 2021 by the community, accumulated over 900 positive reactions. It was the primary pressure point. &lt;a href=&quot;https://github.com/golang/go/issues/50981&quot;&gt;Issue #50981&lt;/a&gt; followed in February 2022 with simpler motivating examples. Both were effectively deferred. Until now.&lt;/p&gt;
&lt;h2&gt;The Syntax&lt;/h2&gt;
&lt;p&gt;The grammar change is minimal. The method declaration production gains an optional type parameter list:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MethodDecl = &quot;func&quot; Receiver MethodName [ TypeParameters ] Signature [ FunctionBody ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In practice:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Stack[T any] struct {
    items []T
}

// Regular method on a generic type (valid since Go 1.18):
func (s *Stack[T]) Push(items ...T) {
    s.items = append(s.items, items...)
}

// Generic method with its own type parameter: new!
func (s *Stack[T]) Map[U any](fn func(T) U) *Stack[U] {
    result := &amp;amp;Stack[U]{}
    for _, item := range s.items {
        result.Push(fn(item))
    }
    return result
}

// Calling it:
nums := &amp;amp;Stack[int]{}
nums.Push(1, 2, 3)
strs := nums.Map[string](func(n int) string { return strconv.Itoa(n) })

// Type inference works:
strs = nums.Map(strconv.Itoa)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There&apos;s a subtler grammar change too: &lt;code&gt;TypeArgs&lt;/code&gt; moves from &lt;code&gt;Operand&lt;/code&gt; to &lt;code&gt;PrimaryExpr&lt;/code&gt;. This is what makes &lt;code&gt;expr.Method[T](args)&lt;/code&gt; parseable. The type arguments attach to the method call expression, not just to identifiers.&lt;/p&gt;
&lt;p&gt;Method expressions and method values work as you&apos;d expect, with one catch:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Method expression: Stack[int].Map produces a generic function
// with signature [U any](*Stack[int], func(int) U) *Stack[U]

// Method value: s.Map produces [U any] func(func(int) U) *Stack[U]

// But this is INVALID: you must instantiate the type first:
// Stack.Map[U any]  // ERROR: Stack is not instantiated
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Boundary: What This Does NOT Do&lt;/h2&gt;
&lt;p&gt;After seeing the syntax, the natural next question is: &lt;em&gt;&quot;Can I use this in interfaces?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;Generic interface methods are explicitly out of scope. This is a feature that solves the right problem first. The interface problem is a different, harder one, and leaving it unsolved here doesn&apos;t preclude a future proposal.&lt;/p&gt;
&lt;h2&gt;The Case Against&lt;/h2&gt;
&lt;p&gt;I&apos;d be dishonest if I didn&apos;t engage with the real objections.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Go said never. Why should we trust them now?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The FAQ reversal is uncomfortable. When language designers draw a line, reversing it risks eroding trust. Some developers chose Go &lt;em&gt;because&lt;/em&gt; of its restraint. Adding features incrementally is one thing; reversing explicit &quot;never&quot; statements is another.&lt;/p&gt;
&lt;p&gt;My read: the original resistance was principled but overly conservative. The reasoning was &quot;generic methods require generic interface methods,&quot; and that premise turned out to be false. Changing course when you discover a false premise isn&apos;t flip-flopping. It&apos;s good engineering. The Go team has a track record of shipping minimal, well-thought-out language changes. I&apos;d rather they correct a mistake than defend it out of pride.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;This adds complexity for marginal benefit.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the strongest objection. Go&apos;s complexity budget is real. Every new feature makes the language harder to learn, harder to tool, and harder to reason about. Is the convenience of &lt;code&gt;cache.Transform(...)&lt;/code&gt; worth the cognitive cost of another grammar production?&lt;/p&gt;
&lt;p&gt;I think so, for two reasons. First, the implementation cost is surprisingly low. The compiler can statically resolve generic methods on concrete types without any runtime changes. This isn&apos;t adding a new dispatch mechanism; it&apos;s removing a parsing restriction. Second, the ergonomic benefit is disproportionate. I keep running into the &lt;code&gt;(receiver, typeParam)&lt;/code&gt; pattern in post-1.18 Go codebases. Fixing it at the language level eliminates a class of API awkwardness that I encounter daily.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;This opens the door to generic interface methods, and then we&apos;re Java.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Slippery slope arguments are easy to make. But the proposal explicitly addresses this: &lt;em&gt;&quot;It also doesn&apos;t preclude the implementation of generic interface methods at some point, should we find an acceptable implementation solution.&quot;&lt;/em&gt; This is honest. It doesn&apos;t promise never, and it doesn&apos;t promise soon. It leaves the door open for a future proposal that makes its own case.&lt;/p&gt;
&lt;p&gt;I&apos;d argue the opposite slippery slope: if Go &lt;em&gt;doesn&apos;t&lt;/em&gt; ship generic methods on concrete types, the pressure for full generic interface methods only grows. Shipping this targeted feature might actually &lt;em&gt;reduce&lt;/em&gt; demand for the more complex one by solving the practical pain point.&lt;/p&gt;
&lt;h2&gt;Practical Examples&lt;/h2&gt;
&lt;p&gt;Let me show you what this actually looks like in code you might write.&lt;/p&gt;
&lt;h3&gt;Type-Safe Builders&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type Query[T any] struct {
    filters []func(T) bool
    limit   int
}

func NewQuery[T any]() *Query[T] {
    return &amp;amp;Query[T]{limit: -1}
}

func (q *Query[T]) Where(pred func(T) bool) *Query[T] {
    q.filters = append(q.filters, pred)
    return q
}

func (q *Query[T]) Limit(n int) *Query[T] {
    q.limit = n
    return q
}

func (q *Query[T]) Run(items []T) []T {
    // Applies all filters and returns matching items (implementation omitted)
}

// Usage: notice how the type parameter flows through the chain:
type User struct {
    Name string
    Age  int
}

users := []User{{&quot;Alice&quot;, 30}, {&quot;Bob&quot;, 25}, {&quot;Charlie&quot;, 30}}
activeAdults := NewQuery[User]().
    Where(func(u User) bool { return u.Age &amp;gt;= 18 }).
    Where(func(u User) bool { return u.Name != &quot;&quot; }).
    Limit(10).
    Run(users)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Today, you&apos;d need to make &lt;code&gt;Where&lt;/code&gt; a package-level function that takes the query as its first argument. The chaining breaks. The type parameter doesn&apos;t flow naturally. With generic methods, the builder pattern becomes first-class.&lt;/p&gt;
&lt;h3&gt;GroupBy: A Method-Level Type Parameter&lt;/h3&gt;
&lt;p&gt;Here&apos;s a case where the method genuinely needs its own type parameter, unrelated to the receiver&apos;s:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Table[R any] struct {
    rows []R
}

func (t *Table[R]) GroupBy[K comparable](keyFn func(R) K) map[K][]R {
    groups := make(map[K][]R)
    for _, row := range t.rows {
        k := keyFn(row)
        groups[k] = append(groups[k], row)
    }
    return groups
}

// Usage:
orders := &amp;amp;Table[Order]{rows: allOrders}
byCustomer := orders.GroupBy[string](func(o Order) string { return o.CustomerID })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The type parameter &lt;code&gt;K&lt;/code&gt; belongs to &lt;code&gt;GroupBy&lt;/code&gt;, not to &lt;code&gt;Table&lt;/code&gt;. This is the kind of thing you simply cannot express as a method today. With generic methods, it becomes natural.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;List[E].Format[F]&lt;/code&gt; Pattern&lt;/h3&gt;
&lt;p&gt;This is the motivating example from the proposal itself: a method that introduces its own type parameter independent of the type&apos;s parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type List[E any] struct {
    elements []E
}

func (l *List[E]) Format[F any](formatter func(E) F) []F {
    result := make([]F, len(l.elements))
    for i, e := range l.elements {
        result[i] = formatter(e)
    }
    return result
}

// E is string, F is int: two independent type parameters
names := &amp;amp;List[string]{elements: []string{&quot;hello&quot;, &quot;world&quot;}}
lengths := names.Format[int](func(s string) int { return len(s) })
// lengths == []int{5, 5}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the case that&apos;s truly impossible to express cleanly today. The method needs a type parameter (&lt;code&gt;F&lt;/code&gt;) that&apos;s unrelated to the type&apos;s parameter (&lt;code&gt;E&lt;/code&gt;). A package-level function can do it, but then you lose the method call syntax entirely.&lt;/p&gt;
&lt;h2&gt;How to Prepare Today&lt;/h2&gt;
&lt;p&gt;You can&apos;t use generic methods yet, but you can write code that migrates trivially when they land.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Wrap your &lt;code&gt;(receiver, typeParam)&lt;/code&gt; functions on types.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you have:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Transform[K comparable, V any, T any](c *Cache[K, V], fn func(V) T) []T {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep the type as the first argument. When generic methods ship, the migration is mechanical: move the function onto the type, drop the receiver parameter, done. The function body doesn&apos;t change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Stop reaching for &lt;code&gt;interface{}&lt;/code&gt; workarounds.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you&apos;re using empty interfaces to work around the inability to write generic methods, stop. Design your types with proper type parameters now. The code will work today with package-level functions and become cleaner tomorrow with methods.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Name your helper functions after the future method.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you plan to add a &lt;code&gt;Transform&lt;/code&gt; method to your &lt;code&gt;Cache&lt;/code&gt; type, name the package-level function &lt;code&gt;Transform&lt;/code&gt;, not &lt;code&gt;TransformCache&lt;/code&gt; or &lt;code&gt;ApplyToCache&lt;/code&gt;. Mechanical migration depends on name alignment.&lt;/p&gt;
&lt;h2&gt;When Will It Land?&lt;/h2&gt;
&lt;p&gt;Here&apos;s where things stand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Parser:&lt;/strong&gt; Already handles type parameters on methods (parses them, currently rejects with an error). The change to accept them is trivial.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type checker:&lt;/strong&gt; Needs modifications to remove the current restriction and handle method expressions/values with type parameters. In progress.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compiler backend:&lt;/strong&gt; Generic method calls on concrete types can be statically resolved and rewritten as generic function calls. This is well-understood: no new dispatch mechanism needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Export/import format:&lt;/strong&gt; This is the hardest part. The serialized format for compiled packages needs to represent generic methods, and changing it affects tooling across the ecosystem (gopls, vulncheck, build cache compatibility). This is what determines the timeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;x/tools&lt;/code&gt; repository already has &lt;a href=&quot;https://github.com/golang/go/issues/77549&quot;&gt;a tracking issue (#77549)&lt;/a&gt; for generic method support across the toolchain. The Go team typically allows one or two release cycles for tooling to catch up after a language change.&lt;/p&gt;
&lt;p&gt;Go 1.27 or 1.28 feels realistic. This is a well-scoped change with a clear implementation path, not a years-long research project like the original generics design.&lt;/p&gt;
&lt;h2&gt;My Take&lt;/h2&gt;
&lt;p&gt;I&apos;m genuinely excited about this, and not just because I get to delete a category of awkward functions from my codebases.&lt;/p&gt;
&lt;p&gt;What I appreciate about this proposal is the discipline. The Go team could have swung for the fences and tried to solve generic interface methods too. Instead, they identified the part that&apos;s well-understood, implementable, and high-value, and they scoped &lt;em&gt;just that&lt;/em&gt;. That&apos;s the kind of restraint that made Go worth using in the first place.&lt;/p&gt;
&lt;p&gt;What I&apos;d warn against: don&apos;t use generic methods as a hammer. If a package-level generic function reads clearly, keep it as a function. Methods aren&apos;t inherently better. They&apos;re better when they improve API discoverability and composability. Use the feature where it earns its complexity budget, not everywhere it&apos;s syntactically legal.&lt;/p&gt;
&lt;p&gt;Go&apos;s relationship with generics has been cautious, sometimes frustratingly so. But this proposal feels like the right next step: small, principled, and immediately useful. The FAQ was wrong. That&apos;s okay. Recognizing a mistake and correcting it is better than defending it indefinitely.&lt;/p&gt;
</content:encoded></item><item><title>I Write, I Code, I Explore — Why Verbs, Not Nouns</title><link>https://corentings.dev/blog/verbs-not-nouns/</link><guid isPermaLink="true">https://corentings.dev/blog/verbs-not-nouns/</guid><description>On defining yourself by what you do, not what you are.</description><pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I was listening to one of my favorite episodes of the ChessMood Podcast from my friend GM Avetik, &lt;a href=&quot;https://www.youtube.com/watch?v=pVHyUzX5IQw&amp;amp;t=4011s&quot;&gt;How you can perform at your peak like top athletes do | Todd Herman on ChessMood Podcast&lt;/a&gt;, and around 01h07 one small idea from it kept following me around.&lt;/p&gt;
&lt;p&gt;He said he prefers verbs to nouns when he talks about himself.&lt;/p&gt;
&lt;p&gt;Not &quot;I&apos;m a coach,&quot; but &quot;I coach.&quot;
Not &quot;I&apos;m an entrepreneur,&quot; but &quot;I build.&quot;
Not &quot;I&apos;m an author,&quot; but &quot;I write.&quot;&lt;/p&gt;
&lt;p&gt;The more I sat with that, the more it explained a tension I&apos;ve felt for years.&lt;/p&gt;
&lt;p&gt;There are days when &quot;developer&quot; feels too heavy a word for me. I can ship something useful in the morning, get stuck on a dumb bug in the afternoon, and end the day feeling like I should hand back the badge. The same thing happens with writing. If I tell myself &quot;I am a writer,&quot; every weak paragraph feels like evidence against me. But if I say &quot;I write,&quot; the whole thing softens. I wrote today. Some of it was good. Some of it wasn&apos;t. Both can be true.&lt;/p&gt;
&lt;p&gt;That&apos;s what I like about verbs: they leave room for motion.&lt;/p&gt;
&lt;p&gt;Nouns can be useful. They help other people understand roughly where you live in the world. But they also freeze you at strange moments. They can turn yesterday&apos;s result into today&apos;s identity. &quot;I am this kind of person.&quot; &quot;I am this level.&quot; &quot;I am this role.&quot; And once that sentence hardens, you start protecting it. You hesitate to be bad at something new because it might threaten the name you&apos;ve chosen for yourself.&lt;/p&gt;
&lt;p&gt;Verbs don&apos;t ask for that kind of defense. Verbs ask for practice.&lt;/p&gt;
&lt;p&gt;&quot;I write&quot; means I can write something sharp one day and clumsy the next. &quot;I code&quot; means I can build a feature, break something obvious, learn, fix it, and keep going. The verb doesn&apos;t collapse because the performance wasn&apos;t perfect. In a quiet way, it&apos;s a more merciful grammar.&lt;/p&gt;
&lt;p&gt;What stayed with me most from Todd Herman&apos;s framing was that this changes across contexts. The verb that describes how I work is not always the same one that describes how I love people. In one part of life, maybe I build. In another, I listen. In another, I encourage. The noun tries to gather everything into one static identity. The verb asks a better question: what am I actually doing here?&lt;/p&gt;
&lt;p&gt;That question feels closer to how life is really lived.&lt;/p&gt;
&lt;p&gt;I don&apos;t want to be trapped by a title that only describes me on my best days. I&apos;d rather use words that still fit on ordinary days. I wrote bad code today. I rewrote a paragraph five times. I explored an idea without being sure it would go anywhere. None of that cancels the practice. It is the practice.&lt;/p&gt;
&lt;p&gt;And there is something quietly reassuring in that. If a title disappears, the verb often remains. If one day I stop calling myself a developer, I can still build. If I stop calling myself a writer, I can still write. The label may change with the season. The work can keep moving.&lt;/p&gt;
&lt;p&gt;So I find myself trusting verbs more.&lt;/p&gt;
&lt;p&gt;Not because nouns are evil, and not because language alone can save us, but because verbs keep me closer to the living part of things. They remind me that I am not a finished category. I am a person in motion, practicing.&lt;/p&gt;
</content:encoded></item><item><title>Go Pipeline Pattern: Turning Streams into Useful Data</title><link>https://corentings.dev/blog/go-pattern-pipeline/</link><guid isPermaLink="true">https://corentings.dev/blog/go-pattern-pipeline/</guid><description>Learn the Pipeline Pattern in Go using goroutines and channels. Build composable stages for parsing, filtering, enriching, and processing log streams.</description><pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Pipeline Pattern in Go&lt;/h1&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Sometimes, the hard part of concurrent programming is not making things run in parallel.
The hard part is keeping the flow of data understandable.&lt;/p&gt;
&lt;p&gt;A pipeline is a simple way to do that.
Instead of putting every responsibility inside one large loop, you split the work into small stages.
Each stage receives values from a channel, does one transformation, and sends the result to the next stage.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source -&amp;gt; parse -&amp;gt; filter -&amp;gt; enrich -&amp;gt; sink
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Go, this pattern feels natural because goroutines and channels already give us the building blocks.
A generator can create the initial stream, each pipeline stage can transform it, and a final consumer can collect or print the result.&lt;/p&gt;
&lt;p&gt;This article continues the &lt;strong&gt;Go Patterns&lt;/strong&gt; series after Producer-Consumer, Generator, and Worker Pool.
The goal is not to build a framework.
The goal is to learn how to structure data processing without turning one function into a pile of responsibilities.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;p&gt;Use the Pipeline Pattern when data moves through multiple steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Processing log streams&lt;/li&gt;
&lt;li&gt;Reading, validating, and transforming CSV rows&lt;/li&gt;
&lt;li&gt;Cleaning API responses before storing them&lt;/li&gt;
&lt;li&gt;Building small ETL-like flows&lt;/li&gt;
&lt;li&gt;Splitting parsing, filtering, enrichment, and reporting into separate responsibilities&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pattern is especially useful when each step can be described as:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;take a stream of values, transform or filter it, and return another stream of values.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Why Use It&lt;/h2&gt;
&lt;p&gt;Pipelines are useful because they keep each stage focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Separation of concerns&lt;/strong&gt;: parsing, filtering, and reporting do not live in the same loop&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composability&lt;/strong&gt;: stages can be reused and reordered&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Natural backpressure&lt;/strong&gt;: channels slow down upstream stages when downstream stages cannot keep up&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testability&lt;/strong&gt;: each stage can be tested with a small input channel&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Readable concurrency&lt;/strong&gt;: the data flow is visible from the stage composition&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pattern is not magic.
It is mostly discipline: one stage, one responsibility.&lt;/p&gt;
&lt;h2&gt;How It Works&lt;/h2&gt;
&lt;p&gt;A pipeline stage usually has this shape:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func stage(in &amp;lt;-chan Input) &amp;lt;-chan Output {
    out := make(chan Output)

    go func() {
        defer close(out)

        for value := range in {
            out &amp;lt;- transform(value)
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The stage receives a read-only channel, creates its own output channel, starts a goroutine, then closes the output channel when the input channel is exhausted.&lt;/p&gt;
&lt;p&gt;This gives us a chain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;raw := source()
parsed := parse(raw)
filtered := filter(parsed)
enriched := enrich(filtered)
sink(enriched)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Simple Example&lt;/h2&gt;
&lt;p&gt;Before looking at logs, let&apos;s start with a tiny pipeline:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;numbers -&amp;gt; square -&amp;gt; keep even -&amp;gt; print
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func numbers(max int) &amp;lt;-chan int {
    out := make(chan int)

    go func() {
        defer close(out)

        for i := 1; i &amp;lt;= max; i++ {
            out &amp;lt;- i
        }
    }()

    return out
}

func square(in &amp;lt;-chan int) &amp;lt;-chan int {
    out := make(chan int)

    go func() {
        defer close(out)

        for n := range in {
            out &amp;lt;- n * n
        }
    }()

    return out
}

func keepEven(in &amp;lt;-chan int) &amp;lt;-chan int {
    out := make(chan int)

    go func() {
        defer close(out)

        for n := range in {
            if n%2 == 0 {
                out &amp;lt;- n
            }
        }
    }()

    return out
}

func main() {
    values := numbers(10)
    squared := square(values)
    evenSquares := keepEven(squared)

    for n := range evenSquares {
        fmt.Println(n)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each function owns one small part of the work.
&lt;code&gt;numbers&lt;/code&gt; produces values, &lt;code&gt;square&lt;/code&gt; transforms them, &lt;code&gt;keepEven&lt;/code&gt; filters them, and &lt;code&gt;main&lt;/code&gt; consumes the final stream.&lt;/p&gt;
&lt;p&gt;That is the pipeline pattern in its smallest useful form.&lt;/p&gt;
&lt;h2&gt;Real-World Example: Log Processing Pipeline&lt;/h2&gt;
&lt;p&gt;Now let&apos;s use a more realistic example.&lt;/p&gt;
&lt;p&gt;Imagine we receive raw log lines and want to turn them into useful information.
We can model that as a pipeline:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;raw log lines -&amp;gt; parse logs -&amp;gt; filter errors -&amp;gt; enrich logs -&amp;gt; print report
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a complete runnable version, this example needs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
    &quot;fmt&quot;
    &quot;strings&quot;
    &quot;time&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, define the data we want to pass between stages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type RawLog string

type LogEntry struct {
    Timestamp time.Time
    Level     string
    Service   string
    Message   string
    Alert     bool
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The source stage sends raw log lines:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func logSource(lines []string) &amp;lt;-chan RawLog {
    out := make(chan RawLog)

    go func() {
        defer close(out)

        for _, line := range lines {
            out &amp;lt;- RawLog(line)
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The parser turns each raw line into a structured &lt;code&gt;LogEntry&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func parseLogs(in &amp;lt;-chan RawLog) &amp;lt;-chan LogEntry {
    out := make(chan LogEntry)

    go func() {
        defer close(out)

        for raw := range in {
            parts := strings.SplitN(string(raw), &quot;|&quot;, 4)
            if len(parts) != 4 {
                continue
            }

            timestamp, err := time.Parse(time.RFC3339, parts[0])
            if err != nil {
                continue
            }

            out &amp;lt;- LogEntry{
                Timestamp: timestamp,
                Level:     parts[1],
                Service:   parts[2],
                Message:   parts[3],
            }
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The filter stage keeps only errors:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func filterErrors(in &amp;lt;-chan LogEntry) &amp;lt;-chan LogEntry {
    out := make(chan LogEntry)

    go func() {
        defer close(out)

        for entry := range in {
            if entry.Level == &quot;ERROR&quot; {
                out &amp;lt;- entry
            }
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The enrichment stage adds a small piece of derived information:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func enrichLogs(in &amp;lt;-chan LogEntry) &amp;lt;-chan LogEntry {
    out := make(chan LogEntry)

    go func() {
        defer close(out)

        for entry := range in {
            entry.Alert = entry.Service == &quot;payment&quot; || entry.Service == &quot;auth&quot;
            out &amp;lt;- entry
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, the sink consumes the enriched entries:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func printReport(in &amp;lt;-chan LogEntry) {
    for entry := range in {
        alert := &quot;&quot;
        if entry.Alert {
            alert = &quot; [ALERT]&quot;
        }

        fmt.Printf(&quot;%s %s: %s%s\n&quot;, entry.Service, entry.Level, entry.Message, alert)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full pipeline becomes very readable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    lines := []string{
        &quot;2026-04-24T10:00:00Z|INFO|api|request completed&quot;,
        &quot;2026-04-24T10:00:01Z|ERROR|payment|card authorization failed&quot;,
        &quot;2026-04-24T10:00:02Z|ERROR|worker|job timeout&quot;,
        &quot;2026-04-24T10:00:03Z|ERROR|auth|invalid token&quot;,
    }

    raw := logSource(lines)
    parsed := parseLogs(raw)
    errors := filterErrors(parsed)
    enriched := enrichLogs(errors)

    printReport(enriched)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is not the log format.
The important part is that the flow is explicit.&lt;/p&gt;
&lt;p&gt;Each stage can be read, tested, and replaced independently.&lt;/p&gt;
&lt;h2&gt;Error Handling&lt;/h2&gt;
&lt;p&gt;The log parser above silently skips invalid lines.
That keeps the example small, but it is not always what you want in production.&lt;/p&gt;
&lt;p&gt;Two common approaches are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Send errors to a separate error channel&lt;/li&gt;
&lt;li&gt;Pass a result type through the pipeline&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type LogResult struct {
    Entry LogEntry
    Err   error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes failures explicit without panicking inside a goroutine.
It also lets the final consumer decide whether to log, count, retry, or ignore invalid records.&lt;/p&gt;
&lt;h2&gt;Cancellation&lt;/h2&gt;
&lt;p&gt;The examples above work for finite inputs.
For long-running pipelines, use &lt;code&gt;context.Context&lt;/code&gt; so every stage can stop when the caller is done.&lt;/p&gt;
&lt;p&gt;The shape usually looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func parseLogs(ctx context.Context, in &amp;lt;-chan RawLog) &amp;lt;-chan LogEntry {
    out := make(chan LogEntry)

    go func() {
        defer close(out)

        for {
            select {
            case &amp;lt;-ctx.Done():
                return
            case raw, ok := &amp;lt;-in:
                if !ok {
                    return
                }

                entry, ok := parseLog(raw)
                if ok {
                    out &amp;lt;- entry
                }
            }
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without cancellation, a pipeline that reads from a never-ending source can leak goroutines when the consumer stops early.&lt;/p&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Keep each stage focused on one responsibility&lt;/li&gt;
&lt;li&gt;Return receive-only channels (&lt;code&gt;&amp;lt;-chan T&lt;/code&gt;) from stages&lt;/li&gt;
&lt;li&gt;Close the output channel from the goroutine that writes to it&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;context.Context&lt;/code&gt; for long-running or cancellable pipelines&lt;/li&gt;
&lt;li&gt;Test each stage independently with small input channels&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Forgetting to close output channels&lt;/li&gt;
&lt;li&gt;Stopping early without cancelling upstream stages&lt;/li&gt;
&lt;li&gt;Creating too many tiny stages that hide simple logic&lt;/li&gt;
&lt;li&gt;Mixing parsing, filtering, enrichment, and reporting in one function&lt;/li&gt;
&lt;li&gt;Assuming ordering will stay the same if you later parallelize a stage&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Related Patterns&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Generator Pattern: Creates the initial stream of values&lt;/li&gt;
&lt;li&gt;Producer-Consumer Pattern: Separates production from consumption&lt;/li&gt;
&lt;li&gt;Worker Pool Pattern: Parallelizes expensive stages&lt;/li&gt;
&lt;li&gt;Fan-Out/Fan-In Pattern: Distributes one stage across multiple workers and merges the results&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Pipeline Pattern is one of the most readable ways to structure data processing in Go.
It lets you split a flow into small stages, connect them with channels, and keep each responsibility isolated.&lt;/p&gt;
&lt;p&gt;It works well when data naturally moves through a sequence: read, parse, filter, enrich, report.&lt;/p&gt;
&lt;p&gt;The pattern is also a bridge to more advanced concurrency designs.
Once one stage becomes too slow, you can combine a pipeline with a Worker Pool or Fan-Out/Fan-In to parallelize only that part of the flow.&lt;/p&gt;
&lt;h2&gt;Testing Pipelines&lt;/h2&gt;
&lt;p&gt;The best pipeline tests treat each stage as a pure function: send a channel in, get a channel out. The goroutines are implementation details. I wrote about that approach in &lt;a href=&quot;/blog/tdd-permission-slip/&quot;&gt;TDD Isn&apos;t About Bugs — It&apos;s Your Permission to Refactor&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Series Navigation&lt;/h2&gt;
&lt;p&gt;This article is part of the &lt;strong&gt;Go Patterns&lt;/strong&gt; series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-worker/&quot;&gt;Mastering the Worker Pool Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;a href=&quot;/tags/go-patterns/&quot;&gt;Go Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Flexible Approaches to Worker Pools in Go</title><link>https://corentings.dev/blog/semaphore-pattern-worker/</link><guid isPermaLink="true">https://corentings.dev/blog/semaphore-pattern-worker/</guid><description>Explore flexible approaches to the Worker Pool pattern in Go, including the Shared Semaphore method and third-party libraries. Learn when to use each approach for optimal concurrency management in your Go projects.</description><pubDate>Thu, 12 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Flexible Approaches to Worker Pools in Go&lt;/h1&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;While the standard Worker Pool pattern is powerful, Go&apos;s flexibility allows for alternative approaches to concurrent task processing. This article explores the Shared Semaphore method and discusses the use of third-party libraries for managing concurrency in Go applications.&lt;/p&gt;
&lt;h2&gt;Shared Semaphore Method&lt;/h2&gt;
&lt;p&gt;The Shared Semaphore method uses a buffered channel as a semaphore to limit concurrency across various parts of an application.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;sync&quot;
	&quot;time&quot;
)

func processWithSemaphore(tasks []int, maxConcurrency int) {
	sem := make(chan struct{}, maxConcurrency)
	var wg sync.WaitGroup

	for _, task := range tasks {
		wg.Add(1)
		sem &amp;lt;- struct{}{} // Acquire semaphore
		go func(task int) {
			defer wg.Done()
			defer func() { &amp;lt;-sem }() // Release semaphore
			processTask(task)
		}(task)
	}

	wg.Wait()
}

func processTask(task int) {
	fmt.Printf(&quot;Processing task %d\n&quot;, task)
	time.Sleep(time.Second) // Simulate work
	fmt.Printf(&quot;Completed task %d\n&quot;, task)
}

func main() {
	tasks := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	processWithSemaphore(tasks, 3)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Advantages:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Simple and lightweight implementation&lt;/li&gt;
&lt;li&gt;Scales to zero when not in use&lt;/li&gt;
&lt;li&gt;Can be used in multiple places without increasing complexity&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Use Case:&lt;/h3&gt;
&lt;p&gt;Ideal for scenarios where you need to limit concurrency across various parts of your application without maintaining a persistent worker pool.&lt;/p&gt;
&lt;h2&gt;Third-Party Libraries&lt;/h2&gt;
&lt;p&gt;While implementing your own worker pool is often the best approach, there are some third-party libraries that can be useful in certain situations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ants&lt;/strong&gt;: A high-performance goroutine pool in Go, providing features like automatic scaling and reuse of goroutines.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;sourcegraph/conc&lt;/strong&gt;: A package for structured concurrency in Go, offering higher-level abstractions for common concurrency patterns.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These libraries can be beneficial when you need advanced features or when working on complex concurrent systems. However, it&apos;s important to note that in approximately 90% of cases, maintaining your own worker pool implementation is preferable. This approach gives you more control, better understanding of your concurrency model, and avoids unnecessary dependencies.&lt;/p&gt;
&lt;h2&gt;Choosing the Right Approach&lt;/h2&gt;
&lt;p&gt;Consider the following factors when deciding on your concurrency approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Assess your concurrency needs:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Do you need to limit concurrency across multiple parts of your application?&lt;/li&gt;
&lt;li&gt;Is your workload consistent or variable?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Evaluate your task characteristics:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Are tasks short-lived or long-running?&lt;/li&gt;
&lt;li&gt;Do you need fine-grained control over task execution?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Consider your application architecture:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Is simplicity a priority?&lt;/li&gt;
&lt;li&gt;Do you need to scale workers dynamically?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Decision tree:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;If (need to limit concurrency in specific sections) AND (variable workload):
Use Shared Semaphore&lt;/li&gt;
&lt;li&gt;Else if (consistent workload) AND (need fine-grained control):
Use Standard Worker Pool&lt;/li&gt;
&lt;li&gt;Else if (need advanced features) AND (complexity is justified):
Consider third-party libraries like Ants or sourcegraph/conc&lt;/li&gt;
&lt;li&gt;Else:
Implement and maintain your own worker pool&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This decision process helps guide your choice based on your specific context and requirements, ensuring you choose the most appropriate concurrency pattern for your needs while prioritizing simplicity and control in most cases.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;While the standard Worker Pool pattern is versatile, Go&apos;s concurrency model allows for flexible approaches like the Shared Semaphore method. This alternative can be particularly useful for applications with varying concurrency needs across different components. Third-party libraries offer advanced features but should be used judiciously, as maintaining your own worker pool often provides the best balance of control and simplicity.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This article provides an overview of flexible approaches to worker pools in Go. While these patterns are powerful, it&apos;s important to consider the specific needs of your application when implementing them. For production use, additional error handling and optimizations may be necessary.&lt;/p&gt;
&lt;p&gt;For more advanced concurrency patterns and best practices in Go, stay tuned for future articles! 🚀&lt;/p&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Mastering the Worker Pool Pattern in Go</title><link>https://corentings.dev/blog/go-pattern-worker/</link><guid isPermaLink="true">https://corentings.dev/blog/go-pattern-worker/</guid><description>Master the Worker Pool Pattern in Go to manage concurrent tasks efficiently. Control resource usage, improve throughput, and scale your applications.</description><pubDate>Tue, 10 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Worker Pool Pattern in Go&lt;/h1&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The Worker Pool pattern is a fundamental concurrency design in Go that efficiently manages a pool of worker goroutines to process tasks from a shared queue. This pattern excels at handling a large number of independent tasks concurrently while maintaining precise control over system resources and performance.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Processing a large number of independent tasks that can be parallelized&lt;/li&gt;
&lt;li&gt;Limiting the number of concurrent operations to prevent resource exhaustion&lt;/li&gt;
&lt;li&gt;Balancing workload across multiple processors or cores&lt;/li&gt;
&lt;li&gt;Managing CPU-bound or I/O-bound tasks efficiently&lt;/li&gt;
&lt;li&gt;Handling batch processing operations with controlled parallelism&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Controls resource utilization by maintaining a fixed number of workers&lt;/li&gt;
&lt;li&gt;Improves performance through efficient parallel processing&lt;/li&gt;
&lt;li&gt;Prevents system overload by limiting concurrent operations&lt;/li&gt;
&lt;li&gt;Enhances application scalability and throughput&lt;/li&gt;
&lt;li&gt;Maintains predictable resource usage patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;p&gt;The Worker Pool pattern consists of three essential components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A pool of worker goroutines that process tasks concurrently&lt;/li&gt;
&lt;li&gt;A job queue (input channel) that holds pending tasks&lt;/li&gt;
&lt;li&gt;A results queue (output channel) that collects processed results&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Workers continuously pull tasks from the job queue, process them independently, and send results to the results queue. This design ensures efficient task distribution and controlled concurrency.&lt;/p&gt;
&lt;h2&gt;Simple Example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func worker(id int, jobs &amp;lt;-chan int, results chan&amp;lt;- int) {
    for job := range jobs {
        fmt.Printf(&quot;Worker %d processing job %d\n&quot;, id, job)
        time.Sleep(time.Second) // Simulating work
        results &amp;lt;- job * 2
    }
}

func main() {
    const numJobs = 5
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // Start worker pool
    for w := 1; w &amp;lt;= numWorkers; w++ {
        go worker(w, jobs, results)
    }

    // Send jobs to the workers
    for j := 1; j &amp;lt;= numJobs; j++ {
        jobs &amp;lt;- j
    }
    close(jobs)

    // Collect and print results
    for a := 1; a &amp;lt;= numJobs; a++ {
        result := &amp;lt;-results
        fmt.Printf(&quot;Job result: %d\n&quot;, result)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Real-World Example: Image Processor&lt;/h2&gt;
&lt;p&gt;Let&apos;s consider a scenario where we need to process multiple images concurrently:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Job struct {
    ID       int
    ImageURL string
    Size     int
}

type Result struct {
    JobID     int
    ImageURL  string
    NewSize   int
    Error     error
    TimeSpent time.Duration
}

func imageProcessor(id int, jobs &amp;lt;-chan Job, results chan&amp;lt;- Result) {
    for job := range jobs {
        startTime := time.Now()

        fmt.Printf(&quot;Worker %d processing image %d from %s\n&quot;, id, job.ID, job.ImageURL)

        result := Result{
            JobID:    job.ID,
            ImageURL: job.ImageURL,
            NewSize:  job.Size,
        }

        // Simulate image processing with realistic steps
        err := processImage(job)
        if err != nil {
            result.Error = err
            results &amp;lt;- result
            continue
        }

        result.TimeSpent = time.Since(startTime)
        results &amp;lt;- result
    }
}

func processImage(job Job) error {
    // Simulate various image processing steps
    time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)

    // Simulate potential errors
    if rand.Float32() &amp;lt; 0.1 {
        return fmt.Errorf(&quot;failed to process image %d: simulation error&quot;, job.ID)
    }

    return nil
}

func main() {
    numCPU := runtime.NumCPU()
    runtime.GOMAXPROCS(numCPU)
    numWorkers := numCPU * 2 // Use 2 workers per CPU core
    const numJobs = 10

    jobs := make(chan Job, numJobs)
    results := make(chan Result, numJobs)

    // Initialize worker pool
    for w := 1; w &amp;lt;= numWorkers; w++ {
        go imageProcessor(w, jobs, results)
    }

    // Send image processing jobs
    for j := 1; j &amp;lt;= numJobs; j++ {
        jobs &amp;lt;- Job{
            ID:       j,
            ImageURL: fmt.Sprintf(&quot;https://example.com/image%d.jpg&quot;, j),
            Size:     100 * j, // Varying sizes
        }
    }
    close(jobs)

    // Collect and handle results
    for a := 1; a &amp;lt;= numJobs; a++ {
        result := &amp;lt;-results
        if result.Error != nil {
            fmt.Printf(&quot;Error processing image %d: %v\n&quot;, result.JobID, result.Error)
        } else {
            fmt.Printf(&quot;Successfully processed image %d to size %dpx in %v\n&quot;,
                result.JobID, result.NewSize, result.TimeSpent)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Always close the channel when generation is complete&lt;/li&gt;
&lt;li&gt;Use buffered channels when appropriate to prevent blocking&lt;/li&gt;
&lt;li&gt;Include monitoring and logging for production environments&lt;/li&gt;
&lt;li&gt;Implement graceful shutdown mechanisms&lt;/li&gt;
&lt;li&gt;Size your worker pool based on available system resources&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creating too many workers, leading to resource exhaustion&lt;/li&gt;
&lt;li&gt;Not handling worker failures or panics&lt;/li&gt;
&lt;li&gt;Forgetting to close channels properly&lt;/li&gt;
&lt;li&gt;Missing timeout mechanisms for long-running tasks&lt;/li&gt;
&lt;li&gt;Inefficient job distribution strategies&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Worker Pool pattern is a powerful tool in Go&apos;s concurrency toolkit, offering a balanced approach to parallel processing. By maintaining a fixed number of workers, it prevents resource exhaustion while maximizing throughput. The pattern is particularly valuable in real-world scenarios such as image processing, batch operations, and API request handling, where controlled concurrent processing is essential for optimal performance and resource utilization.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This article provides an introduction to the Worker Pool pattern in Go. While the pattern is powerful, it&apos;s important to consider the specific needs of your application when implementing it. For production use, additional error handling and optimizations may be necessary.&lt;/p&gt;
&lt;h2&gt;Testing Worker Pools&lt;/h2&gt;
&lt;p&gt;If you test a worker pool, focus on the contract: jobs go in, results come out. The number of goroutines is an implementation detail. I wrote about that in &lt;a href=&quot;/blog/tdd-permission-slip/&quot;&gt;TDD Isn&apos;t About Bugs — It&apos;s Your Permission to Refactor&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Series Navigation&lt;/h2&gt;
&lt;p&gt;This article is part of the &lt;strong&gt;Go Patterns&lt;/strong&gt; series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-generator/&quot;&gt;Mastering the Generator Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-pipeline/&quot;&gt;Go Pipeline Pattern: Turning Streams into Useful Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;a href=&quot;/tags/go-patterns/&quot;&gt;Go Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;For more advanced concurrency patterns and best practices in Go, stay tuned for future articles! 🚀&lt;/p&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Advanced Generator Pattern: Consuming and Testing Data Streams</title><link>https://corentings.dev/blog/advanced-real-world-generator/</link><guid isPermaLink="true">https://corentings.dev/blog/advanced-real-world-generator/</guid><description>Advanced Generator Pattern in Go: testing, error handling, and real-world data generation techniques for robust applications.</description><pubDate>Sun, 08 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Advanced Generator Pattern: Consuming and Testing Streams&lt;/h1&gt;
&lt;p&gt;:::warning[Difficulty Level]
Advanced
:::&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Expanding on our previous discussions of the Generator pattern, we&apos;ll explore two advanced applications: consuming large datasets lazily and simulating data streams for testing. These techniques are crucial for efficient data processing and robust application testing.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Processing large datasets that don&apos;t fit in memory&lt;/li&gt;
&lt;li&gt;Simulating data sources for testing&lt;/li&gt;
&lt;li&gt;Implementing ETL (Extract, Transform, Load) processes&lt;/li&gt;
&lt;li&gt;Creating reproducible test scenarios for data processing pipelines&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Memory Efficiency&lt;/strong&gt;: Process large datasets without loading everything into memory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testability&lt;/strong&gt;: Create controlled environments for testing data processing logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;: Easily switch between real and simulated data sources&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reproducibility&lt;/strong&gt;: Generate consistent test cases for data processing scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Create generator functions that yield data items one at a time&lt;/li&gt;
&lt;li&gt;Use channels to stream data from the source to the consumer&lt;/li&gt;
&lt;li&gt;Implement lazy loading for large datasets&lt;/li&gt;
&lt;li&gt;Create mock data generators for testing scenarios&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Example 1: Lazy Loading of Large Datasets&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type DataItem struct {
    ID   int
    Data string
}

// lazyDataLoader simulates loading a large dataset lazily
func lazyDataLoader(filePath string) &amp;lt;-chan DataItem {
    out := make(chan DataItem)
    go func() {
        defer close(out)
        // Simulate opening a large file
        fmt.Printf(&quot;Opening file: %s\n&quot;, filePath)

        // Simulate reading the file line by line
        for i := 0; i &amp;lt; 1000000; i++ {
            // Simulate processing delay for each item
            time.Sleep(1 * time.Millisecond)
            out &amp;lt;- DataItem{
                ID:   i + 1,
                Data: fmt.Sprintf(&quot;Data from line %d&quot;, i+1),
            }
            if i%100000 == 0 {
                fmt.Printf(&quot;Processed %d items\n&quot;, i)
            }
        }
    }()
    return out
}

func processData(data &amp;lt;-chan DataItem) {
    for item := range data {
        // Simulate data processing
        processedData := fmt.Sprintf(&quot;Processed: %s (ID: %d)&quot;, item.Data, item.ID)
        fmt.Println(processedData)
    }
}

func main() {
    dataStream := lazyDataLoader(&quot;large_dataset.txt&quot;)
    processData(dataStream)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example demonstrates lazy loading of a large dataset, processing items one at a time without loading the entire dataset into memory.&lt;/p&gt;
&lt;h2&gt;Example 2: Simulating Data Streams for Testing&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type DataItem struct {
    ID   int
    Data string
}

// mockDataStream simulates a data source (e.g., a file, queue, or network stream)
func mockDataStream(count int) &amp;lt;-chan DataItem {
    out := make(chan DataItem)
    go func() {
       defer close(out)
       for i := 0; i &amp;lt; count; i++ {
          // Simulate reading from a data source
          time.Sleep(100 * time.Millisecond)
          out &amp;lt;- DataItem{
             ID:   i + 1,
             Data: fmt.Sprintf(&quot;Data-%d&quot;, i+1),
          }
       }
    }()
    return out
}

// dataGenerator consumes the mock stream and yields processed data
func dataGenerator(stream &amp;lt;-chan DataItem) &amp;lt;-chan string {
    out := make(chan string)
    go func() {
       defer close(out)
       for item := range stream {
          // Process the data item
          processedData := fmt.Sprintf(&quot;Processed: %s (ID: %d)&quot;, item.Data, item.ID)
          out &amp;lt;- processedData
       }
    }()
    return out
}

type StreamGenerator struct{}

func (g StreamGenerator) Execute() {
    // Create a mock data stream
    dataStream := mockDataStream(10)

    // Create a generator to process the stream
    processedDataGen := dataGenerator(dataStream)

    // Consume and print the processed data
    for data := range processedDataGen {
       fmt.Println(data)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example demonstrates a more structured approach to using the Generator pattern for testing data processing pipelines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mockDataStream simulates a data source by generating items with controlled timing&lt;/li&gt;
&lt;li&gt;dataGenerator shows how to process a stream of data items and transform them&lt;/li&gt;
&lt;li&gt;The StreamGenerator type provides a clean interface for executing the pipeline and can be replaced with real data sources in production using DI (Dependency Injection)&lt;/li&gt;
&lt;li&gt;Each stage of the pipeline is clearly separated and testable&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use buffered channels for improved performance when processing large streams&lt;/li&gt;
&lt;li&gt;Implement timeout mechanisms for long-running operations&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;context&lt;/code&gt; package for cancellation in long-running generators&lt;/li&gt;
&lt;li&gt;Create configurable mock generators for diverse test scenarios&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Not handling errors or edge cases in data generation&lt;/li&gt;
&lt;li&gt;Overlooking resource cleanup in generators (e.g., closing file handles)&lt;/li&gt;
&lt;li&gt;Creating overly complex mock generators that don&apos;t reflect real-world scenarios&lt;/li&gt;
&lt;li&gt;Ignoring performance implications in lazy loading implementations&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Generator pattern proves invaluable for both consuming large datasets efficiently and creating robust test environments for data processing logic. By leveraging Go&apos;s concurrency features, we can create flexible, memory-efficient, and testable data processing pipelines that can handle real-world scenarios and simulated test cases alike.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;While these examples demonstrate the power of the Generator pattern for data processing and testing, real-world implementations may require additional error handling, resource management, and optimizations. Always consider the specific requirements and constraints of your application when applying these patterns.&lt;/p&gt;
&lt;p&gt;For more advanced concurrency patterns and best practices in Go, stay tuned for future articles! 🚀&lt;/p&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;font-size: 0.875rem; color: color-mix(in oklch, var(--text-base) 75%, transparent);&quot;&amp;gt;
&amp;lt;span property=&quot;dct:title&quot;&amp;gt;Educational Go Patterns &amp;lt;/span&amp;gt; by &amp;lt;a rel=&quot;cc:attributionURL dct:creator&quot; property=&quot;cc:attributionName&quot; href=&quot;https://corentings.dev&quot;&amp;gt;Corentin Giaufer Saubert&amp;lt;/a&amp;gt;
is licensed under &amp;lt;a href=&quot;https://creativecommons.org/licenses/by-nc-nd/4.0/?ref=chooser-v1&quot; target=&quot;_blank&quot; rel=&quot;license noopener noreferrer&quot;&amp;gt;Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International &amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
The code examples are licensed under the &amp;lt;a href=&quot;https://opensource.org/licenses/MIT&quot; target=&quot;_blank&quot; rel=&quot;license noopener noreferrer&quot;&amp;gt;MIT License&amp;lt;/a&amp;gt;.
The banner image has been created by (DALL·E) and is licensed under the same license as the article and other graphics.
&amp;lt;/div&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Advanced Generator Pattern in Go: Test Data Generation</title><link>https://corentings.dev/blog/real-world-generator/</link><guid isPermaLink="true">https://corentings.dev/blog/real-world-generator/</guid><description>Practical Generator Pattern examples in Go for test data generation, streaming, and building composable data pipelines.</description><pubDate>Fri, 06 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Advanced Generator Pattern: Test Data for Web Services&lt;/h1&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Building upon our previous exploration of the Generator pattern, let&apos;s dive into a more advanced real-world application: generating test data for a web service. This pattern is particularly useful for creating large datasets to stress test APIs or simulate high-load scenarios.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Stress testing web services&lt;/li&gt;
&lt;li&gt;Simulating high-load scenarios for databases&lt;/li&gt;
&lt;li&gt;Creating diverse datasets for QA environments&lt;/li&gt;
&lt;li&gt;Benchmarking system performance&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: Easily generate large volumes of test data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Customization&lt;/strong&gt;: Tailor data generation to specific test scenarios&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Realism&lt;/strong&gt;: Create data that closely mimics production patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;: Generate data on-the-fly, reducing storage needs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Define structures representing your API&apos;s data models&lt;/li&gt;
&lt;li&gt;Create generator functions for each data type&lt;/li&gt;
&lt;li&gt;Combine generators to create complex, interrelated data sets&lt;/li&gt;
&lt;li&gt;Use channels to stream generated data to consumers (e.g., API clients)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Advanced Example: E-commerce API Test Data Generator&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type Product struct {
    ID    int
    Name  string
    Price float64
}

type Order struct {
    ID       int
    UserID   int
    Products []Product
    Total    float64
}

func productGenerator(count int) &amp;lt;-chan Product {
    out := make(chan Product)
    go func() {
        defer close(out)
        for i := 0; i &amp;lt; count; i++ {
            out &amp;lt;- Product{
                ID:    i + 1,
                Name:  fmt.Sprintf(&quot;Product-%d&quot;, i+1),
                Price: 10.0 + float64(i),
            }
        }
    }()
    return out
}

func orderGenerator(userCount, orderPerUser int, products &amp;lt;-chan Product) &amp;lt;-chan Order {
    out := make(chan Order)
    go func() {
        defer close(out)
        var orderID int
        for userID := 1; userID &amp;lt;= userCount; userID++ {
            for i := 0; i &amp;lt; orderPerUser; i++ {
                orderID++
                var orderProducts []Product
                var total float64
                for j := 0; j &amp;lt; rand.Intn(5)+1; j++ {
                    product := &amp;lt;-products
                    orderProducts = append(orderProducts, product)
                    total += product.Price
                }
                out &amp;lt;- Order{
                    ID:       orderID,
                    UserID:   userID,
                    Products: orderProducts,
                    Total:    total,
                }
            }
        }
    }()
    return out
}

func main() {
    productChan := productGenerator(1000)
    orderChan := orderGenerator(100, 5, productChan)

    // Simulate sending orders to an API
    for order := range orderChan {
        // In a real scenario, you&apos;d send this to your API
        fmt.Printf(&quot;Sending order %d for user %d with total $%.2f\n&quot;, order.ID, order.UserID, order.Total)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example demonstrates a more complex use of the Generator pattern to create realistic test data for an e-commerce API. It generates products and orders, simulating a scenario where multiple users are placing orders with varying numbers of products.&lt;/p&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use buffered channels for improved performance when generating large datasets&lt;/li&gt;
&lt;li&gt;Implement cancellation mechanisms for long-running generators&lt;/li&gt;
&lt;li&gt;Consider using worker pools for parallel data generation in complex scenarios&lt;/li&gt;
&lt;li&gt;Seed random number generators for reproducible test data&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generating more data than necessary, leading to increased test times&lt;/li&gt;
&lt;li&gt;Not closing channels properly, causing goroutine leaks&lt;/li&gt;
&lt;li&gt;Overlooking edge cases in data generation, leading to incomplete test coverage&lt;/li&gt;
&lt;li&gt;Generating unrealistic data that doesn&apos;t reflect real-world scenarios&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The advanced application of the Generator pattern for test data generation showcases its power in creating scalable, customizable, and efficient solutions for testing web services. By leveraging Go&apos;s concurrency features, we can create sophisticated data generation pipelines that closely mimic real-world scenarios, enabling thorough and realistic testing of our systems.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This article expands on the Generator pattern with a focus on test data generation. While the example provided is more complex, it&apos;s still simplified for educational purposes. In real-world applications, additional considerations such as data variety, error handling, and integration with actual API endpoints would be necessary.&lt;/p&gt;
&lt;p&gt;For more advanced concurrency patterns and best practices in Go, stay tuned for future articles! 🚀&lt;/p&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Mastering the Generator Pattern in Go</title><link>https://corentings.dev/blog/go-pattern-generator/</link><guid isPermaLink="true">https://corentings.dev/blog/go-pattern-generator/</guid><description>Master the Generator Pattern in Go using goroutines and channels. Learn lazy evaluation, composability, and practical examples for data streams and iterators.</description><pubDate>Tue, 03 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Generator Pattern in Go&lt;/h1&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The Generator pattern in Go is a powerful concurrency pattern used to create functions that produce a sequence of values. It leverages Go&apos;s goroutines and channels to generate data asynchronously, providing an elegant way to work with streams of data or implement iterators.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Creating sequences of numbers or data&lt;/li&gt;
&lt;li&gt;Implementing iterators&lt;/li&gt;
&lt;li&gt;Processing streams of data&lt;/li&gt;
&lt;li&gt;Generating test data&lt;/li&gt;
&lt;li&gt;Simulating real-time data sources&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lazy Evaluation&lt;/strong&gt;: Values are generated on-demand, saving memory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encapsulation&lt;/strong&gt;: Hides the complexity of data generation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;: Allows for asynchronous data production&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;: Can generate infinite or finite sequences&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composability&lt;/strong&gt;: Generators can be chained or combined easily&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;A function creates and returns a channel&lt;/li&gt;
&lt;li&gt;The function starts a goroutine that sends values through the channel&lt;/li&gt;
&lt;li&gt;The caller receives values from the channel, typically using a for-range loop&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Simple Example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func evenGenerator(max int) &amp;lt;-chan int {
    out := make(chan int)
    go func() {
        for i := 0; i &amp;lt;= max; i += 2 {
            out &amp;lt;- i
        }
        close(out)
    }()
    return out
}

func main() {
    for num := range evenGenerator(10) {
        fmt.Println(num)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example creates a generator that produces even numbers up to a specified maximum. The &lt;code&gt;evenGenerator&lt;/code&gt; function returns a receive-only channel (&lt;code&gt;&amp;lt;-chan int&lt;/code&gt;). It starts a goroutine that sends even numbers through the channel and closes it when done.&lt;/p&gt;
&lt;h2&gt;Real-World Example: Log Line Generator&lt;/h2&gt;
&lt;p&gt;Let&apos;s consider a scenario where we need to generate sample log lines for testing a log analysis system.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type LogEntry struct {
    Timestamp time.Time
    Level     string
    Message   string
}

func logGenerator(count int) &amp;lt;-chan LogEntry {
    out := make(chan LogEntry)
    go func() {
        levels := []string{&quot;INFO&quot;, &quot;WARNING&quot;, &quot;ERROR&quot;}
        messages := []string{
            &quot;User logged in&quot;,
            &quot;Failed login attempt&quot;,
            &quot;Database connection lost&quot;,
            &quot;API request received&quot;,
            &quot;Cache miss&quot;,
        }
        for i := 0; i &amp;lt; count; i++ {
            out &amp;lt;- LogEntry{
                Timestamp: time.Now().Add(time.Duration(i) * time.Second),
                Level:     levels[rand.Intn(len(levels))],
                Message:   messages[rand.Intn(len(messages))],
            }
            time.Sleep(100 * time.Millisecond) // Simulate delay between log entries
        }
        close(out)
    }()
    return out
}

func main() {
    for entry := range logGenerator(5) {
        fmt.Printf(&quot;[%s] %s: %s\n&quot;, entry.Timestamp.Format(time.RFC3339), entry.Level, entry.Message)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This generator creates a stream of log entries, simulating real-world log generation. It&apos;s useful for testing log processing systems, allowing developers to generate a controlled stream of diverse log entries.&lt;/p&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Always close the channel when generation is complete&lt;/li&gt;
&lt;li&gt;Use receive-only channels (&lt;code&gt;&amp;lt;-chan&lt;/code&gt;) as return types&lt;/li&gt;
&lt;li&gt;Consider using context for cancellation in long-running generators&lt;/li&gt;
&lt;li&gt;Implement error handling for robust generators&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Forgetting to close channels, leading to goroutine leaks&lt;/li&gt;
&lt;li&gt;Creating infinite generators without proper termination conditions&lt;/li&gt;
&lt;li&gt;Blocking indefinitely on channel operations without timeout mechanisms&lt;/li&gt;
&lt;li&gt;Overusing generators for simple, finite sequences where slices might suffice&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Related Patterns&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Pipeline Pattern: Often used in conjunction with generators to process data streams&lt;/li&gt;
&lt;li&gt;Fan-Out/Fan-In Pattern: Can distribute generator output to multiple consumers&lt;/li&gt;
&lt;li&gt;Iterator Pattern: Generators can be seen as concurrent iterators&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Generator pattern in Go provides a powerful way to create sequences or streams of data asynchronously. By leveraging goroutines and channels, it offers lazy evaluation, encapsulation of complex logic, and seamless integration with Go&apos;s concurrency model. Whether you&apos;re working with infinite sequences, simulating data sources, or implementing iterators, the Generator pattern offers a flexible and efficient solution.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This article provides an introduction to the Generator pattern in Go. While the pattern is powerful, it&apos;s important to consider the specific needs of your application when implementing it. For production use, additional error handling and optimizations may be necessary.&lt;/p&gt;
&lt;h2&gt;Testing Generators&lt;/h2&gt;
&lt;p&gt;Generators are easy to test when you treat them as contracts: send input, receive output. The goroutine is an implementation detail. I wrote about why that matters in &lt;a href=&quot;/blog/tdd-permission-slip/&quot;&gt;TDD Isn&apos;t About Bugs — It&apos;s Your Permission to Refactor&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Series Navigation&lt;/h2&gt;
&lt;p&gt;This article is part of the &lt;strong&gt;Go Patterns&lt;/strong&gt; series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-producer-consumer/&quot;&gt;Understanding the Producer-Consumer Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-worker/&quot;&gt;Mastering the Worker Pool Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;a href=&quot;/tags/go-patterns/&quot;&gt;Go Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;For more advanced concurrency patterns and best practices in Go, stay tuned for future articles! 🚀&lt;/p&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Producer-Consumer in Go: Beyond the Basics</title><link>https://corentings.dev/blog/real-world-producer-consumer/</link><guid isPermaLink="true">https://corentings.dev/blog/real-world-producer-consumer/</guid><description>Explore advanced aspects of Go&apos;s Producer-Consumer pattern with buffered channels and real-world examples. A beginner-friendly deep dive into practical applications.</description><pubDate>Mon, 02 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Producer-Consumer in Go: Beyond the Basics&lt;/h1&gt;
&lt;p&gt;In our &lt;a href=&quot;/blog/go-pattern-producer-consumer&quot;&gt;previous article&lt;/a&gt;, we explored the fundamentals of the Producer-Consumer pattern in Go. Today, we&apos;ll take it a step further by examining more practical scenarios and introducing buffered channels - a powerful feature that can significantly improve your concurrent applications.&lt;/p&gt;
&lt;h2&gt;Buffered Channels: A Game Changer&lt;/h2&gt;
&lt;p&gt;While regular channels provide immediate synchronization between producers and consumers, sometimes we need more flexibility. Buffered channels act like a small warehouse, temporarily storing items when producers are faster than consumers or when we want to batch process items.&lt;/p&gt;
&lt;p&gt;Let&apos;s see how they work:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Unbuffered channel - synchronous
ch := make(chan int)

// Buffered channel - can hold up to 5 items
bufferedCh := make(chan int, 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key difference? With a buffered channel, producers can send up to 5 items without waiting for a consumer to receive them. This can significantly improve performance in certain scenarios.&lt;/p&gt;
&lt;h2&gt;Real-World Example: Log Processing System&lt;/h2&gt;
&lt;p&gt;Imagine building a log processing system for a busy web application. Logs come in rapidly during peak hours, but we want to process them in batches for efficiency. This is a perfect use case for buffered channels.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type LogEntry struct {
    Timestamp time.Time
    Level     string
    Message   string
}

func logGenerator(logs chan&amp;lt;- LogEntry) {
    // Simulate incoming logs
    for i := 0; i &amp;lt; 10 ; i++{
        log := LogEntry{
            Timestamp: time.Now(),
            Level:     []string{&quot;INFO&quot;, &quot;WARNING&quot;, &quot;ERROR&quot;}[rand.Intn(3)],
            Message:   fmt.Sprintf(&quot;Event #%d occurred&quot;, i),
        }
        logs &amp;lt;- log
        time.Sleep(100 * time.Millisecond) // Simulate varying log frequencies
    }
    close(logs)
}

func logProcessor(logs &amp;lt;-chan LogEntry) {
    batch := make([]LogEntry, 0, 3) // Process logs in batches of 3

    for log := range logs {
        batch = append(batch, log)

        if len(batch) == 3 {
            // Process batch
            processLogBatch(batch)
            batch = batch[:0] // Clear the batch
        }
    }

    // Process remaining logs
    if len(batch) &amp;gt; 0 {
        processLogBatch(batch)
    }
}

func processLogBatch(batch []LogEntry) {
    fmt.Println(&quot;Processing batch of logs:&quot;)
    for _, log := range batch {
        fmt.Printf(&quot;[%s] %s: %s\n&quot;,
            log.Timestamp.Format(&quot;15:04:05&quot;),
            log.Level,
            log.Message)
    }
    fmt.Println(&quot;Batch processing complete\n&quot;)
}

func main() {
    // Buffer size of 5 to handle burst of logs
    logs := make(chan LogEntry, 5)

    go logGenerator(logs)
    logProcessor(logs)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run this program, you&apos;ll see output similar to this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Processing batch of logs:
[14:23:45] INFO: Event #0 occurred
[14:23:45] WARNING: Event #1 occurred
[14:23:45] ERROR: Event #2 occurred
Batch processing complete

Processing batch of logs:
[14:23:46] INFO: Event #3 occurred
[14:23:46] WARNING: Event #4 occurred
[14:23:46] INFO: Event #5 occurred
Batch processing complete

Processing batch of logs:
[14:23:47] ERROR: Event #6 occurred
[14:23:47] INFO: Event #7 occurred
[14:23:47] WARNING: Event #8 occurred
Batch processing complete

Processing remaining logs:
[14:23:48] INFO: Event #9 occurred
Batch processing complete
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Understanding the Benefits&lt;/h2&gt;
&lt;p&gt;This implementation showcases several advanced concepts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Burst Handling&lt;/strong&gt;: The buffered channel (size 5) can handle bursts of logs without blocking the producer, even if the consumer is busy processing a batch.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Batch Processing&lt;/strong&gt;: Instead of processing each log immediately, we batch them for more efficient processing. This is common in real-world scenarios where batching can reduce database writes or API calls.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Graceful Shutdown&lt;/strong&gt;: The system handles remaining logs before shutting down, ensuring no data is lost.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Flexible Processing&lt;/strong&gt;: The batch size (3) is independent of the channel buffer size (5), allowing us to optimize both independently.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;When to Use Buffered Channels&lt;/h2&gt;
&lt;p&gt;Buffered channels are particularly useful when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your producers and consumers work at different speeds&lt;/li&gt;
&lt;li&gt;You want to batch process items for efficiency&lt;/li&gt;
&lt;li&gt;You need to handle burst of data without blocking producers&lt;/li&gt;
&lt;li&gt;You want to improve performance by reducing synchronization overhead&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, remember that buffered channels don&apos;t solve all problems. They can hide deadlocks and make it harder to reason about your program&apos;s behavior if used incorrectly.&lt;/p&gt;
&lt;h2&gt;Tips for Success&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Choose buffer sizes carefully—too small negates the benefits, too large can hide problems&lt;/li&gt;
&lt;li&gt;Monitor channel capacity in production using &lt;code&gt;len(ch)&lt;/code&gt; and &lt;code&gt;cap(ch)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Consider using contexts for cancellation and timeouts&lt;/li&gt;
&lt;li&gt;Always handle the remaining items when shutting down&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;What&apos;s Next?&lt;/h3&gt;
&lt;p&gt;Now that you understand buffered channels and batch processing, you&apos;re ready to explore more advanced patterns like Fan-Out/Fan-In or the Pipeline pattern. Stay tuned for our next article where we&apos;ll dive into those topics!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The Producer-Consumer pattern becomes even more powerful when combined with buffered channels and batch processing. While staying true to Go&apos;s simplicity, these features allow us to build more efficient and resilient systems.&lt;/p&gt;
&lt;p&gt;Remember: The key to mastering these patterns is practice. Try modifying the log processing example to handle different batch sizes or add error handling. The possibilities are endless!&lt;/p&gt;
</content:encoded></item><item><title>Understanding the Producer-Consumer Pattern in Go</title><link>https://corentings.dev/blog/go-pattern-producer-consumer/</link><guid isPermaLink="true">https://corentings.dev/blog/go-pattern-producer-consumer/</guid><description>Understanding the Producer-Consumer Pattern in Go with channels. Modular architecture, flexible scaling, and real-world concurrent data processing examples.</description><pubDate>Sat, 30 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Producer-Consumer Pattern in Go&lt;/h1&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The Producer-Consumer pattern is a fundamental concurrency pattern in Go that elegantly separates the production of data from its consumption. By using channels as intermediaries, this pattern creates a robust foundation for concurrent data processing.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;When you need to separate data generation from its processing&lt;/li&gt;
&lt;li&gt;In scenarios where production and consumption rates differ&lt;/li&gt;
&lt;li&gt;When scaling producers and consumers independently is desired&lt;/li&gt;
&lt;li&gt;For managing workload distribution in concurrent systems&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why Use It&lt;/h2&gt;
&lt;p&gt;This pattern offers several compelling advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modularity&lt;/strong&gt;: Clear separation between data production and consumption logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible Scaling&lt;/strong&gt;: Easy to add more producers or consumers as needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buffer Management&lt;/strong&gt;: Handles different processing rates naturally&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource Control&lt;/strong&gt;: Better management of system resources through controlled data flow&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;p&gt;The pattern consists of three main components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Producers&lt;/strong&gt;: Goroutines that generate data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queue&lt;/strong&gt;: A channel that acts as a buffer between producers and consumers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumers&lt;/strong&gt;: Goroutines that process the data&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Simple Example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func producer(ch chan&amp;lt;- int) {
    for i := 0; i &amp;lt; 5; i++ {
        ch &amp;lt;- i
        fmt.Printf(&quot;Produced: %d\n&quot;, i)
    }
    close(ch)
}

func consumer(ch &amp;lt;-chan int) {
    for num := range ch {
        fmt.Printf(&quot;Consumed: %d\n&quot;, num)
    }
}

func main() {
    ch := make(chan int)
    go producer(ch)
    consumer(ch)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Real-World Example: Web Scraper&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type Page struct {
    URL     string
    Content string
}

func scraper(urls []string, pages chan&amp;lt;- Page) {
    for _, url := range urls {
        // Simulate web scraping
        time.Sleep(100 * time.Millisecond)
        pages &amp;lt;- Page{
            URL:     url,
            Content: fmt.Sprintf(&quot;Content from %s&quot;, url),
        }
    }
    close(pages)
}

func processor(pages &amp;lt;-chan Page, results chan&amp;lt;- string) {
    for page := range pages {
        // Simulate content processing
        time.Sleep(200 * time.Millisecond)
        results &amp;lt;- fmt.Sprintf(&quot;Processed %s: %s&quot;, page.URL, page.Content)
    }
    close(results)
}

func main() {
    urls := []string{&quot;https://example1.com&quot;, &quot;https://example2.com&quot;}
    pages := make(chan Page)
    results := make(chan string)

    go scraper(urls, pages)
    go processor(pages, results)

    for result := range results {
        fmt.Println(result)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Always use directional channel types (&lt;code&gt;chan&amp;lt;-&lt;/code&gt; for send-only, &lt;code&gt;&amp;lt;-chan&lt;/code&gt; for receive-only)&lt;/li&gt;
&lt;li&gt;Consider buffered channels when producers and consumers work at different speeds&lt;/li&gt;
&lt;li&gt;Implement proper error handling and propagation&lt;/li&gt;
&lt;li&gt;Use context for cancellation when needed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Common Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Forgetting to close channels&lt;/li&gt;
&lt;li&gt;Not handling backpressure when producers are faster than consumers&lt;/li&gt;
&lt;li&gt;Creating deadlocks by improper channel management&lt;/li&gt;
&lt;li&gt;Memory leaks from unclosed goroutines&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Related Patterns&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Pipeline Pattern: For multi-stage data processing&lt;/li&gt;
&lt;li&gt;Worker Pool Pattern: For parallel task processing&lt;/li&gt;
&lt;li&gt;Fan-Out/Fan-In Pattern: For distributed workload processing&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Producer-Consumer pattern is a cornerstone of concurrent programming in Go. Its simplicity and effectiveness make it an excellent starting point for learning concurrency patterns. By separating concerns and managing data flow through channels, it provides a clean and efficient way to handle concurrent data processing tasks.&lt;/p&gt;
&lt;h2&gt;Series Navigation&lt;/h2&gt;
&lt;p&gt;This article is part of the &lt;strong&gt;Go Patterns&lt;/strong&gt; series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-generator/&quot;&gt;Mastering the Generator Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;a href=&quot;/tags/go-patterns/&quot;&gt;Go Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This article is a simple introduction to the Producer-Consumer pattern in Go. For more advanced use cases and optimizations, consider exploring additional resources and best practices in concurrent programming.
I may write more articles on this topic in the future, so stay tuned! 🚀 &amp;lt;br/&amp;gt;
If you want to have a look at the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>My Experience at the Karen Asrian Memorial Tournament</title><link>https://corentings.dev/blog/karen-asrian-memorial-tournament/</link><guid isPermaLink="true">https://corentings.dev/blog/karen-asrian-memorial-tournament/</guid><description>My experience at the Karen Asrian Memorial chess tournament in Armenia—games, culture, and lessons learned.</description><pubDate>Fri, 17 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;My recent trip to Armenia wasn&apos;t just a vacation—it was a chess pilgrimage!
I participated in the prestigious Karen Asrian Memorial, a chess tournament that pushed me to my limits and left me with unforgettable memories.&lt;/p&gt;
&lt;h2&gt;The Tournament&lt;/h2&gt;
&lt;p&gt;The Karen Asrian Memorial is a tournament dedicated to a chess legend.
GM Avetik Grigoryan wrote a fantastic article about him, which I highly advise you to read:
&amp;lt;a rel=&apos;noopener noreferrer&apos; aria-labal=&quot;In Memory of GM Karen Asrian&quot; href=&quot;https://chessmood.com/blog/karen-asrian&quot;&amp;gt;In Memory of GM Karen Asrian&amp;lt;/a&amp;gt;.
One of the tournament&apos;s highlights was meeting Levon Aronian, a true chess superstar. I was fortunate to meet such a player in person.&lt;/p&gt;
&lt;p&gt;The tournament format consisted of nine rounds spread over nine days. Each game was a marathon, lasting between 3 and 4.5 hours.&lt;/p&gt;
&lt;h2&gt;My performance&lt;/h2&gt;
&lt;p&gt;Overall, this was my most robust performance yet.
I beat a player rated over 2150 and even faced a Grandmaster in a classical chess game for the first time.
While I achieved a dominant position, time pressure ultimately led to a bittersweet loss.&lt;/p&gt;
&lt;p&gt;At the beginning of the tournament, I was rated 1803 Elo and was the 67th player over 73 according to the initial ranking.
I finished with 3.5 points over 9, half a point more than my initial goal. I earned 34 fide elo points and a performance of 2000 elo!
My final rank is 57th, meaning I had an excellent performance.&lt;/p&gt;
&lt;h2&gt;Chessmood&lt;/h2&gt;
&lt;p&gt;Speaking of improvement, a big part of my chess journey wouldn&apos;t have been possible without the Chessmood team. Their training platform helped me climb over 300 points in online blitz and rapid chess ratings.
This translated into real-world results, allowing me to consistently defeat players rated around 2000 Elo.
I also had the opportunity to meet them in person. I played in the tournament with IM David Shahinyan, who helped me improve recently.
Furthermore, I could visit Chessmood&apos;s office, discover their waiting room, and have a great barbecue with them!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This Armenian adventure tested my skills and reminded me of the power of dedicated practice.
It left me eager to return and continue my chess journey!&lt;/p&gt;
&lt;p&gt;If you want to know more about what I discovered in Armenia besides chess, stay tuned for my next article! I would like to share some insights about the country and its culture.&lt;/p&gt;
</content:encoded></item><item><title>Solving the Sum of Squares Problem: Optimizing Performance</title><link>https://corentings.dev/blog/optimizing-goroutines-sum-of-squares/</link><guid isPermaLink="true">https://corentings.dev/blog/optimizing-goroutines-sum-of-squares/</guid><description>Optimize Go performance by solving the sum of squares problem. Benchmark goroutines vs sequential code and avoid common concurrency pitfalls.</description><pubDate>Wed, 09 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Disclaimer: Enhancing Algorithm Discussions&lt;/h1&gt;
&lt;p&gt;Before delving into the main topic, I want to express my respect for the original post&apos;s intent.
My response aims to provide slight corrections that can benefit readers seeking accurate information.
In the spirit of shared learning, I intend to contribute constructively rather than criticize. &amp;lt;br /&amp;gt;Clarifications and alternative viewpoints can foster a deeper understanding of complex concepts. Let&apos;s continue engaging in open discussions, embracing diverse insights as we collectively refine our knowledge.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Greetings, fellow Gophers! Are you ready to explore the intriguing world of Goroutines and unravel the mysteries of the sum of squares problem?
Recently, a friend learning Go shared a &amp;lt;a href={&apos;https://medium.com/@ShivamSouravJha/golang-only-things-i-know-for-the-interview-4322d29d67a3&apos;} style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;medium article&amp;lt;/a&amp;gt; with me.
It discussed Goroutines and how they can be used to solve a simple problem: calculating the sum of square numbers in a given array.&lt;/p&gt;
&lt;p&gt;In this blog post, we&apos;ll delve into how Goroutines, channels, and data structures in Golang can be used to tackle the sum of squares problem. We&apos;ll also discuss common pitfalls and inefficient practices that can hinder performance.
Fear not, my friends, for we shall also unveil secrets to optimizing your code and achieving exceptional performance. So, let&apos;s embark on this quest together!&lt;/p&gt;
&lt;h2&gt;The Sum of Squares Problem: A simple problem&lt;/h2&gt;
&lt;p&gt;The medium article of my friend addressed the following problem: &quot;6. Write a program that returns the sum of the squares of each element of an array in Golang.&quot;&lt;/p&gt;
&lt;p&gt;Easy enough. We need to iterate through every element in the array and add the square of each one.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func simpleSumSquare(items []int) int {
	total := 0 // total sum
	for i := 0; i &amp;lt; len(items); i++ {
		total += items[i] * items[i] // square the item and add it to the total
	}
	return total // return the total sum
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Can we simplify it further? The code is reasonably straightforward. However, can we make it faster?&lt;/p&gt;
&lt;h2&gt;Goroutines: Harnessing the Power of Concurrency&lt;/h2&gt;
&lt;p&gt;Goroutines, the superheroes of Golang, are lightweight threads that enable concurrent task execution.
By using Goroutines, we can execute multiple functions simultaneously, making our code more efficient and faster. But with great power comes great responsibility.&amp;lt;br /&amp;gt;
One common mistake in solving the sum of squares problem is the improper use of Goroutines.
Some developers create too many Goroutines without proper synchronization, leading to chaos and incorrect results.
Remember, coordination is crucial!&lt;/p&gt;
&lt;h2&gt;Goroutines: Common pitfall&lt;/h2&gt;
&lt;p&gt;When solving the sum of squares problem, the choice of algorithm and data structures significantly impacts performance.
The aforementioned medium blog post highlights the use of Goroutines and their potential to substantially improve performance.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func sumSquare(items []int) int {
	number := make(chan int)   // channel for sending numbers
	response := make(chan int) // channel for receiving responses

	var wg sync.WaitGroup // wait group for waiting for all goroutines to finish

	total := 0 // total sum

	// Create a goroutine for each item in the slice
	for _, item := range items {
		wg.Add(1)           // increment the wait group counter
		go func(item int) { // create a goroutine
			defer wg.Done()    // decrement the wait group counter when the goroutine finishes
			sum1 := &amp;lt;-number   // receive a number from the number channel
			sum1 = sum1 * sum1 // square the number
			response &amp;lt;- sum1   // send the result to the response channel
		}(item) // pass the item to the goroutine
		number &amp;lt;- item      // send the item to the number channel
		total += &amp;lt;-response // receive the result from the response channel
	}

	defer close(number)   // close the number channel
	defer close(response) // close the response channel

	wg.Wait() // wait for all goroutines to finish

	return total // return the total sum
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code creates a new goroutine for each item in the array and then performs the squaring operation. It uses channels to pass values between the goroutine and the main thread.&lt;/p&gt;
&lt;p&gt;However, is this code correct? As discussed in &amp;lt;a href={&apos;../mergesort-parallel&apos;} style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;prefetch noopener&quot;&amp;gt;my previous blog post about the mergesort algorithm&amp;lt;/a&amp;gt;, there are better practices than spawning numerous goroutines.
More than being lightweight is needed to justify using many goroutines; it still uses memory and time, and we must be cautious.&lt;/p&gt;
&lt;p&gt;As always, let&apos;s benchmark the code!&lt;/p&gt;
&lt;h2&gt;Benchmarking: The Quest for Ultimate Performance&lt;/h2&gt;
&lt;p&gt;Benchmarking lets us measure code performance and compare implementations to identify the best.&lt;/p&gt;
&lt;p&gt;With Golang&apos;s built-in benchmarking tools, we can easily measure the execution time of our code and identify bottlenecks. By tweaking our implementation and experimenting with different approaches, we can achieve optimal performance and revel in our achievements.&lt;/p&gt;
&lt;p&gt;Hence, I&apos;ve created a small function to benchmark our algorithms using randomly generated arrays of varying sizes to observe their performance at different scales:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func benchmarkFramework(b *testing.B, sumFunction func([]int) int) {
	sizes := [][]int{RandomArray(100, 0, 100),
		RandomArray(1000, 0, 1000),
		RandomArray(10000, 0, 10000),
		RandomArray(100000, 0, 100000),
		RandomArray(1000000, 0, 1000000),
	}
	b.ResetTimer()
	for _, size := range sizes {
		b.Run(fmt.Sprintf(&quot;%d&quot;, len(size)), func(b *testing.B) {
			for i := 0; i &amp;lt; b.N; i++ {
				sumFunction(size)
			}
		})
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here are the results:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;time/op&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/100&lt;/td&gt;
&lt;td&gt;49.5ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/1000&lt;/td&gt;
&lt;td&gt;450ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/10000&lt;/td&gt;
&lt;td&gt;4.41µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/100000&lt;/td&gt;
&lt;td&gt;43.8µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/1000000&lt;/td&gt;
&lt;td&gt;437µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/100&lt;/td&gt;
&lt;td&gt;75.2µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/1000&lt;/td&gt;
&lt;td&gt;730µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/10000&lt;/td&gt;
&lt;td&gt;7.24ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/100000&lt;/td&gt;
&lt;td&gt;72.4ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/1000000&lt;/td&gt;
&lt;td&gt;739ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As we can see, the simple algorithm outperforms the complex one! Why? Because spawning too many Goroutines slows down the program and consumes a significant amount of memory.&lt;/p&gt;
&lt;h2&gt;Data Structures &amp;amp; Algorithms: Choose Wisely&lt;/h2&gt;
&lt;p&gt;When solving the sum of squares problem, the choice of data structures and algorithms significantly impacts performance.&lt;/p&gt;
&lt;p&gt;I decided to develop a more efficient function. Consequently, I used chunks and a Goroutine pool.&lt;/p&gt;
&lt;p&gt;A goroutines pool manages a set number of reusable goroutines, reducing overhead in concurrent programs. Chunks break data into segments for parallel processing, optimizing resource use, and enhancing efficiency. Combining these techniques streamlines parallelism, maximizing concurrency benefits.&lt;/p&gt;
&lt;p&gt;Here is my code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func simpleParallelSumSquare(items []int) int {
	const chunkSize = 10000

	if len(items) &amp;lt;= 10000 { // Threshold for small slices
		return simpleSumSquare(items) // Use the simpleSumSquare function
	}

	// Divide the items into chunks
	chunks := make([][]int, 0)
	for i := 0; i &amp;lt; len(items); i += chunkSize {
		end := i + chunkSize // end index for the chunk
		if end &amp;gt; len(items) {
			end = len(items) // last chunk may be smaller than chunkSize
		}
		chunks = append(chunks, items[i:end]) // append the chunk to the chunks slice
	}

	// Create a goroutine for each chunk
	wg := sync.WaitGroup{}
	resultChan := make(chan int, len(chunks)) // channel for receiving results

	for _, chunk := range chunks { // iterate over the chunks
		wg.Add(1)              // increment the wait group counter
		go func(chunk []int) { // create a goroutine
			resultChan &amp;lt;- simpleSumSquare(chunk) // send the result to the result channel
			wg.Done()                            // decrement the wait group counter when the goroutine finishes
		}(chunk) // pass the chunk to the goroutine
	}

	wg.Wait()         // Wait for all goroutines to finish
	close(resultChan) // close the result channel

	// Sum the results
	total := 0
	for partialSum := range resultChan {
		total += partialSum
	}

	return total // return the total sum
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Can this code be improved further? I could reduce the number of allocations and enhance performance. However, I opted for simplicity to better illustrate pitfalls and good practices related to Goroutines.&lt;/p&gt;
&lt;p&gt;Is it genuinely efficient? Let&apos;s benchmark it, as always!&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;time/op&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/100&lt;/td&gt;
&lt;td&gt;49.5ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/1000&lt;/td&gt;
&lt;td&gt;450ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/10000&lt;/td&gt;
&lt;td&gt;4.41µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/100000&lt;/td&gt;
&lt;td&gt;43.8µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/1000000&lt;/td&gt;
&lt;td&gt;437µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/100&lt;/td&gt;
&lt;td&gt;75.2µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/1000&lt;/td&gt;
&lt;td&gt;730µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/10000&lt;/td&gt;
&lt;td&gt;7.24ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/100000&lt;/td&gt;
&lt;td&gt;72.4ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/1000000&lt;/td&gt;
&lt;td&gt;739ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleParallelSumSquare/100&lt;/td&gt;
&lt;td&gt;49.5ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleParallelSumSquare/1000&lt;/td&gt;
&lt;td&gt;450ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleParallelSumSquare/10000&lt;/td&gt;
&lt;td&gt;4.41µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleParallelSumSquare/100000&lt;/td&gt;
&lt;td&gt;18.0µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleParallelSumSquare/1000000&lt;/td&gt;
&lt;td&gt;67.2µs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As we can observe, our new code is significantly faster!&lt;/p&gt;
&lt;h2&gt;Pitfalls and Bad Practices: The Road to Destruction&lt;/h2&gt;
&lt;p&gt;Ah, the treacherous road of pitfalls and bad practices. One common mistake is the excessive creation and destruction of Goroutines.
Creating Goroutines carries a cost; making too many can slow your program and consume unnecessary resources.&lt;/p&gt;
&lt;p&gt;Furthermore, we can use unsafe features to improve our code&apos;s performance but must not sacrifice maintainability and safety for speed if it&apos;s not really required.
If you want to learn more about unsafe features available on Golang, you can look at the code published &amp;lt;a href=&apos;https://github.com/CorentinGS/go-teaching/tree/main/goroutines_sum_square&apos; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&amp;gt; here on my Github &amp;lt;/a&amp;gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion: The Sum of Success&lt;/h2&gt;
&lt;p&gt;Congratulations, my friends! We&apos;ve journeyed through Goroutines, channels, and data structures, conquering the sum of squares problem.
We&apos;ve learned from our mistakes, optimized our code, and achieved top-notch performance.&lt;/p&gt;
&lt;p&gt;But remember, the pursuit of knowledge is everlasting.
Keep exploring, experimenting, and pushing the boundaries of what&apos;s possible. And always remember, with great power comes great responsibility.
So go forth, my fellow Gophers, may your code be swift, your bugs be few, and your adventures be legendary!&lt;/p&gt;
&lt;h2&gt;Related Articles&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/simple-go-vs-goroutines/&quot;&gt;Goroutine vs Simple function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/go-pattern-worker/&quot;&gt;Mastering the Worker Pool Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://medium.com/@ShivamSouravJha/golang-only-things-i-know-for-the-interview-4322d29d67a3&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&amp;gt;Medium article&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/CorentinGS/go-teaching/tree/main/goroutines_sum_square&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;Code snippets&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Upgrading to dnf5: A step-by-step guide for Fedora users</title><link>https://corentings.dev/blog/dnf5-step-by-step/</link><guid isPermaLink="true">https://corentings.dev/blog/dnf5-step-by-step/</guid><description>Step-by-step guide to upgrading from DNF to DNF5 on Fedora. Faster package management with C++ multi-threaded performance.</description><pubDate>Fri, 28 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Upgrading to dnf5: A guide for Fedora users&lt;/h1&gt;
&lt;p&gt;:::warning
This guide is old and may contain outdated information.
:::&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;DNF5 is Fedora&apos;s new, faster, and more powerful package manager. Although it is still in development and won&apos;t be the default package manager until Fedora 39, you can install it now and start using it.
This blog post will show you how to replace DNF with DNF5 on your Fedora system.&lt;/p&gt;
&lt;h2&gt;What&apos;s the difference between dnf5 and dnf ?&lt;/h2&gt;
&lt;p&gt;DNF is an old, single-threaded package manager with much legacy code. It&apos;s written in Python and is usually described as slow by users.
DNF5, on the other hand, is a complete rewrite of DNF written in C++. It&apos;s multi-threaded, has a better user experience, should be easier to maintain, and is faster.&lt;/p&gt;
&lt;h2&gt;Why should I upgrade to dnf5?&lt;/h2&gt;
&lt;p&gt;Upgrading to DNF5 offers several benefits, including improved speed and efficiency.
DNF5 is designed to be faster and more efficient than DNF, which can help speed up your system&apos;s package installation and update process.
Additionally, as it will be the default package manager in &lt;a href=&quot;https://github.com/rpm-software-management/dnf5/issues/411&quot;&gt;Fedora39&lt;/a&gt;,
starting to use it now and reporting any bugs you encounter will help the developers fix them before the release.&lt;/p&gt;
&lt;h2&gt;How to upgrade to dnf5?&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;Step 1: Install dnf5&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;To install DNF5 from the unstable repository, run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dnf copr enable rpmsoftwaremanagement/dnf5-unstable ;
dnf install dnf5 dnf5-plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re using sudo, use this command instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dnf copr enable rpmsoftwaremanagement/dnf5-unstable ;
sudo dnf install dnf5 dnf5-plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;Step 2: Create an alias for dnf5 (optional)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;You can create an alias if you want to use DNF5 instead of DNF. Run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alias dnf=&quot;dnf5&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make this alias permanent, add it to your &lt;code&gt;~/.bashrc&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;alias dnf=\&quot;dnf5\&quot;&quot; &amp;gt;&amp;gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or add it to your &lt;code&gt;~/.zshrc&lt;/code&gt; file if you use zsh:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;alias dnf=\&quot;dnf5\&quot;&quot; &amp;gt;&amp;gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;Remember that DNF5 is still in development and not ready for production use. It may contain bugs and should not be used on production systems. Use it at your own risk. I am not responsible for any damage caused by using DNF5.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;DNF5 may still have some bugs, so it&apos;s essential to experiment with it and keep this blog post up-to-date with the latest changes.
This guide helped you upgrade to DNF5. If you have any questions or suggestions, feel free to contact me on &amp;lt;a href=&quot;https://twitter.com/GSCorentinDev&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;Twitter&amp;lt;/a&amp;gt; or &amp;lt;a href=&quot;https://corentings.dev/discord&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;Discord&amp;lt;/a&amp;gt;.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://fedoraproject.org/wiki/Changes/ReplaceDnfWithDnf5&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;Fedora Project&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.reddit.com/r/Fedora/comments/12jv7uc/what_is_the_state_of_affairs_with_fedora_38_and/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;r/fedora&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/rpm-software-management/dnf5/issues/411&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;Github issue&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Merge Sort using Goroutines</title><link>https://corentings.dev/blog/mergesort-parallel/</link><guid isPermaLink="true">https://corentings.dev/blog/mergesort-parallel/</guid><description>Implement parallel Merge Sort in Go using goroutines. Compare performance with sequential version and learn when parallelization pays off.</description><pubDate>Wed, 11 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Parallel Merge Sort vs Simple Merge Sort&lt;/h1&gt;
&lt;p&gt;This is a simple example of how to use goroutines to &lt;strong&gt;parallelize&lt;/strong&gt; a merge sort algorithm.
We compare the performance of a simple merge sort algorithm with a parallel merge sort algorithm that uses goroutines.&lt;/p&gt;
&lt;h2&gt;The Merge Sort Algorithm&lt;/h2&gt;
&lt;p&gt;The merge sort algorithm is a divide and conquer algorithm that recursively splits the input array into two halves,
sorts each half, and then merges the two sorted halves into a single sorted array.&lt;/p&gt;
&lt;p&gt;To speed up the merge sort algorithm, we use insertion sort for small subarrays (less than 12 elements).&lt;/p&gt;
&lt;p&gt;The implementation of the algorithm uses generics to allow sorting of any type of numbers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func MergeSort[T Number](items []T) []T {
	size := len(items)
	if size &amp;lt; 2 {
		return items
	}

	if size &amp;lt; K {
		return Insertionsort(items)
	}

	middle := size / 2
	var a = MergeSort(items[:middle])
	var b = MergeSort(items[middle:])

	return merge(a, b)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Merge Sort Algorithm with Goroutines&lt;/h2&gt;
&lt;p&gt;The parallel merge sort algorithm uses &lt;strong&gt;goroutines&lt;/strong&gt; to sort the two halves of the input array in parallel.&lt;/p&gt;
&lt;p&gt;To prevent the creation of too many goroutines, we use a &lt;strong&gt;threshold&lt;/strong&gt; to determine when to use goroutines.
If the size of the input array is less than the threshold,
we use a simple merge sort algorithm instead of a parallel one.&lt;/p&gt;
&lt;p&gt;Here we use a threshold of 512 elements. We can benchmark the performance of the algorithm with
different thresholds to find the optimal threshold, but we will not do that in this example.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ParallelMerge Perform merge sort on a slice using goroutines
func ParallelMerge[T Number](items []T) []T {
	if len(items) &amp;lt; 2 {
		return items
	}

	// Use a simple merge sort algorithm if the size of the input array is less than the threshold
	if len(items) &amp;lt; 512 {
		return MergeSort(items)
	}

	// Create the wait group to wait for the goroutines to finish
	var wg sync.WaitGroup
	wg.Add(1)

	var middle = len(items) / 2  // Find the middle index of the input array
	var a []T                   // Create a slice to hold the first half of the input array
	go func() {                // Create a goroutine to sort the first half of the input array
		defer wg.Done()       // Decrement the wait group counter when the goroutine finishes
		a = ParallelMerge(items[:middle]) // Sort the first half of the input array
	}()
	var b = ParallelMerge(items[middle:]) // Sort the second half of the input array

	wg.Wait() // Wait for the goroutine to finish
	return merge(a, b) // Merge the two sorted halves
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Benchmarking the Merge Sort Algorithms&lt;/h2&gt;
&lt;p&gt;Now we can benchmark our merge sort algorithms to compare their &lt;strong&gt;performance&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;To benchmark the algorithms, we use arrays of different sizes and &lt;strong&gt;measure the time&lt;/strong&gt; it takes to sort the arrays.&lt;/p&gt;
&lt;p&gt;To run the benchmark we use the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go test -bench=. -benchtime 5s &amp;gt; benchmark.txt &amp;amp;&amp;amp; benchstat benchmark.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Benchstat is a tool that can be used to compare the results of benchmarks.&lt;/p&gt;
&lt;p&gt;The results of the benchmark are as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name                                   time/op
Mergesort/1000                     14.6µs ± 0%
Mergesort/10000                     473µs ± 0%
Mergesort/100000                   6.33ms ± 0%
Mergesort/1000000                  87.4ms ± 0%

MergesortWithGoroutines/1000       18.7µs ± 0%
MergesortWithGoroutines/10000       217µs ± 0%
MergesortWithGoroutines/100000     2.71ms ± 0%
MergesortWithGoroutines/1000000    29.0ms ± 0%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As we can see, the parallel merge sort algorithm is &lt;strong&gt;much faster&lt;/strong&gt; than the simple merge sort algorithm for &lt;strong&gt;large arrays&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Why is the Parallel Merge Sort Algorithm Faster?&lt;/h2&gt;
&lt;p&gt;The parallel merge sort algorithm is faster because it uses goroutines to sort the two halves of the input array in &lt;strong&gt;parallel&lt;/strong&gt;.
The simple merge sort algorithm sorts the two halves of the input array &lt;strong&gt;sequentially&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;As discussed in the previous article, the cost of &lt;strong&gt;creating a goroutine can be high&lt;/strong&gt;.
So we should use a parallel merge sort algorithm only when the size of the input array is &lt;strong&gt;large enough to justify the cost&lt;/strong&gt; of creating goroutines.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this example, we have seen how to use &lt;strong&gt;goroutines to parallelize&lt;/strong&gt; a merge sort algorithm. We have also benchmarked the performance of the merge sort algorithms to compare their performance.
Goroutines are a &lt;strong&gt;powerful tool&lt;/strong&gt; that should be used &lt;strong&gt;only when the cost of creating goroutines is justified&lt;/strong&gt; by the performance improvement.&lt;/p&gt;
&lt;p&gt;In this example, the code isn&apos;t more complex when using goroutines therefore it&apos;s worth using them.&lt;/p&gt;
&lt;h2&gt;Related Articles&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/go-pattern-worker/&quot;&gt;Mastering the Worker Pool Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;p&gt;The code for this article can be found on &amp;lt;a href=&quot;https://github.com/CorentinGS/go-teaching/tree/main/goroutines_merge_sort&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt; GitHub &amp;lt;/a&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Goroutine vs Simple function</title><link>https://corentings.dev/blog/simple-go-vs-goroutines/</link><guid isPermaLink="true">https://corentings.dev/blog/simple-go-vs-goroutines/</guid><description>When are goroutines overkill in Go? Benchmark comparison showing simple functions can outperform goroutines for small datasets.</description><pubDate>Wed, 11 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Goroutine vs Simple function&lt;/h1&gt;
&lt;p&gt;This is a simple example of why goroutines might be &lt;strong&gt;overkill&lt;/strong&gt; for some tasks and less efficient than a simple function.&lt;/p&gt;
&lt;h2&gt;Structures&lt;/h2&gt;
&lt;p&gt;We got a simple structure that contains sensitive information that we don&apos;t want to be exposed to the outside world.
Therefore, we created a second structure that hides the sensitive information and only exposes the information we want to be public.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Pineapple is a struct that represents a database object with sensitive data that should be hidden
type Pineapple struct {
	Paro       string `faker:&quot;name&quot;`
	Turkey     string `faker:&quot;name&quot;`
	Banana     string `faker:&quot;name&quot;`
	Age        int    `faker:&quot;number&quot;`
	Size       int    `faker:&quot;number&quot;`
	IsAlive    bool
	ID         uint
	SecretCode []byte
	Created    time.Time
	Updated    time.Time
}

// SafePineApple is a struct that represents a Pineapple object without sensitive data
type SafePineApple struct {
    Paro    string
    Turkey  string
    Banana  string
    IsAlive bool
    Age     int
    ID      uint
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conversion&lt;/h2&gt;
&lt;p&gt;We need to convert our Pineapple object to a SafePineApple object.
We can do this by creating a method on the Pineapple struct that returns a SafePineApple object.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ToSafePineApple converts a Pineapple object to a SafePineApple object
func (p *Pineapple) ToSafePineApple() SafePineApple {
	return SafePineApple{
		Paro:    p.Paro,
		Turkey:  p.Turkey,
		Banana:  p.Banana,
		IsAlive: p.IsAlive,
		Age:     p.Age,
		ID:      p.ID,
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Use case&lt;/h2&gt;
&lt;p&gt;In our use case we have an &lt;strong&gt;array of Pineapple objects&lt;/strong&gt; coming from our database that we want to convert to &lt;strong&gt;SafePineApple objects&lt;/strong&gt; and store them in a &lt;strong&gt;new array&lt;/strong&gt;.
The &lt;strong&gt;order of the objects&lt;/strong&gt; in the array should be the same as the original array as it has already been sorted by a sql query.&lt;/p&gt;
&lt;h2&gt;Simple function&lt;/h2&gt;
&lt;p&gt;We can do this by creating a simple function that takes an array of Pineapple objects and returns an array of SafePineApple objects.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// SimpleConvertPineApplesToSafety converts an array of Pineapple objects to an array of SafePineApple objects
func SimpleConvertPineApplesToSafety(pineapples []Pineapple) []SafePineApple {
	safePineApples := make([]SafePineApple, len(pineapples))

	for idx, pineapple := range pineapples {
		safePineApples[idx] = pineapple.ToSafePineApple()
	}

	return safePineApples
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function is very &lt;strong&gt;simple&lt;/strong&gt; and &lt;strong&gt;easy to understand&lt;/strong&gt;. We loop through the array of Pineapple objects and convert them to SafePineApple objects.&lt;/p&gt;
&lt;h2&gt;Goroutine without mutex&lt;/h2&gt;
&lt;p&gt;We can do this by using &lt;strong&gt;goroutines&lt;/strong&gt; to work on the array concurrently and store the results in a new array.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func GoroutinesNoMutexConvertPineApplesToSafety(pineapples []Pineapple) []SafePineApple {
	// Create a slice to store the SafePineApples
	safePineApples := make([]SafePineApple, len(pineapples)/2, len(pineapples))
	safePineApples2 := make([]SafePineApple, len(pineapples)/2)

	var wg sync.WaitGroup // Create a WaitGroup to wait for all goroutines to finish
	wg.Add(1)            // Add 1 to the WaitGroup

	// Create a goroutine to convert the first half of the Pineapple objects
	go func(chunk []Pineapple) {
		defer wg.Done() // Decrement the WaitGroup when the goroutine is done
		for idx, pineapple := range chunk { // Loop through the chunk of Pineapple objects
			safePineApples[idx] = pineapple.ToSafePineApple() // Convert the Pineapple object to a SafePineApple object
		}
	}(pineapples[:len(pineapples)/2]) // Pass the first half of the Pineapple objects to the goroutine

	// Convert the second half of the Pineapple objects in the main thread
	for idx, pineapple := range pineapples[len(pineapples)/2:] {
		safePineApples2[idx] = pineapple.ToSafePineApple()
	}

	// Wait for all goroutines to finish
	wg.Wait()

	// Group both pineapples
	safePineApples = append(safePineApples, safePineApples2...)

	// Return the SafePineApples
	return safePineApples
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function is a bit more &lt;strong&gt;complex&lt;/strong&gt; than the simple function. We use a goroutine to convert the first half while the other half is handled by the main thread.
We use a &lt;strong&gt;WaitGroup&lt;/strong&gt; to wait for the goroutine to finish before returning the results.&lt;/p&gt;
&lt;h2&gt;Goroutine with mutex&lt;/h2&gt;
&lt;p&gt;We can also add &lt;strong&gt;mutexes&lt;/strong&gt; to the goroutine to make it &lt;strong&gt;thread safe&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func GoroutinesConvertPineApplesToSafety(pineapples []Pineapple) []SafePineApple {
	// Create a slice to store the SafePineApples
	safePineApples := make([]SafePineApple, len(pineapples))

	// Split the offers into chunks
	chunks := [][]Pineapple{pineapples[:len(pineapples)/2], pineapples[len(pineapples)/2:]}

	mutex := sync.Mutex{} // Create a mutex to lock the slice when writing to it

	var wg sync.WaitGroup // Create a WaitGroup to wait for all goroutines to finish
	wg.Add(1)           // Add 1 to the WaitGroup

	// Create a goroutine to convert the first half of the Pineapple objects
	go func(chunk []Pineapple) {
		defer wg.Done() // Decrement the WaitGroup when the goroutine is done
		for idx, pineapple := range chunk { // Loop through the chunk of Pineapple objects
			mutex.Lock() // Lock the mutex
			safePineApples[idx] = pineapple.ToSafePineApple() // Convert the Pineapple object to a SafePineApple object
			mutex.Unlock()  // Unlock the mutex
		}
	}(chunks[0]) // Pass the first half of the Pineapple objects to the goroutine

	// Convert the second half of the Pineapple objects in the main thread
	for idx, pineapple := range chunks[1] {
		mutex.Lock() // Lock the mutex
		safePineApples[idx+len(chunks[0])] = pineapple.ToSafePineApple() // Convert the Pineapple object to a SafePineApple object
		mutex.Unlock() // Unlock the mutex
	}

	// Wait for all goroutines to finish
	wg.Wait()

	return safePineApples
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function make use of the mutex to lock the slice when writing to it.
This makes sure that the goroutine and the main thread don&apos;t write to the same index at the same time but instead wait for the other to finish.&lt;/p&gt;
&lt;h2&gt;Benchmark&lt;/h2&gt;
&lt;p&gt;Now that we have our functions we can benchmark them to see which one is the &lt;strong&gt;fastest.&lt;/strong&gt;
To benchmark our functions we run them with arrays of different sizes.&lt;/p&gt;
&lt;p&gt;Our benchmark function looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Benchmark_SimpleConvertPineApplesToSafety(b *testing.B) {
	for _, n := range []int{500, 1000, 2000, 5000, 10000} {
		b.Run(fmt.Sprintf(&quot;Benchmark_SimpleConvertPineApplesToSafety-%d&quot;, n), func(b *testing.B) {
			pineApples := make([]Pineapple, n)
			var pine Pineapple
			for i := 0; i &amp;lt; n; i++ {
				_ = faker.FakeData(&amp;amp;pine)
				pine.Created = time.Now().AddDate(0, 0, -i)
				pine.ID = uint(i)
				pine.IsAlive = true
				pineApples[i] = pine
			}
			for i := 0; i &amp;lt; b.N; i++ {
				SimpleConvertPineApplesToSafety(pineApples)
			}
		})
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the benchmark we use the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go test -bench=. -benchtime 5s &amp;gt; benchmark.txt &amp;amp;&amp;amp; benchstat benchmark.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Benchstat is a tool that can be used to compare the results of benchmarks.&lt;/p&gt;
&lt;p&gt;The results of the benchmark are as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name                                                                        time/op
Benchmark_SimpleConvertPineApplesToSafety-500-32                          15.8µs ± 0%
Benchmark_SimpleConvertPineApplesToSafety-1000-32                         32.0µs ± 0%
Benchmark_SimpleConvertPineApplesToSafety-2000-32                         66.5µs ± 0%
Benchmark_SimpleConvertPineApplesToSafety-5000-32                          193µs ± 0%
Benchmark_SimpleConvertPineApplesToSafety-10000-32                         465µs ± 0%
Benchmark_GoroutinesConvertPineApplesToSafety-500-32                      23.5µs ± 0%
Benchmark_GoroutinesConvertPineApplesToSafety-1000-32                     46.2µs ± 0%
Benchmark_GoroutinesConvertPineApplesToSafety-2000-32                     87.7µs ± 0%
Benchmark_GoroutinesConvertPineApplesToSafety-5000-32                      242µs ± 0%
Benchmark_GoroutinesConvertPineApplesToSafety-10000-32                     507µs ± 0%
Benchmark_NoMutexGoroutinesConvertPineApplesToSafety-500-32               28.3µs ± 0%
Benchmark_NoMutexGoroutinesConvertPineApplesToSafety-1000-32              48.7µs ± 0%
Benchmark_NoMutexGoroutinesConvertPineApplesToSafety-2000-32               105µs ± 0%
Benchmark_NoMutexGoroutinesConvertPineApplesToSafety-5000-32               257µs ± 0%
Benchmark_NoMutexGoroutinesConvertPineApplesToSafety-10000-32              533µs ± 0%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see the &lt;strong&gt;simple function is the fastest&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Why ?&lt;/h2&gt;
&lt;p&gt;The reason why the simple function is the fastest is that the process of converting the Pineapple objects to SafePineApple objects is very fast.
The time it takes to create the goroutines and wait for them to finish is longer than the time it takes to convert the Pineapple objects to SafePineApple objects.&lt;/p&gt;
&lt;p&gt;Furthermore, in our goroutines implementation we have to convert the Pineapple objects then lock the mutex, write to the slice and unlock the mutex.
This is a lot of &lt;strong&gt;overhead for a very simple task&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Don&apos;t use goroutines when you don&apos;t need them.&lt;/em&gt; That may seem obvious, but it&apos;s easy to forget when you&apos;re trying to optimize your code.
In this example the overhead of creating the goroutines and waiting for them to finish is &lt;strong&gt;longer&lt;/strong&gt; than the time it takes to convert the Pineapple objects to SafePineApple objects.&lt;/p&gt;
&lt;p&gt;Goroutines are great for tasks that take a &lt;strong&gt;long time to complete&lt;/strong&gt; and can be done in parallel.
I would suggest to write the &lt;strong&gt;simplest code possible&lt;/strong&gt; and then benchmark it to see if you can improve it using goroutines instead.&lt;/p&gt;
&lt;p&gt;Moreover, simple code is &lt;strong&gt;easier to read and maintain&lt;/strong&gt; than complex code, that&apos;s why writing complex code might not be necessary if performance is not an issue.
&lt;strong&gt;I&apos;d prefer to have a simple function that takes a few milliseconds longer to complete than a complex function.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;p&gt;The code for this article can be found on &amp;lt;a href=&quot;https://github.com/CorentinGS/go-teaching/tree/main/goroutines_simple_vs_complex&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt; GitHub &amp;lt;/a&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>How to optimize a Go deployment with Docker </title><link>https://corentings.dev/blog/docker-and-go/</link><guid isPermaLink="true">https://corentings.dev/blog/docker-and-go/</guid><description>Optimize your Go deployment with Docker using multi-stage builds. Reduce image size from 1GB to 15MB with practical Dockerfile examples.</description><pubDate>Tue, 08 Nov 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;How to optimize a Go deployment with Docker ?&lt;/h1&gt;
&lt;p&gt;In this article, I will present the different steps that led me to optimize the deployment of services in &lt;strong&gt;golang&lt;/strong&gt;
using &lt;strong&gt;docker&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;A simple Dockerfile&lt;/h2&gt;
&lt;p&gt;When I started Go and deployed a rest api service for my Memnix application, I used a basic Dockerfile
that I had found on the internet.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM golang:1.19
RUN mkdir -p /go/src/myapp
WORKDIR /go/src/myapp
COPY archived /go/src/myapp

RUN go get -d -v
RUN go install -v

EXPOSE 8080
CMD [&quot;/go/bin/myapp&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem is that I ended up with a 2GB image while my project compiled locally without docker and optimized was only 10MB!
So I decided to try to optimize the size of my Docker image as much as possible.&lt;/p&gt;
&lt;h2&gt;The first step: using a multi-stage build&lt;/h2&gt;
&lt;p&gt;It is possible to separate our Dockerfile in several parts and to use several images:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One image to build the project&lt;/li&gt;
&lt;li&gt;A minimalist image to deploy it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This method allows us not to keep the sources and all the development tools provided with the golang image.
Thus, we only keep the binaries.
I decided to use the &lt;strong&gt;golang:1.19-alpine&lt;/strong&gt; image as the &lt;strong&gt;builder&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM golang:1.19-alpine as builder

LABEL stage=gobuilder
ENV CGO_ENABLED=0
ENV GOOS linux
WORKDIR /build

COPY go.mod go.sum .
RUN go mod download

COPY . .
RUN go get -d -v
RUN go build -o /app/myapp .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This first part of the Dockerfile allows us to copy the sources, to synchronize the packages and to launch the command go build to generate the binary.
Once the build is finished, we use a minimalist alpine image that will be used for deployment. We just need to copy the binary from our builder and run it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM alpine:latest

WORKDIR /app

COPY --from=builder /app/myapp .

EXPOSE 8080

CMD [&quot;/app/myapp&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After testing this new Dockerfile, the final image was 34MB. It&apos;s far from the original 10MB but it&apos;s still much better than 2GB.&lt;/p&gt;
&lt;h2&gt;Improve the build process&lt;/h2&gt;
&lt;p&gt;It is possible in Golang to add flags to the build command in order to slightly optimize the binary size.
After a little research on internet, I discovered the existence of &quot;-s -w&quot; flags. After testing them locally, I noticed an improvement of a few MB so I decided to add them to my Dockerfile.
I also discovered &lt;a href=&quot;https://upx.github.io/&quot;&gt;Upx&lt;/a&gt; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;&amp;lt;/a&amp;gt; which is a small program that allows to compress binary files to reduce their size.
I tested this little software in a terminal and I noticed an improvement of almost &amp;lt;b&amp;gt;50%&amp;lt;/b&amp;gt; on the size of the Memnix api. So I also added this step to my Dockerfile.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM golang:1.19-alpine as builder

LABEL stage=gobuilder
ENV CGO_ENABLED=0
ENV GOOS linux
RUN apk update --no-cache &amp;amp;&amp;amp; apk add upx
WORKDIR /build

COPY go.mod go.sum .
RUN go mod download

COPY . .
RUN go get -d -v
RUN go build -ldflags=&quot;-s -w&quot;  -o /app/myapp .
RUN upx /app/myapp

FROM alpine:3.14

WORKDIR /app

COPY --from=builder /app/myapp .

EXPOSE 8080

CMD [&quot;/app/myapp&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is my new Dockerfile! It may seem much longer and more complex than the first version but in the end, the result is very interesting. After testing this new Dockerfile, the final image is 16Mb so only 6Mb more than the version without Docker which is almost negligible.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Docker is a great tool to simplify application development and deployment, but it can be problematic if used incorrectly. This experience helped me understand how to better use Docker and how to optimize my images.&lt;/p&gt;
&lt;h2&gt;Related Articles&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/optimizing-goroutines-sum-of-squares/&quot;&gt;Solving the Sum of Squares Problem: Optimizing Performance&lt;/a&gt;
This article is inspired by a &amp;lt;a href=&quot;https://twitter.com/GSCorentinDev/status/1564536030795075585?s=20&amp;amp;t=l2hgvKBPDMVs4pS6um2HeA&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;twitter thread&amp;lt;/a&amp;gt; and the complete code of my Dockerfile is available on my &amp;lt;a href=&quot;https://github.com/memnix/memnix-rest/blob/e8e52b3d10731df5b2767f0d24bcdcb5d13d72e3/Dockerfile&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;github&amp;lt;/a&amp;gt;.
I hope this first article will interest you, don&apos;t hesitate to give me feedback so I can improve for the next articles and share your tips on Docker or Golang!&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>