# rss_item

guid: e84cc63a5332c8fdd0279761ced2a9a085f603f5/online_pseudonymity
title: Pseudonymity and the point of no return
url: https://voussoir.net/writing/online_pseudonymity
feedurl: https://voussoir.net/writing/writing.atom
content_mime_type: text/html
content:
            
            <article>
<p><a href="/writing">Back to writing</a></p>

<p><a class="tag_link" data-qualname="introspection" href="/writing/tags/introspection">[introspection]</a> <a class="tag_link" data-qualname="life" href="/writing/tags/life">[life]</a> <a class="tag_link" data-qualname="photography" href="/writing/tags/photography">[photography]</a></p>
<h1>Pseudonymity and the point of no return</h1>
<p>A conundrum. As usual. I must warn you this article is a self-contradictory stream of consciousness mess.</p>
<p>I was raised in the <a href="https://www.youtube.com/watch?v=TpNfQlcIjAM" title="only 90s kids">era</a> where "don't share your real name online" was a standard rule. Pseudonymity practically paramount. I'm not old enough to have taken part in myspace, but I am old enough to have seen the rise of facebook and twitter.</p>
<p>In traditional web forums of the <a href="https://en.wikipedia.org/wiki/VBulletin">vBulletin</a> and <a href="https://en.wikipedia.org/wiki/phpBB">phpBB</a> kind, and in online video games, it is customary to use a pseudonym and an avatar that is not a photo of yourself. Of course, the word "avatar" by definition means something that represents you but isn't literally you. Nowadays, websites don't ask for an avatar, but rather a "profile picture". They want names and faces, and plenty oblige.</p>
<p>When I say that the rule is "don't share your real name online", I don't just mean "don't connect it to your favorite pseudonym" or "make a separate profile with your real name". I mean don't share it at all. As in, you should always only use a pseudonym and there should be zero search results for your real name. That's difficult to guarantee, because any time you participate in a local club, competition, or event, there's a chance you'll be named in a roster or announcement that goes online. Where did I get this rule, anyway? I don't know. That's just how childhood programming works, I guess.</p>
<p>With a few exceptions, this is how I've lived my online life. It stresses me out when my name is published anywhere, especially linked to a physical place like a city or school, because it violates this rule that I have, thus far without enough question, taken as truth. If that doesn't sound stressful to you, perhaps you could imagine finding naked pictures of yourself shared online. If that still doesn't sound stressful, you must already be very popular.</p>
<p>But isn't it foolish for me to call y2k-era forum pseudonymity "traditional"? This is such an obvious bias on my part. People were posting on Usenet <a href="/writing/online_pseudonymity/usenet_berlin_wall.html">while the Berlin wall was coming down</a>, which I was startled to learn. They wrote under their full names, and some even signed off with a postal address. I hope they don't mind that being copied around all these years later.</p>
<p>Anyway, as far as I can remember, this rule never came with a qualifier like "don't share your real name online until you're an adult" or anything like that. Maybe the rule is only for children and I just haven't grown out of it? Like, I was supposed to realize at some point that I no longer have to follow it? No, I'm sure that pseudonymity isn't just for children, or else <a href="https://en.wikipedia.org/wiki/Doxing">doxxing</a> wouldn't be considered such an aggression. I'm not the only one who perceives this rule as being generally applicable, and the use of real names as being a shameful symptom of <a href="https://en.wikipedia.org/wiki/Eternal_September">eternal september</a>:</p>
<blockquote><p>Internet .1 - Finger protocol - Real Names, everyone pretty much knows each other (or the organization where you work) and is atmosphere is generally very friendly.</p>
<p>Internet .2 - Finger is no longer used, malicious users and hackers exist, social networks become very personal (Usenet, IRC) pseudonyms make sense, not just for privacy but to usher in a new sentiment of power and respect through anonymity. Computer security is very low.</p>
<p>Internet .3 - WWW becomes a thing, people still use pseudonyms and generally don't trust anything for good measure, computer security is very low.</p>
<p>Internet .4 - WWW evolves for the masses/commerce and social networks re-emerge on the web (Myspace, Facebook), people start to use real names everywhere. - A new generation exists that was never on the internet before .4. Computer security is much better.</p>
<p>Internet .5 - Pseudonyms don't become popular again thanks to the general ignorance of the tube watchers.</p>
<p>..not entirely accurate..just my 2C</p>
<p><a href="https://news.ycombinator.com/item?id=6871394">wyck</a></p>
</blockquote>
<blockquote><p>When I got on the intertubes, oh-so-many years ago - the rules were simple: the ONLY piece of information you could freely give on public forums, or IRC, was your nick. Now, the only piece of information you are supposed to withhold is your credit card number.</p>
<p>I'll say this about the new ways: I'm extremely glad that I didn't have my teenage years documented and archived. Dodged that bullet!</p>
<p><a href="https://news.ycombinator.com/item?id=6870798">mrspeaker</a></p>
</blockquote>
<p>Here I go again quoting HN users for general purpose life advice. Probably not the best idea.</p>
<p>I generally try to approach problems with a long-term perspective. I prefer things that are high-effort, long-lasting, and deliberate. That's one of the reasons I'd rather maintain my own website than play on any company's <a href="/writing/cancel_this_album">platform</a>. The word "platform" is used so much these days it makes me sick. They spring up, screw people over for a while, then shut down. I'm not going to entrust them with anything I care about.</p>
<p>You could say that it's fine to have some real-name presence online: a boring, ordinary drone that blends in with the crowd; and to use pseudonyms to compartmentalize your hobbies, politics, and anything controversial. This is challenging because the more time you spend living in those pseudonyms, the larger a portion of your total life they become, and the more oppressive those compartment walls feel. If you do maintain perfect opsec, and keep your pseudonyms from ever touching, what do you have? A dozen identities, each a shallow <a href="https://en.wikipedia.org/wiki/Magical_objects_in_Harry_Potter#Horcruxes">dozenth</a> of a person, and a dozen groups of peers that aren't allowed to mix? I don't think I want to invest such portions of my life into siloed identities, leaving my real name a boring husk of no consequence. I will only ever get to be one meatbag, at the end of the day. We like to treat the internet as if it's a separate place where you live a separate life, but it's not. This is your actual life.</p>
<p>When I first made my reddit account, I chose the name <a href="https://old.reddit.com/u/GoldenSights">/u/GoldenSights</a> instead of <a href="https://old.reddit.com/u/voussoir">/u/voussoir</a> because I specifically wanted to use a name that I wasn't using anywhere else. I assumed reddit would be a low-investment site that I would not want to connect back to my identity. But, through reddit's culture of <a href="https://github.com/voussoir/reddit">bot programming</a> (<a href="https://en.wikipedia.org/wiki/2023_Reddit_API_controversy">RIP</a>) I discovered Python, and now I've been writing Python code for nine years. That's not the kind of time investment I want to keep hidden in a corner. If someone asks me what I've been up to for the past nine years, am I supposed to sheepishly say "uh, nothing, I can't tell you" so that I don't pwn myself? What a waste.</p>
<p>"Maintain perfect opsec". What kind of CIA espionage LARP am I on here? This sounds stupid. But really, there isn't that much middle ground. Either you allow your online names to lead back to your meatbag self, or you don't.</p>
<blockquote><p>I gave up anonymity. I just learned to lean into taking control of my ID. Some time ago, I realized that there's no way for me to participate online, without things being attributed to me.</p>
<p>I learned this, by setting up a Disqus ID. I wanted to comment on a blog post, and started to set up an account.</p>
<p>After I started the process, it came back, with a list of random posts, from around the Internet (and some, very old), and said "Are these yours? If so, would you like to associate them with your account?"</p>
<p>I freaked. Many of them were outright troll comments (I was not always the haloed saint that you see before you) that I had sworn were done anonymously. They came from many different places (including DejaNews). I have no idea how Disqus found them.</p>
<p>Every single one of them was mine. Many, were ones that I had sworn were dead and buried in a deep grave in the mountains.</p>
<p><a href="https://news.ycombinator.com/item?id=31883896">ChrisMarshallNY</a></p>
</blockquote>
<blockquote><p>In fact, only two or three decades ago it was standard for the telephone service to print everyone's name, address, and phone number in a big book, distribute it to every house and charge you $5 a month extra if you wanted to opt out of the list.</p>
<p><a href="https://news.ycombinator.com/item?id=37375010">13of40</a></p>
<blockquote><p>In the past I've tracked down the names of people in old photo's with a viable house number by guessing the city and searching newspaper archives. Then confirm with a streetview. The archives will have articles mentioning people with their street address.</p>
<p>My thought is Americans have become really paranoid compared to people 50 years ago. And people are often under the illusion that people can find out a lot about you with little effort isn't true.</p>
<p><a href="https://news.ycombinator.com/item?id=37375229">Gibbon1</a></p>
</blockquote>
</blockquote>
<p>At the moment, <a href="https://github.com/voussoir">my github page</a> already shows my real name. That cat is out. I added that because as a computer science student there is a cultural expectation to use one's github profile as a public portfolio and to include it on resumes. I mean, once again, it would be really weird to say "I've been writing code as a hobby for this many years but I can't show it to you". I guess the pressure for employment was enough for me to break the rule. For a few years I had a linkedin account with a real photo, but I deleted that because linkedin is gross.</p>
<p>I am usually pretty good at resisting cultural expectations if I want to. I've never tasted alcohol, for example, and I don't swear. I never did use facebook or twitter or instagram or vine or tiktok. And while it'd be more noble to say that I am simply following my own beliefs and methods regardless of expectations, I know that I deliberately go out of my way to be contrarian. To not be a <a href="https://en.wiktionary.org/wiki/normie">normie</a>. But although I'm not harboring a secret desire to drink or to watch tiktok, and I am inwardly convinced of these choices, there's no doubt it is also very much an outward thing, a desire to be seen by others as the kind of person who does not follow the norm.</p>
<p>Sharing your real name and location online is different from following the other kinds of trends, though, because it involves crossing a significant point of no return. If I decide to try a drink, I can then decide to not have another one. I'd never be able to truthfully say I haven't tasted alcohol ever again — it would be a crossing in its own right — but it won't bring me any kind of future harm.</p>
<p>But once your name and location are public, you can't take it back. There is no return to anonymity. Is that important? Will that be a problem? Will it hurt me? Probably not. 99% chance not. But if it does, you can't go back. As long as you remain anonymous, you have unlimited time to ponder the consequences of deanonymizing; you can always delay that crossing. But once you are deanonymized, no amount of pondering can undo it. Are you sure you've thought it through? How sure?</p>
<p>The fearmongering that exists, or at least used to exist when I was young, about sharing your identity on the internet is that you'll get harassed by <a href="https://en.wikipedia.org/wiki/Mean_world_syndrome">stalkers, molesters, and other crazies</a>. Or, at least, scammers attempting identity theft or fraud or impersonation.</p>
<p>Wait... that doesn't make sense. Is that what this all comes down to? You've got to be kidding me. It seems like the vast majority of normies make their name, face, birthday, family ties, and whereabouts relatively public on social media, and the vast majority are not victims of death threats or fraud. It's not as rare as being struck by lightning, but if that's the entire scope of the threat then it doesn't seem worth the disproportionate worry about having my name online.</p>
<p>There are already billions of strangers' names on the internet. To me, I am very important, but to everybody else I am just one more stranger. It seems like knowing someone's home address is a big deal, but if you pan around on a map you've got thousands of people's home addresses right before your eyes. How meaningless. The fact that I write a blog makes me ever so slightly different from the norm, but the stuff I write is not nearly as inflammatory or unhinged as a whole lot of other stuff that's out there, available under the author's real name. I know the names, faces, birthdays, and/or hometowns of various filmmakers, actors, authors, athletes, musicians, CEOs, politicians, professors, and youtubers, and they're doing fine. Why would I not also be fine?</p>
<blockquote><p>Horsley's website, created in 1997, listed roughly 200, as the website called them, "abortionists." These abortion providers were listed in three different fonts, as described by the site's legend: "Black font (working); Greyed-out Name (wounded); Strikethrough (fatality)." In addition to this list, the website included the abortion providers' personal information, such as their home addresses, phone numbers, and photographs.</p>
<p>The website, donned with blood-dripping graphics, both celebrated providers' deaths and, with a wink and a nod, encouraged others to harm the remaining providers on the list so that more names could be crossed off.</p>
<p>[...]</p>
<p>This fear is justified. Since 1993, eight abortion providers have been murdered because of their jobs. The most recent assassination was of Dr. George Tiller in 2009, shot while he was serving as an usher in his church. Several of the murders were preceded by Old West–style "wanted" posters that featured providers' personal information, a tactic still in use today.</p>
<p><a href="/writing/online_pseudonymity/slate_neal_horsley.html">Slate - Strikethrough (Fatality)</a></p>
</blockquote>
<p>Ok, so, there really are risks associated with deanonymizing. There are also risks associated with stepping outside and eating solid foods. I don't expect to be molested and murdered over my work, but it's... possible. While I was proofreading this article, Maddox <a href="https://www.youtube.com/watch?v=tgDIZ17440o" title="Is Justin Whang a plagiarist and a hack, or just a plagiarist?">released</a> an hour long tale of stalking and harassment, and I guess I'll just keep telling myself it wouldn't happen to me because I'm not as inflammatory as he is. Or maybe I just shouldn't risk it. I have two choices: one carries a small chance of harm and the other carries none; so even if I can't actually justify the thought process, I may as well stay on the safe side.</p>
<p><em>Bang!</em></p>
<p>One of my assumptions has just burst. The assumption that remaining pseudonymous forever is not harmful. I am very lonely. Like, <a href="/writing/e_unibus_pluram_1993">Joe Briefcase</a> lonely. A lifetime of compartmentalization and self-imposed isolation between my real life and hobbies and work and creations and thoughts is taking its toll. My refusal to share my digital pseudonymous work with real-life coworkers / friends / acquaintances, and my refusal to make myself available for discovery / contact online is successfully preventing me from meeting like-minded people. There's more to it than that, but this isn't helping. I'm pretty certain that the <a href="https://en.wikipedia.org/wiki/Expected_value">expected value</a> of this compartmentalized lifestyle is worse than a more public one.</p>
<p>Another reason to be more public is that if you don't define the web search results that come up for your name, you could be giving that privilege to someone else. If you ever do acquire some kind of enemy, and they publish your name and face with some message of hate, and they get to dominate the search results about you because you've kept all the good things about yourself a secret, that would kind of suck. Is this too contrived? Maybe.</p>
<center><a href="https://www.youtube.com/watch?v=sXE8LdXzeHM" style="color:currentcolor; text-decoration:none;">* * *</a></center><p>This problem is on my mind because recently I have been attending some free outdoor concerts put on by the city as part of my effort to get out of the house more and curb my loneliness. I always bring my camera and take pictures and email them to the band as a way of giving something back. Plus it is useful and fun practice for me.</p>
<p>The reason this is a problem is because some of the pictures I've taken are actually really good, if you'll excuse the self-praise. I've got quite a few photos now that I think are pretty special, but you wouldn't know it because my <a href="https://voussoir.net/photography">/photography</a> page is stagnating under the self-imposed pressure to not reveal who or where I am. In my <a href="/writing/hobby_photography">hobby photography article</a>, I already wrote that I'm not interested in taking meaningless eye candy pictures. Well, it turns out that meaningful pictures usually involve some quantity of recognizable people and places, at least for the genre and manner of photography I want to do.</p>
<p>So far, the responses I've gotten from the bands has been (presumably genuine) ecstatic appreciation. One of the guys I took pictures of last week was in the audience this week, and he and his wife approached me to say how grateful they were. As I said in <a href="/writing/motivations_for_writing">my motivations for writing</a>, visual arts can wow people in a way that my other hobby, computer programming, cannot. People actually <em>want</em> to share my photos on their website, and they <em>want</em> to credit me. "You can post the pictures but I prefer not to be credited" is so odd. Who do I think I am, some kind of Batman? Here I am sending an email to a musician whose email address I got from their website with their name and hometown and upcoming show dates, all public, and I'm playing Batman. My logic is completely incoherent. If I'm afraid of my work being known, maybe I don't deserve to do it in the first place.</p>
<p>Now that it's been a few weeks, I'm starting to learn the names of some of the also-regulars at these concerts. I've run into neighbors, coworkers, and even my dentist. Apparently one of the also-regulars goes to the same dentist. Is this what having a <a href="https://www.youtube.com/watch?v=VvdQ381K5xg" title="Not Just Bikes - The Great Places Erased by Suburbia (the Third Place)">third place</a> is like? People are beginning to ask questions:</p>
<blockquote><p>Them:<br/>
Are you shooting for a newspaper?</p>
<p>Me:<br/>
No, just for fun.</p>
<p>Them:<br/>
Oh, what's your instagram?</p>
<p>Me:<br/>
Sorry, I don't do instagram.</p>
<p>Them:<br/>
Do you have a website where I can see your work?</p>
<p>Me:<br/>
Ummmm... I was just gonna email the band directly.</p>
</blockquote>
<p>I don't give them a link to my website, partly because I don't know if I want to build connections between voussoir.net and my real identity, and partly because I would look like a big idiot with my 'portfolio' full of anonymous closeups and abstracts, with none of the live band photos which I just told them I've been taking for the past four weeks. So instead I just look like a different kind of big idiot who takes five hundred pictures and apparently does almost nothing with them and awkwardly terminates the conversation at any mention of an outlet. Gotta go, bye!</p>
<p>I am not interested in making a new pseudonym and buying a new domain name for this purpose, either. Once again, I don't want to put years of investment into another identity that isn't me, leaving my actual identity empty and uninvested. Besides, all it takes is one person to share that domain in a facebook comment with "thanks to [my name] for taking pictures at our event" and it's blown anyway. May as well be ahead of it.</p>
<p>So, my real name is already tied to my website. That line has been crossed. But to publish these pictures would be to cross another line: tying my identity to a city. An environment for which I have already <a href="/writing/not_just_bikes">expressed</a> some <a href="https://www.youtube.com/watch?v=xN4Ev8AfV3E" title="YTSPPOVVLOG #49: Street Photography in Suburbia, California">disdain</a>. With named buildings in the background and the band's name on their banner or drum set, you could find exactly where I was... and where I'll be next week. That's pretty normal, but I don't like it. My other option is to let this body of work stagnate.</p>
<p>I've been using the word "portfolio", but I'll reiterate that this is absolutely not some kind of side-hustle for me. I want to publish these pictures on my website not to generate leads, but because I'm proud of them. I publish all of my Python code for fun, and I want to publish my pictures for fun, too. Also, because when I'm standing face-to-face with someone asking "Can I see more of your work?", I want to be able to say "Yes", instead of "No".</p>
<p>You're overthinking it!</p>
<p>Feedback wanted.</p>
<hr/>
<p><strong>Update</strong>: I have not received any feedback, and I've taken some more good pictures. <a href="https://voussoir.net/photography">Scales tipped</a>.</p>
<p><img alt="" loading="lazy" src="/writing/online_pseudonymity/photo1.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/online_pseudonymity/photo2.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/online_pseudonymity/photo3.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/online_pseudonymity/photo4.jpg"/></p>
<!-- By the way, here is a tip for any photographers or photographer-hopefuls if you're feeling discouraged about the quality of your work compared to pictures you see online: stay away from photography forums, at least for a little while, and go deal with regular people instead. When you take pictures of a small local band (or dance group or festival or what have you), you might very well be the first person to ever do so. If you share those pictures on a photography forum, you'll get a bunch of tryhards comparing your work to every band photo available on the world wide web; if you share them with the band themselves, they'll say "we've never gotten anything like this before, thank you, what a gift!". It's pretty obvious which opinion is worth more. Your work doesn't have to be the best in the world, because the rest of the world doesn't matter to that one group. -->

<hr/>
<p><a href="https://git.voussoir.net/voussoir/voussoir.net/commits/master/voussoir.net/writing/online_pseudonymity/online_pseudonymity.md">View this document's history</a></p>
<ul>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/89c431a15ac1df93751f56f915c63691f0dda366"><time datetime="2023-09-12T19:43:52-07:00">2023-09-12</time> Scales tipped.</a></li>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/798f55e0e5a2d379fe8544687799574f6220eb2d"><time datetime="2023-09-04T11:12:58-07:00">2023-09-04</time> Add phonebook comment.</a></li>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/e84cc63a5332c8fdd0279761ced2a9a085f603f5"><time datetime="2023-08-30T21:32:12-07:00">2023-08-30</time> Add online_pseudonymity.md.</a></li>
</ul>
<address>Contact me: writing@voussoir.net</address>
<p>If you would like to subscribe for more, add this to your RSS reader: <a href="/writing/writing.atom" rel="alternate" type="application/atom+xml">https://voussoir.net/writing/writing.atom</a></p>
</article>
            
            


guid: f93f5eebb89e0bce80206733fe752fb9d87e3ed3/git_dot_voussoir_dot_net
title: git.voussoir.net
url: https://voussoir.net/writing/git_dot_voussoir_dot_net
feedurl: https://voussoir.net/writing/writing.atom
content_mime_type: text/html
content:
            
            <article>
<p><a href="/writing">Back to writing</a></p>

<h1>git.voussoir.net</h1>
<p>Github has been bothering me. I don't like their <a href="/writing/git_dot_voussoir_dot_net/lets_build_from_here.html">drunk-on-buzzwords marketing copy</a> or the <a href="/writing/git_dot_voussoir_dot_net/copilot.png">advertisements they're placing above my work</a>. If I give someone a link to my work, I want to feel proud of it.</p>
<p>So, I installed <a href="https://gitea.com">Gitea</a> on my server and uploaded everything to <a href="https://git.voussoir.net">git.voussoir.net</a>. This was easy because I have no investment or dependency on github's issue tracker, project tracker, wiki, pages, CI, Actions, or whatever other proprietary lock-ins they offer. I will need to move some image assets eventually and that's about it.</p>
<p>I had tried <a href="https://gogs.io/">Gogs</a> at first, but it was failing on every push with a <a href="/writing/git_dot_voussoir_dot_net/gogs_502.png">502 error</a> that I couldn't diagnose.</p>
<p>I followed Gitea's <a href="https://docs.gitea.com/installation/install-from-binary">installation from binary</a> instructions and updated my nginx settings. From start to finish, the process took maybe an hour and a half, which was a little longer than it should have been because gitea was trying to do some mkdir in <code>/usr/local/bin/data/tmp</code> even though I asked it to use a different path. I had to manually set <code>APP_DATA_PATH</code> in the <code>.ini</code> file to make it stop doing that, and now it's fine. Even if I accidentally explode the whole server somehow, I can just reinstall gitea from scratch and push it back again. I <em>still</em> don't plan to use the issue tracker or whatever CI they come up with. I am deliberately calling it git.voussoir.net instead of gitea.voussoir.net in case I change the backing software.</p>
<p>...</p>
<p>Just a week after setting up Gitea, I learned that Gitea made a controversial move last year in forming a for-profit entity, and a new non-profit fork called <a href="https://forgejo.org/faq/#why-was-forgejo-created">Forgejo</a> has started. I haven't personally being slighted. I just got here. But the words "non-profit" are like catnip to me so it looks like git.voussoir.net runs Forgejo now. Wow, my foresight paid off!</p>
<p>I won't delete my github account in the foreseeable future, because it's free and easy to keep pushing to all remotes. But github is losing my favor, and that's great for them too because I'm a nobody and I wasn't paying them anyway. So long, and thanks for all the fetch.</p>
<p>Note: If you want to keep it even simpler than this, git does have a web UI built in. See <a href="https://git-scm.com/book/en/v2/Git-on-the-Server-GitWeb">gitweb</a>.</p>

<hr/>
<p><a href="https://git.voussoir.net/voussoir/voussoir.net/commits/master/voussoir.net/writing/git_dot_voussoir_dot_net/git_dot_voussoir_dot_net.md">View this document's history</a></p>
<ul>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/f93f5eebb89e0bce80206733fe752fb9d87e3ed3"><time datetime="2023-09-18T21:03:43-07:00">2023-09-18</time> Add git_dot_voussoir_dot_net.md.</a></li>
</ul>
<address>Contact me: writing@voussoir.net</address>
<p>If you would like to subscribe for more, add this to your RSS reader: <a href="/writing/writing.atom" rel="alternate" type="application/atom+xml">https://voussoir.net/writing/writing.atom</a></p>
</article>
            
            


guid: ce1d388279b781c375e4d645875145324a5f15ae/notes_on_paper
title: Notes on paper
url: https://voussoir.net/writing/notes_on_paper
feedurl: https://voussoir.net/writing/writing.atom
content_mime_type: text/html
content:
            
            <article>
<p><a href="/writing">Back to writing</a></p>

<p><a class="tag_link" data-qualname="today_i_did_this" href="/writing/tags/today_i_did_this">[today_i_did_this]</a></p>
<h1>Notes on paper</h1><ol id="table_of_contents">Table of contents<li><a href="#introduction">Introduction</a></li><li><a href="#habits">Habits</a></li><li><a href="#scanning">Scanning</a></li><li><a href="#some_pages">Some pages</a></li></ol>
<h2 id="introduction">Introduction<a class="header_anchor_link" href="#introduction"> (§)</a></h2>
<p>Over the past few weeks, I have been scanning my notes and homework from school using the nice professional scanners at my workplace. These notes have been sitting in my closet for several years because I don't want to keep them, but didn't want to throw them out until scanning them, and I've never had access to a scanner that could do all these pages.</p>
<p>This has prompted me to reflect on some of the habits and choices that I follow in my handwritten notes. I realize there are entire online cultures of journaling and notetaking and notebook-buying, and I'm not here to compete with them. This is just what I do.</p>
<h2 id="habits">Habits<a class="header_anchor_link" href="#habits"> (§)</a></h2>
<p>Always put a date on every page, including pages that are given to you. For multiple pages in one day, it helps to add a page number.</p>
<p>Always use loose leaf paper, never bound notebooks.</p>
<ul>
<li>You can hand a single sheet to someone.</li>
<li>You can rewrite a sheet later and put it back in the same order, instead of keeping or tearing out the bad copy.</li>
<li>Easier to cross reference a previous day without flipping back and forth.</li>
<li>Easier to integrate with other material: pages you receive, homework you submit and get back.</li>
<li>Easier to purchase the same or equivalent paper over the course of years, rather than developing an eclectic assortment of different notebooks or, worse, a brand dependency.</li>
<li>Easier to scan.</li>
</ul>
<p>Always use full-size paper (US Letter or A4 depending on your region) so that your notes stack well with printed material. A lot of lined/punched notebook paper is <a href="/writing/notes_on_paper/2018-03-05.png">smaller</a> than printer paper.</p>
<p>Prefer to write in black ink rather than colored ink or pencil. Use quality pens that produce deep black lines, not cheap pens that produce off-black or hollow lines.</p>
<ul>
<li>Easier to scan in 1-bit format.</li>
<li>I like black, what can I say.</li>
</ul>
<p>Always write on a single side of the paper, never the backside, unless forced to do so. The dollar cost of the paper is less than the <a href="/writing/notes_on_paper/2018-03-06.png">inconvenience</a> cost of double sided material.</p>
<p>Resist the urge to cram everything onto one page. The telltale switch from big handwriting to "I'm running out of space" small handwriting at the bottom of the page is shameful. Two pages with healthy margins and spacing is better than one page of cramming.</p>
<p>Consider using graph paper, even for non-mathematics, non-engineering notes. Prefer faint gridlines. You can:</p>
<ul>
<li>Accurately indent line continuations, sub-commentary, or <a href="/writing/notes_on_paper/2017-10-24.png">nested bullet points</a>.</li>
<li>Write in a <a href="/writing/notes_on_paper/2017-02-21.png">two-column</a> format.</li>
<li>Cordon off side-thoughts in <a href="/writing/notes_on_paper/2016-04-03.png">boxes</a>.</li>
<li>Draw pictures or diagrams of <a href="/writing/notes_on_paper/2017-02-02.png">consistent size and spacing</a>.</li>
</ul>
<p>Consider using plain, blank printer paper. At work, I am currently using blank printer paper for notetaking, and my "manuscript" is up to about 140 sheets. I don't think I would recommend this for structured school lectures, but it's good for unstructured meetings.</p>
<ul>
<li>Very low cost per sheet.</li>
<li>Complete freedom to draw (beware, too much freedom = <a href="/writing/notes_on_paper/2017-10-17.png">wobbly, drifting text</a>).</li>
</ul>
<p>As a result of writing this article, I have decided to try going back to graph paper for my work notes. This time, instead of using store-bought graph paper, I'm just printing my own. I created an <a href="/writing/notes_on_paper/graphpaper_025_grid_240.svg">svg file</a> with a half-inch margin and a whisper-faint quarter-inch grid. This is working out great and I wish I had done it sooner.</p>
<ul>
<li>Pattern, margin, lightness, and paper stock tailored exactly to your preferences.</li>
<li>Trying new patterns is completely free — no commitment to a reamful of one pattern.</li>
<li>No dependence on brand or manufacturer. As long as you can find blank paper that suits you, you can make your graph paper.</li>
<li>Cost per sheet still extremely low when using laser printers.</li>
<li>Very consistent across pages — cheap store-bought graph paper often has inconsistent grid offset from page to page.</li>
<li>Lightness of gridlines must be tuned per-printer by experimentation.</li>
<li>The paper may become slightly curled or wavy due to the heat and rollers — the paper will need time and/or weight to flatten back out.</li>
</ul>
<p>Treat yourself to heavier paper stock. The cheapest printer paper is usually <a href="https://en.wikipedia.org/wiki/Grammage">20lb or 75gsm</a>. Try something heavier like 28lb/105gsm or 32lb/120gsm. Heavier paper costs more, but it feels more pleasant to write on. If you enjoy it more, you'll do it more.</p>
<p>Consider re-writing your notes after your class/meeting, so that you can:</p>
<ul>
<li>Reorganize the information in a new order that you could not have foreseen when taking it down.</li>
<li>Use slower, more careful handwriting.</li>
<li>Replace abbreviations with full words and phrases.</li>
<li>Eliminate the scratched-out mistakes and keep only the good parts.</li>
</ul>
<p>I have never used a digital tablet for notetaking, and I will not make the appeal to tradition that paper is always better. I can imagine it brings some advantages:</p>
<ul>
<li>Don't need to scan it.</li>
<li>Instant OCR makes it searchable (assuming software support) (requires non-awful handwriting).</li>
<li>Can mix typing for perfect legibility with drawing for visual aids.</li>
<li>Easier to integrate photos taken on the spot.</li>
<li>Infinite paper and ink.</li>
<li>Undo button, layers, drag to rearrange.</li>
</ul>
<p>But paper has its own advantages:</p>
<ul>
<li>Cheap.</li>
<li>Easier to lay out multiple sheets and review / cross-reference pages in any order — not limited to real estate of one screen and scrolling / tab-switching.</li>
<li>You can hand someone a single sheet so they can read it while you work on other sheets — you're not handing them your entire system.</li>
<li>Easier to integrate with material you receive.</li>
<li>Open-note exams will probably only allow paper for quite some time yet.</li>
</ul>
<h2 id="scanning">Scanning<a class="header_anchor_link" href="#scanning"> (§)</a></h2>
<p>I scanned everything at 600 PPI in 1-bit TIFF. The scanners at work don't seem to support lossless full color, and I'd rather have the aliasing and dithering artifacts of lossless 1-bit than the jpeggy, blocky artifacts of lossy color. This is an aesthetic preference and I think dithering looks cool anyway.</p>
<p>I wrote <a href="https://git.voussoir.net/voussoir/cmd/src/branch/master/imagesequence_to_images.py">imagesequence_to_images.py</a> to turn the scanner's multi-page TIF file into separate PNGs.</p>
<p>I use <a href="https://git.voussoir.net/voussoir/cmd/src/branch/master/brename.py">brename.py</a> to rename everything.</p>
<p>When dealing with double-sided material and a single-sided scanner, I scanned the stack twice for the fronts and backs. Then, I did something like this:</p>
<div class="highlight batch"><pre><span class="k">md</span> <span class="">fronts</span> <span class="p">&amp;&amp;</span> <span class="k">move</span> <span class="">fronts*.png</span> <span class="">fronts</span><span class=""></span>
<span class="k">md</span> <span class="">backs</span> <span class="p">&amp;&amp;</span> <span class="k">move</span> <span class="">backs*.png</span> <span class="">backs</span><span class=""></span>

<span class="k">cd</span> <span class="">fronts</span><span class=""></span>

<span class="">#</span> <span class="">Fronts</span> <span class="">will</span> <span class="">get</span> <span class="">even</span> <span class="">index</span> <span class="">numbers</span><span class="">,</span> <span class="">starting</span> <span class="">from</span> <span class="">zero.</span><span class=""></span>
<span class="">brename</span> <span class="s2">"</span><span class="s2">f'{(index*2):03d}.png'</span><span class="s2">"</span><span class=""></span>

<span class="k">cd</span> <span class="">..\backs</span><span class=""></span>

<span class="">#</span> <span class="">Large</span> <span class="">number</span> <span class="">minus</span> <span class="">index</span> <span class="">to</span> <span class="">reverse</span> <span class="">the</span> <span class="">file</span> <span class="">order.</span><span class=""></span>
<span class="">brename</span> <span class="s2">"</span><span class="s2">f'{(999-index):03d}.png'</span><span class="s2">"</span><span class=""></span>

<span class="">#</span> <span class="">Backs</span> <span class="">will</span> <span class="">get</span> <span class="">odd</span> <span class="">index</span> <span class="">numbers</span><span class="">,</span> <span class="">starting</span> <span class="">from</span> <span class="">one.</span><span class=""></span>
<span class="">brename</span> <span class="s2">"</span><span class="s2">f'{(index*2)+1:03d}.png'</span><span class="s2">"</span><span class=""></span>

<span class="k">cd</span> <span class="">..</span><span class=""></span>
<span class="k">move</span> <span class="">fronts\*</span> <span class="">.</span> <span class="p">&amp;&amp;</span> <span class="k">rmdir</span> <span class="">fronts</span><span class=""></span>
<span class="k">move</span> <span class="">backs\*</span> <span class="">.</span> <span class="p">&amp;&amp;</span> <span class="k">rmdir</span> <span class="">backs</span><span class=""></span>

<span class="">#</span> <span class="">...</span><span class=""></span>
<span class="">#</span> <span class="">Spend</span> <span class="">a</span> <span class="">few</span> <span class="">minutes</span> <span class="">manually</span> <span class="">deleting</span> <span class="">the</span> <span class="">blank</span> <span class="">backsides.</span><span class=""></span>
<span class="">#</span> <span class="">...</span><span class=""></span>

<span class="">#</span> <span class="">Reindex</span> <span class="">all</span> <span class="">the</span> <span class="">files</span> <span class="">so</span> <span class="">we</span> <span class="">can</span> <span class="">pretend</span> <span class="">the</span> <span class="">blank</span> <span class="">backsides</span> <span class="">didn't</span> <span class="">exist.</span><span class=""></span>
<span class="">#</span> <span class="">Renaming</span> <span class="">will</span> <span class="">fail</span> <span class="">if</span> <span class="">the</span> <span class="">target</span> <span class="">filename</span> <span class="">already</span> <span class="">exists</span><span class="">,</span> <span class="">so</span> <span class="">temporarily</span><span class=""></span>
<span class="">#</span> <span class="">bump</span> <span class="">up</span> <span class="">to</span> <span class="">more</span> <span class="">digits</span><span class="">,</span> <span class="">then</span> <span class="">back</span> <span class="">down.</span><span class=""></span>
<span class="">brename</span> <span class="s2">"</span><span class="s2">f'{index1:04d}.png'</span><span class="s2">"</span><span class=""></span>
<span class="">brename</span> <span class="s2">"</span><span class="s2">f'{index1:03d}.png'</span><span class="s2">"</span><span class=""></span>
</pre></div><h2 id="some_pages">Some pages<a class="header_anchor_link" href="#some_pages"> (§)</a></h2>
<p>Here are some pages that I thought were visually interesting. These are not perfect demonstrations of the habits above, and that's the point — over time I have realized things I should have been doing all along.</p>
<p>I find there's something really engaging about being able to zoom in and pan around and pixel peep at these millimeter-sized features that, at the time of their making, only occupied a few tenths of a second of attention. The small writ large, as it were.</p>
<p>I am including more pages here than most anybody will care to look through, but I couldn't pick any more to eliminate. I think they're kind of cool.</p>
<style>
section
{
    text-align: center;
}
</style><section>
<a href="/writing/notes_on_paper/2015-10-29.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2015-10-29.png"/></a>
<a href="/writing/notes_on_paper/2015-11-19.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2015-11-19.png"/></a>
<a href="/writing/notes_on_paper/2016-04-08.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2016-04-08.png"/></a>
<a href="/writing/notes_on_paper/2016-04-25.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2016-04-25.png"/></a>
<a href="/writing/notes_on_paper/2017-01-12.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-01-12.png"/></a>
<a href="/writing/notes_on_paper/2017-01-26.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-01-26.png"/></a>
<a href="/writing/notes_on_paper/2017-02-02.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-02-02.png"/></a>
<a href="/writing/notes_on_paper/2017-02-28.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-02-28.png"/></a>
<a href="/writing/notes_on_paper/2017-04-13.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-04-13.png"/></a>
<a href="/writing/notes_on_paper/2017-04-21.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-04-21.png"/></a>
<a href="/writing/notes_on_paper/2017-05-01.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-05-01.png"/></a>
<a href="/writing/notes_on_paper/2017-05-03.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-05-03.png"/></a>
<a href="/writing/notes_on_paper/2017-05-15.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-05-15.png"/></a>
<a href="/writing/notes_on_paper/2017-05-22.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-05-22.png"/></a>
<a href="/writing/notes_on_paper/2017-06-22.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-06-22.png"/></a>
<a href="/writing/notes_on_paper/2017-06-29.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-06-29.png"/></a>
<a href="/writing/notes_on_paper/2017-07-11.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-07-11.png"/></a>
<a href="/writing/notes_on_paper/2017-10-06.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-10-06.png"/></a>
<a href="/writing/notes_on_paper/2017-10-13.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-10-13.png"/></a>
<a href="/writing/notes_on_paper/2017-10-20.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-10-20.png"/></a>
<a href="/writing/notes_on_paper/2017-10-30.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-10-30.png"/></a>
<a href="/writing/notes_on_paper/2017-11-05.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-11-05.png"/></a>
<a href="/writing/notes_on_paper/2017-11-13.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-11-13.png"/></a>
<a href="/writing/notes_on_paper/2017-11-27.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2017-11-27.png"/></a>
<a href="/writing/notes_on_paper/2018-01-03.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2018-01-03.png"/></a>
<a href="/writing/notes_on_paper/2018-01-12.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2018-01-12.png"/></a>
<a href="/writing/notes_on_paper/2018-01-22.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2018-01-22.png"/></a>
<a href="/writing/notes_on_paper/2018-01-26.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2018-01-26.png"/></a>
<a href="/writing/notes_on_paper/2018-01-29.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2018-01-29.png"/></a>
<a href="/writing/notes_on_paper/2018-01-31.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2018-01-31.png"/></a>
<a href="/writing/notes_on_paper/2018-02-12.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2018-02-12.png"/></a>
<a href="/writing/notes_on_paper/2018-06-20.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2018-06-20.png"/></a>
<a href="/writing/notes_on_paper/2018-10-10.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2018-10-10.png"/></a>
<a href="/writing/notes_on_paper/2018-11-07.png"><img height="512" loading="lazy" src="/writing/notes_on_paper/small_2018-11-07.png"/></a>
</section>

<hr/>
<p><a href="https://git.voussoir.net/voussoir/voussoir.net/commits/master/voussoir.net/writing/notes_on_paper/notes_on_paper.md">View this document's history</a></p>
<ul>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/d95c72e72997a8d8973926546c9944e3af2b276c"><time datetime="2024-03-03T11:58:59-08:00">2024-03-03</time> Add note about heavier paper stock.</a></li>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/11ff76d768b69966eca9d759e955396e2334d12b"><time datetime="2023-10-15T14:40:29-07:00">2023-10-15</time> Add paragraph on printed graph paper.</a></li>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/ce1d388279b781c375e4d645875145324a5f15ae"><time datetime="2023-09-24T16:31:19-07:00">2023-09-24</time> Add notes_on_paper.md.</a></li>
</ul>
<address>Contact me: writing@voussoir.net</address>
<p>If you would like to subscribe for more, add this to your RSS reader: <a href="/writing/writing.atom" rel="alternate" type="application/atom+xml">https://voussoir.net/writing/writing.atom</a></p>
</article>
            
            


guid: 25fcaa458034edaf1fcf9a918b655bcf7fb966ee/blacked_out_lens_markings
title: Blacked out lens markings
url: https://voussoir.net/writing/blacked_out_lens_markings
feedurl: https://voussoir.net/writing/writing.atom
content_mime_type: text/html
content:
            
            <article>
<p><a href="/writing">Back to writing</a></p>

<p><a class="tag_link" data-qualname="photography" href="/writing/tags/photography">[photography]</a></p>
<p><a class="tag_link" data-qualname="today_i_did_this" href="/writing/tags/today_i_did_this">[today_i_did_this]</a></p>
<h1>Blacked out lens markings</h1>
<p>My camera lenses have white lettering printed and etched into the front describing their properties. I decided to paint over it.</p>
<p>Why:</p>
<ol>
<li><p>I like stuff that is debadged or debranded and I think it will look cool.</p>
</li>
<li><p>I think / assume that the white lettering is distracting and catches attention, which is bad for candids.</p>
</li>
<li><p>I don't need my lens to remind me what it is. <a href="https://www.youtube.com/watch?v=WMO0mPb-DlM" title="Pulp Fiction - I don't need you to tell me how good my coffee is">I'm the one who bought it</a>.</p>
</li>
<li><p>I don't need my lens to tell everyone else what it is. If they care, they'll ask. If they don't, I'll spare them the advertisement.</p>
</li>
<li><p>I have no intention of ever selling them.</p>
</li>
</ol>
<p>I used some matte black enamel paint and a 2mm round brush. I got the best results by picking up <strong>very</strong> light amounts of paint and dabbing it on. Any attempt to stroke the brush or speed up the process resulted in a horrible texture. I did a few coats on each lens. If you are a chemist accustomed to handling individual molecules of substance at a time, you might have more patience for this. It doesn't look factory-perfect, but I am satisfied with the look and texture I got in the end.</p>
<p><a href="https://files.voussoir.net/writing/blacked_out_lens_markings/27_before.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/blacked_out_lens_markings/small_27_before.jpg"/></a></p>
<p><a href="https://files.voussoir.net/writing/blacked_out_lens_markings/27_after.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/blacked_out_lens_markings/small_27_after.jpg"/></a></p>
<p><a href="https://files.voussoir.net/writing/blacked_out_lens_markings/1855_before.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/blacked_out_lens_markings/small_1855_before.jpg"/></a></p>
<p><a href="https://files.voussoir.net/writing/blacked_out_lens_markings/1855_after.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/blacked_out_lens_markings/small_1855_after.jpg"/></a></p>
<p><a href="https://files.voussoir.net/writing/blacked_out_lens_markings/55200_before.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/blacked_out_lens_markings/small_55200_before.jpg"/></a></p>
<p><a href="https://files.voussoir.net/writing/blacked_out_lens_markings/55200_after.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/blacked_out_lens_markings/small_55200_after.jpg"/></a></p>
<p><a href="https://files.voussoir.net/writing/blacked_out_lens_markings/glamour.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/blacked_out_lens_markings/small_glamour.jpg"/></a></p>

<hr/>
<p><a href="https://git.voussoir.net/voussoir/voussoir.net/commits/master/voussoir.net/writing/blacked_out_lens_markings/blacked_out_lens_markings.md">View this document's history</a></p>
<ul>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/25fcaa458034edaf1fcf9a918b655bcf7fb966ee"><time datetime="2023-09-25T19:02:58-07:00">2023-09-25</time> Add blacked_out_lens_markings.md.</a></li>
</ul>
<address>Contact me: writing@voussoir.net</address>
<p>If you would like to subscribe for more, add this to your RSS reader: <a href="/writing/writing.atom" rel="alternate" type="application/atom+xml">https://voussoir.net/writing/writing.atom</a></p>
</article>
            
            


guid: ee2ed2937b48df3a019b23481b838cacd5a5a935/auto_off_power_banks
title: Auto-off power banks
url: https://voussoir.net/writing/auto_off_power_banks
feedurl: https://voussoir.net/writing/writing.atom
content_mime_type: text/html
content:
            
            <article>
<p><a href="/writing">Back to writing</a></p>

<p><a class="tag_link" data-qualname="electronics" href="/writing/tags/electronics">[electronics]</a></p>
<h1>Auto-off power banks</h1>
<p>The essential parts of a USB power bank are:</p>
<ol>
<li>A battery.</li>
<li>Charge controller to receive USB input and safely charge the battery.</li>
<li>Protection circuit to prevent the battery from being excessively discharged.</li>
<li>Voltage regulator to convert the battery's voltage to 5 V for USB output.</li>
</ol>
<p>Artist's rendition:</p>
<p><img alt="Photograph of a breadboard containing a TP4056 charge controller, a 5 V USB regulator, and an 18650 battery cell. A cable connects this breadboard to a cell phone which is being charged." loading="lazy" src="/writing/auto_off_power_banks/breadboard1.jpg"/></p>
<p>But the majority of power banks on the market go the extra mile by adding a microcontroller that detects when the output current is low, and turns off the power bank.</p>
<p>The expected use case for these products is, first and foremost, to charge cell phones. I would love to be more broad and inclusive with that, but, no, it's mainly about cell phones. The phone will draw a high current while it is charging, and a low current when it is full, so the power bank detects that and shuts off.</p>
<p>This drives me crazy! These auto-off power banks make it impossible to:</p>
<ul>
<li>run low-current loads like gentle ambient lights.</li>
<li>run arduino or other microcontroller projects that draw less than the minimum current.</li>
<li>deliberately keep the phone out of <a href="https://git.voussoir.net/voussoir/trkpt#doze">doze mode</a>.</li>
<li>use an inline switch, or a device with its own power switch.</li>
</ul>
<p>A couple of quick searches will find you plenty of electronics hobbyists and DIYers who'd rather their power banks not turn off:</p>
<ul>
<li><a href="https://old.reddit.com/r/usbchardware">/r/usbchardware</a>: <a href="/writing/auto_off_power_banks/usbchardware_powerbank.html">USB powerbanks without "trickle" switching</a></li>
<li>arduino.cc: <a href="/writing/auto_off_power_banks/arduino_powerbank.html">USB battery bank without low current shutoff</a></li>
<li>candlepowerforums: <a href="/writing/auto_off_power_banks/candlepowerforums_powerbank.html">USB powerbank WITHOUT auto off?</a></li>
<li>electronics.stackexchange: <a href="/writing/auto_off_power_banks/stackexchange_powerbank.html">A proper way to stop power banks to turn off, and how to power low-current devices through USB</a></li>
<li>mtbr: <a href="/writing/auto_off_power_banks/mtbr_powerbank.html">Power Bank with no "on button"</a></li>
<li>pistonheads: <a href="/writing/auto_off_power_banks/pistonheads_powerbank.html">'Always On' Powerbank without auto standby</a></li>
<li><a href="https://old.reddit.com/r/homeautomation">/r/homeautomation</a>: <a href="/writing/auto_off_power_banks/homeautomation_powerbank.html">Always on powerbank?</a></li>
<li>hackernews: <a href="/writing/auto_off_power_banks/hackernews_powerbank.html">There's no reason anymore that a power bank should ever even have an off mode</a></li>
</ul>
<p>When I look at power banks for sale, the majority that I see have a single-button user interface, where pressing the button wakes up the power bank and displays the remaining charge level. Some brands, but not all, offer a low current mode that disables the auto-off feature, accessed by double-pressing the button, or holding it for a few seconds, etc. The joys of single-button UIs. This only takes effect for a single discharge session, and often comes with the caveat that it will <strong>still</strong> turn itself off after a <a href="/writing/auto_off_power_banks/xiaomi_manual.png">few hours</a>.</p>
<p>For power banks that don't offer a low current mode, the most commonly recommended solution is to put a resistor across the power bank's output. At 5 V, a 50 Ohm resistor will dissipate 100 mA, for example. That's right, the solution to your low-power project is simply to draw more power! Wow! Some more ambitious problem solvers will put the resistor on a timer so it draws a burst of current just often enough to keep the battery awake and minimize waste. But this still adds a clunky, intermediate dongle to your project.</p>
<p>Some of the very cheap power banks don't turn themselves off at all, and are valuable to the hobbyists. But they usually come from anonymous brands of no repute, which makes it impossible to provide a specific product recommendation because the brand won't exist in six months, and barely exists today. They typically don't even consider "always on" to be one of their selling points, so they don't mention it, which makes it nearly impossible to search a marketplace by keyword.</p>
<p>What I mean is, there are plenty of auto-off power banks that don't mention they are auto-off because it's considered standard behavior; and there are always-on power banks that don't mention they are always-on because it's not considered to be a selling point. The absence of either keyword practically means you have to assume it is auto-off.</p>
<p>One of the very, very few brands that prominently advertises their always-on power banks is <a href="/writing/auto_off_power_banks/voltaic.html">Voltaic</a>, and they charge a hefty price premium for what is, from this perspective, feature subtraction. The high price is accounted for by other perspectives: they're designed to charge from solar without browning out, they explicitly support charging while discharging, and they're aiming towards business clients purchasing in high volume. But that's not what I was looking for.</p>
<p>If you want an always-on battery, the most likely candidates are the $5 unit from a no-name whitelabeler that was too cheap to include the auto-off microcontroller, or the $50+ unit from the premium B2B IOT market, it seems. There are precious few in the middle, but they do exist, sometimes. I bought <a href="/writing/auto_off_power_banks/aliexpress_listing.png">this unit</a> via aliexpress for $19 and now that seller has gone bust, probably because they were falsely using Xiaomi's name or committing other scams. I was desperate.</p>
<p>So far, I haven't even talked about why power banks do the auto-off thing. I don't really know. It's probably because all the manufacturers are putting together building blocks from a small handful of suppliers and someone came up with it and now they're all the same.</p>
<p>Ostensibly it's to save power, and that's the answer people give. Are they referring to the output voltage regulator, drawing some current even when not in use? That's a good point, I wonder what else we can do to disconnect loads when they are not in use.</p>
<p>Artist's rendition:</p>
<p><img alt="Closeup photo of an on-off rocker switch" loading="lazy" src="/writing/auto_off_power_banks/switch.jpg"/></p>
<p><img alt="Photo of the same breadboard from before, this time with the rocker switch inserted in the circuit before the 5 V output regulator." loading="lazy" src="/writing/auto_off_power_banks/breadboard2.jpg"/></p>
<p>I don't get it. Why do I see hundreds of power banks with the one-button interface, and none with a plain old on-off switch? Is that too 20th century? Even if you leave it switched on accidentally, it will still take weeks or months to self-discharge from the quiescent current. I know this because the always-on batteries I have sometimes go months without use. It's not the same as leaving a flashlight burning in your backpack.</p>
<p>The only other way I can think auto-off saves power is that the device being charged — remember, as a product designer you should anticipate it could be anything as long as it's a cell phone — will perform more background activity while it's plugged in. As I mentioned, Android will not enter Doze mode while it detects the charger, and I'd expect iOS to also use charging as an opportunity to do more syncing or network activity or whatever. So, specifically, I'm referring to a device that has reached full capacity and the user has not unplugged it from the power bank yet, so it's using a few extra milliwatts. And that's apparently worth discarding all other low-power use cases.</p>
<p>Some people will guess that the auto-off feature is to protect the power bank's own battery from overdischarge, but this is wrong. Low-voltage cutoff is done by the protection circuit which, ideally, is provided by a dedicated chip immediately in series with the battery. Auto-off also cannot be to protect the phone's battery, because the power bank cannot force the phone to take more power than it wants. Or else, mains-powered chargers would be popping everyone's phones.</p>
<p>A rant about batteries, of all things. I'm sure there are bigger problems out there. But this is simply one example of an annoying and pervasive trend in tech: solved problems becoming unsolved over time due to reduction in user control. Stuff used to be made with buttons and switches so I can decide what it does. Now it is made "smart" so it can decide what it does. A slight change in the product's design can improve the experience of one group of users with no downsides for the other. I am very tired of:</p>
<ul>
<li><p>devices and appliances calling themselves smart devices, where the word "smart" means it does something you didn't ask for because the designers couldn't imagine more than one use case or mode of operation. I want you to do less!</p>
</li>
<li><p>devices that are non-configurable, non-customizable, and non-openable. I would be totally fine with a DIP switch on the inside of the unit, or soldering a jumper, if only you wouldn't use one-way plastic snaps in its construction!</p>
</li>
<li><p>single-button user interfaces, their multi-click and hold incantations, and internal state machines taking the place of simple physical toggle switches.</p>
</li>
<li><p>marketplaces flooded with thousands of whitelabeled copies of the same products, swamping out the few that are actually different.</p>
</li>
<li><p>feeling dismayed by our failure to do easy things.</p>
</li>
</ul>

<hr/>
<p><a href="https://git.voussoir.net/voussoir/voussoir.net/commits/master/voussoir.net/writing/auto_off_power_banks/auto_off_power_banks.md">View this document's history</a></p>
<ul>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/ee2ed2937b48df3a019b23481b838cacd5a5a935"><time datetime="2023-10-08T10:53:11-07:00">2023-10-08</time> Add auto_off_power_banks.md.</a></li>
</ul>
<address>Contact me: writing@voussoir.net</address>
<p>If you would like to subscribe for more, add this to your RSS reader: <a href="/writing/writing.atom" rel="alternate" type="application/atom+xml">https://voussoir.net/writing/writing.atom</a></p>
</article>
            
            


guid: 2ec2455a986c5d725288e69eab90babc9011e3c4/mediasonic_fan_replacement
title: Mediasonic Probox fan replacement
url: https://voussoir.net/writing/mediasonic_fan_replacement
feedurl: https://voussoir.net/writing/writing.atom
content_mime_type: text/html
content:
            
            <article>
<p><a href="/writing">Back to writing</a></p>

<p><a class="tag_link" data-qualname="electronics" href="/writing/tags/electronics">[electronics]</a> <a class="tag_link" data-qualname="today_i_did_this" href="/writing/tags/today_i_did_this">[today_i_did_this]</a></p>
<h1>Mediasonic Probox fan replacement</h1>
<p>I have a Mediasonic Probox for file storage. After 3.5 years of use (purchased May 2020), one of the fans has gone bad:</p>
<audio controls="" src="/writing/mediasonic_fan_replacement/before.flac"></audio><p>The stock fans are labeled YLTC DFS802012H, 12 Volts, 0.23 Amps, and measure 80x80x20mm.</p>
<p>For my replacement, I chose the Arctic P8 because it seems to have a good reputation for being quiet, and I feel that a pressure-optimized fan (the P series) is more appropriate than a flow-optimized fan (the F series) for pushing air around the box's motherboard and between the drives.</p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/old_fan_new_fan.jpg"/></p>
<p>Before doing some research, it hadn't occurred to me that the thicknesses of 80mm case fans wouldn't all be the same. On this <a href="/writing/mediasonic_fan_replacement/mediasonic_forum_4054.html">Mediasonic forum post</a> about fan replacement, someone commented that most fans on the market are 25mm thick and would not be a drop-in replacement for the 20mm stock. The screws are not long enough to go all the way through a 25mm fan, and the shroud, and have any real bite left over.</p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/fan_thickness.jpg"/></p>
<p>Besides the problem of length, there is also diameter: without the shroud, the screws slip straight through the mounting holes in the fan itself, so the shroud is necessary... or so I thought. Then I found <a href="https://www.thingiverse.com/thing:4965892" title="Mediasonic Probox fan stanchion by fl0rm">this thingiverse thing</a> where the author takes advantage of the empty space between the two faces of the fan housing to slip in a little hollow post. This bypasses the extra 5mm of fan thickness by placing the screw head within the Z-profile of the fan, and gives it something to grab onto, solving both problems at once! Very clever!</p>
<div class="highlight openscad"><pre><span class="o">$fa</span> <span class="o">=</span> <span class="m">0.1</span><span class="p">;</span>
<span class="o">$fs</span> <span class="o">=</span> <span class="m">0.1</span><span class="p">;</span>

<span class="nb">difference</span><span class="p">(</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nb">cylinder</span><span class="p">(</span><span class="n">d</span><span class="o">=</span><span class="m">5.5</span><span class="p">,</span> <span class="n">h</span><span class="o">=</span><span class="m">10</span><span class="p">)</span><span class="p">;</span>
    <span class="nb">cylinder</span><span class="p">(</span><span class="n">d</span><span class="o">=</span><span class="m">2.9</span><span class="p">,</span> <span class="n">h</span><span class="o">=</span><span class="m">10</span><span class="p">)</span><span class="p">;</span>
<span class="p">}</span>
</pre></div><p>Please excuse the poor quality of the prints...</p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/standoffs.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/standoff_length.jpg"/></p>
<p>Also, most fans use a three-pin connector with one wire carrying RPM signal data, but the Mediasonic only uses the two power pins and a 2mm connector. There are <a href="https://www.aliexpress.us/item/2251832624753638.html" title="ARSYLID Conversion cable 3 pin to 2 pin 2.0mm adapter fan cable 12V cooler fan for VGA cooling fan 2pin micro-2pin">adapters</a> out there, mentioned in the forum post, so you can make the swap non-destructively. I was not in the mood to wait multiple weeks for aliexpress shipping, so I cut and soldered the ends.</p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/wires_cut.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/wires_soldered.jpg"/></p>
<p>Of course, you could just buy a fan that's 20mm thick with a two-pin connector in the first place. But all the options I saw were very cheap/generic looking, and the per-fan prices were not cheaper than the Arctics, and I've been wanting to replace the stock fans with quieter ones for a long time anyway. In this case, I'd rather buy a fan from a known brand with a good reputation for quiet, even if it requires some modification.</p>
<p>You have to admit this doesn't really seem like the ideal air path:</p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/guts.jpg"/></p>
<p>But the replacement went just fine and the fan feels very secure.</p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/install_1.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/install_2.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/install_3.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/install_4.jpg"/></p>
<p>I felt I should add a bit of tape around the edge to discourage air from leaking into the sides, since the shroud is gone now.</p>
<p><img alt="" loading="lazy" src="/writing/mediasonic_fan_replacement/tape_seal.jpg"/></p>
<p>After this, I also replaced the other fan in the same manner.</p>
<p>At low speed the new fans are essentially inaudible:</p>
<audio controls="" src="/writing/mediasonic_fan_replacement/after_low.flac"></audio><p>At full speed, they're still pretty quiet:</p>
<audio controls="" src="/writing/mediasonic_fan_replacement/after_high.flac"></audio><p>These recordings were made with no hard drives in the box. Once the drives are in... well... it's quieter than it was before but it's not exactly life-changing.</p>
<p>For the first few days, I heard a new sound that made me think the whole thing was pointless — it was a kind of ringing, or resonance, or beat frequency that was audible when I sat in my chair, but seemed to disappear when I got close to the box to listen for it. I'm not sure if it was an illusion, or the <a href="https://en.wikipedia.org/wiki/The_Tell-Tale_Heart">heart</a> of my latest victim, or the bearings just breaking themselves in, but it's better now.</p>
<p>I don't normally publish this kind of thing, since I feel it's a little trivial or unimpressive, and because it mentions <a href="/writing/advertixing#HailCorporate">brand names</a>. But whenever I am solving a problem, I rely on posts made by other people solving the same problem — something as simple as a hollow cylinder turned out to be a very helpful bit of inspiration to me. If that person thought a cylinder was too trivial to share, it would have taken me longer to do this. So maybe I should contribute back in the same way more often.</p>

<hr/>
<p><a href="https://git.voussoir.net/voussoir/voussoir.net/commits/master/voussoir.net/writing/mediasonic_fan_replacement/mediasonic_fan_replacement.md">View this document's history</a></p>
<ul>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/2ec2455a986c5d725288e69eab90babc9011e3c4"><time datetime="2023-12-17T12:50:46-08:00">2023-12-17</time> Add mediasonic_fan_replacement.md.</a></li>
</ul>
<address>Contact me: writing@voussoir.net</address>
<p>If you would like to subscribe for more, add this to your RSS reader: <a href="/writing/writing.atom" rel="alternate" type="application/atom+xml">https://voussoir.net/writing/writing.atom</a></p>
</article>
            
            


guid: cf815afed1f59f56847a6677d5c53b493cc6dad2/css_for_printing
title: CSS for printing to paper
url: https://voussoir.net/writing/css_for_printing
feedurl: https://voussoir.net/writing/writing.atom
content_mime_type: text/html
content:
            
            <article>
<p><a href="/writing">Back to writing</a></p>

<h1>CSS for printing to paper</h1><ol id="table_of_contents">Table of contents<li><a href="#introduction">Introduction</a></li><li><a href="#sample_files">Sample files</a></li><li><a href="#page">@page</a></li><li><a href="#media_print">@media print</a></li><li><a href="#width_height_margin_and_padding">Width, height, margin, and padding</a></li><li><a href="#element_positioning">Element positioning</a></li><li><a href="#multipage_documents_with_repeating_elements">Multi-page documents with repeating elements</a></li><li><a href="#portrait_landscape_mode">Portrait / Landscape mode</a></li><li><a href="#data_source">Data source</a></li><li><a href="#essentials_cheatsheet">Essentials cheatsheet</a></li></ol>
<h2 id="introduction">Introduction<a class="header_anchor_link" href="#introduction"> (§)</a></h2>
<p>At work, one of the things I do pretty often is write print generators in HTML to recreate and replace forms that the company has traditionally done handwritten on paper or in Excel. This allows the company to move into new web-based tools where the form is autofilled by URL parameters from our database, while getting the same physical output everyone's familiar with.</p>
<p>This article explains some of the CSS basics that control how your webpages look when printed, and a couple of tips and tricks I've learned that might help you out.</p>
<h2 id="sample_files">Sample files<a class="header_anchor_link" href="#sample_files"> (§)</a></h2>
<p>Here are some sample page generators to establish some context, and perhaps a shred of credibility.</p>
<p>I'll be the first to admit these pages are a little bit ugly and could use more polish. But they get the job done and I'm still employed.</p>
<p><a href="/writing/css_for_printing/sample_invoice.html">Invoice generator</a></p>
<p><a href="/writing/css_for_printing/sample_sidebar.html
?jobno=BRAINIACS
&amp;commissioner=Minister%20McMinisterface
&amp;medium=film
&amp;execproducer=Golan%20%26%20Globus
&amp;color=B/W
&amp;director=Peter%20Jackson
&amp;length=80m
&amp;funding=80%%20benefactor;%2020%%20panhandling
&amp;budget=$1,234,567.00
&amp;jobcontactname=Taylor%20Shift
&amp;jobcontactphone=555-123-4567
&amp;jobcontactemail=production@voussoir.net
&amp;unioncontact=Richard%20Pryor
&amp;union=TAAGSRFA
&amp;synopsis=A%20group%20of%20ten%20quirky%20teenagers%20from%20opposite%20ends%20of%20town%20awaken%20one%20morning%20to%20find%20themselves%20in%20an%20unrecognizable%20world.%20All%20their%20friends%20and%20family%20are%20chasing%20them%20with%20sawed-off%20shotguns%20and%20flamethrowers.%20The%20group%20realizes%20they%20need%20to%20overcome%20their%20differences%20and%20work%20together%20if%20they%20want%20to%20have%20any%20chance%20of%20ending%20the%20human%20race%20and%20eating%20all%20the%20brains.%20Rated%20G.
&amp;principlephoto=2024-06-01
&amp;wrapdate=2024-06-03
&amp;talent=Molly%20Ringwald%20as%20Claire%0AAngus%20Scrimm%20as%20Tiny%20Tim%0ASteve%20McQueen%20as%20Chadster%0AKeith%20David%20as%20Frank%20Armitage%0Alocal%20pigeon%20as%20The%20Great%20Destroyer%0AMichael%20Beck%20as%20Swan%0ADavid%20Cronenberg%20as%20himself
&amp;payfreq=weekly
&amp;otrate=1.1x
&amp;dtrate=1.15x
&amp;payrollcontact=shredpile@voussoir.net
&amp;remarks=Please%20try%20to%20get%20Tom%20Cruise%20in%20the%20promotional%20material%20because%20we%20need%20something%20to%20draw%20crowds.
&amp;signedby=voussoir
&amp;signeddate=2024-02-29">Coversheet with sidebar inputs</a></p>
<p><a href="/writing/css_for_printing/sample_contenteditable.html
?missionnumber=ABC123
&amp;victimname=Minister%20McMinisterface
&amp;dob=1970-01-01
&amp;occupation=Prime%20Minister%20of%20Governmentown
&amp;maritalstatus=Divorced%20once%20removed
&amp;language=en_CA-eh
&amp;children=9
&amp;issuedate=2024-02-27
&amp;duedate=2024-07-04
&amp;customerrequest=I%20want%20McMinisterface%20to%20experience%20a%20pranking%20the%20likes%20of%20which%20he%27s%20never%20seen%20and%20will%20never%20see%20again
&amp;catalogno=GOT-UR-NOSE
&amp;catalogvar=yogababe
&amp;catalogsubvar=downwarddog
&amp;specifics=At%20precisely%200604%20hours,%20as%20McMinisterface%20is%20heading%20out%20the%20front%20door%20of%20his%20flat%20carrying%20a%20breakfast%20scone%20and%20that%20stupid%20yellow%20coffee%20mug,%20the%20Operative%20shall%20place%20himself%20in%20front%20of%20the%20door%20of%20the%20flat,%20assuming%20the%20Downward%20Dog%20position,%20rendering%20him%20undetectable%20to%20all%20passersby.%20McMinisterface%20will%20unexpectedly%20trip%20over%20the%20firmly%20planted%20body%20of%20the%20Operative,%20landing%20on%20the%20concrete,%20breaking%20the%20cartilage%20in%20his%20nose%20and%20dropping%20that%20stupid%20yellow%20coffee%20mug.%20The%20Operative%20will%20approach%20the%20fallen%20McMinisterface%20and%20offer%20to%20help%20him%20up,%20but%20at%20the%20last%20minute%20say%20YOINK,%20GOT%20YOUR%20NOSE%20and%20run%20towards%20the%20bus%20station%20at%20the%20corner%20of%20First%20and%201st%20to%20catch%20the%200606%20bus%20for%20a%20timely%20escape.
&amp;callsick=1
&amp;leavephone=1
&amp;scheduleddelivery=Anti-itch%20ointment,%20to%20be%20left%20on%20porch%20because%20too%20embarrassed%20to%20make%20knowing%20eye%20contact%20with%20delivery%20person
&amp;leaveotherphone=1
&amp;leaveotherotherphone=0
&amp;soundeffects=snoring;%20groaning;%20leavemealone%20120min%20loop
&amp;jointeffort=0
&amp;jointopname=NA
&amp;jointlocation=NA
&amp;jointtime=NA
&amp;leavehome=0430
&amp;leavecafe=0545
&amp;arrivesite=0602
&amp;getaway=0606
&amp;laylow=72hr%20minimum">Coversheet with contenteditable</a></p>
<p><a href="/writing/css_for_printing/sample_qr.html">QR code generator</a></p>
<h2 id="page">@page<a class="header_anchor_link" href="#page"> (§)</a></h2>
<p>CSS has a rule called <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@page"><code>@page</code></a> that informs the browser of your website's printing preferences. Normally, I use</p>
<div class="highlight css"><pre><span class="p">@</span><span class="k">page</span>
<span class="p">{</span>
    <span class="nt">size</span><span class="o">:</span> <span class="nt">Letter</span> <span class="nt">portrait</span><span class="n">;</span>
    <span class="nt">margin</span><span class="o">:</span> <span class="nt">0</span><span class="n">;</span>
<span class="p">}</span>
</pre></div><p>I will explain why I choose <code>margin: 0</code> in the later section about margins. You should use Letter or A4 as appropriate for your relationship with the metric system.</p>
<p>Setting the size and margin of @page is not the same as setting the width, height, and margin of your <code>&lt;html&gt;</code> or <code>&lt;body&gt;</code> element. @page is beyond the DOM — it contains the DOM. On the web, your <code>&lt;html&gt;</code> element is bounded by the edges of your screen, but when printing it is bounded by @page.</p>
<p>The settings controlled by @page more or less correspond to the settings you get in your browser's print dialog when you press Ctrl+P.</p>
<p>Here's a sample file I used to do some experiments:</p>
<div class="highlight html"><pre><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span>
<span class="p">@</span><span class="k">page</span>
<span class="p">{</span>
    <span class="c">/* see below for each experiment */</span>
<span class="p">}</span>
<span class="nt">html</span>
<span class="p">{</span>
    <span class="k">width</span><span class="p">:</span> <span class="mi">100</span><span class="kt">%</span><span class="p">;</span>
    <span class="k">height</span><span class="p">:</span> <span class="mi">100</span><span class="kt">%</span><span class="p">;</span>
    <span class="k">background-color</span><span class="p">:</span> <span class="kc">lightblue</span><span class="p">;</span>

    <span class="c">/* grid by shunryu111 https://stackoverflow.com/a/32861765/5430534 */</span>
    <span class="k">background-size</span><span class="p">:</span> <span class="mf">0.25</span><span class="kt">in</span> <span class="mf">0.25</span><span class="kt">in</span><span class="p">;</span>
    <span class="k">background-image</span><span class="p">:</span>
    <span class="nb">linear-gradient</span><span class="p">(</span><span class="kc">to</span> <span class="kc">right</span><span class="p">,</span> <span class="kc">gray</span> <span class="mi">1</span><span class="kt">px</span><span class="p">,</span> <span class="kc">transparent</span> <span class="mi">1</span><span class="kt">px</span><span class="p">)</span><span class="p">,</span>
    <span class="nb">linear-gradient</span><span class="p">(</span><span class="kc">to</span> <span class="kc">bottom</span><span class="p">,</span> <span class="kc">gray</span> <span class="mi">1</span><span class="kt">px</span><span class="p">,</span> <span class="kc">transparent</span> <span class="mi">1</span><span class="kt">px</span><span class="p">)</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">&lt;</span><span class="p">/</span><span class="nt">style</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span><span class="">Sample text</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">h1</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span><span class="">sample text</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">p</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="p">/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="p">/</span><span class="nt">html</span><span class="p">&gt;</span>
</pre></div><p>Here's how that looks in the browser:</p>
<p><img alt="" loading="lazy" src="/writing/css_for_printing/sample_in_browser.png" title="but i don't want new chrome"/></p>
<p>And here are the results of some different @page values:</p>
<p><code>@page { size: Letter portrait; margin: 1in; }</code>:</p>
<p><img alt="" loading="lazy" src="/writing/css_for_printing/letter_portrait_1in.png"/></p>
<p><code>@page { size: Letter landscape; margin: 1in; }</code>:</p>
<p><img alt="" loading="lazy" src="/writing/css_for_printing/letter_landscape_in1.png"/></p>
<p><code>@page { size: Letter landscape; margin: 0; }</code>:</p>
<p><img alt="" loading="lazy" src="/writing/css_for_printing/letter_landscape_0.png"/></p>
<p>Setting the @page size won't actually put that size of paper into your printer's feed tray. <a href="https://youtu.be/3z9YjGWaxC0" title="Office Space - PC Load Letter?">You'll have to do that part yourself</a>.</p>
<p>Notice how when I set <code>size</code> to A5, my printer stays on Letter, and the A5 size fits entirely within the Letter size which gives the appearance of a margin even though it's not coming from the <code>margin</code> setting.</p>
<p><code>@page { size: A5 portrait; margin: 0; }</code>:</p>
<p><img alt="" loading="lazy" src="/writing/css_for_printing/a5_portrait_0.png"/></p>
<p>But if I tell the printer that I have actual A5 paper loaded, then it looks as expected.</p>
<p><img alt="" loading="lazy" src="/writing/css_for_printing/a5_portrait_0_a5paper.png"/></p>
<p>From what I gather by experimentation, Chrome only follows the @page rule if you have Margin set to Default. As soon as you change Margin in the print dialog, your output is instead the product of your physical paper size and the chosen margin.</p>
<p><code>@page { size: A5 portrait; margin: 0; }</code>:</p>
<p><img alt="" loading="lazy" src="/writing/css_for_printing/a5_portrait_0_default.png"/></p>
<p><img alt="" loading="lazy" src="/writing/css_for_printing/a5_portrait_0_none.png"/></p>
<p>Even when you choose a @page size that fits fully within your physical paper, the <code>margin</code> still matters. Here, I make a 5x5 square with no margin, and a 5x5 square with margin. The size of the <code>&lt;html&gt;</code> element is bounded by the @page size <strong>and</strong> margin combined.</p>
<p><code>@page { size: 5in 5in; margin: 0; }</code>:</p>
<p><img alt="" loading="lazy" src="/writing/css_for_printing/5in_5in_0.png"/></p>
<p><code>@page { size: 5in 5in; margin: 1in; }</code>:</p>
<p><img alt="" loading="lazy" src="/writing/css_for_printing/5in_5in_1in.png"/></p>
<p>I did all these tests not because I expect to print on A5 or 5x5 paper, but because it took me a while to figure out what exactly @page is. Now I am pretty confident in always using Letter with margin 0.</p>
<h2 id="media_print">@media print<a class="header_anchor_link" href="#media_print"> (§)</a></h2>
<p>There is a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media">media query</a> called <code>print</code> where you can write styles that only apply during printing. My generator pages often contain a header, some options, and some help text for the user that obviously shouldn't come out on the print, so this is where you add <code>display:none</code> on those elements.</p>
<div class="highlight css"><pre><span class="c">/* Normal styles that appear while you are preparing the document */</span>
<span class="nt">header</span>
<span class="p">{</span>
    <span class="k">display</span><span class="p">:</span> <span class="kc">block</span><span class="p">;</span>
<span class="p">}</span>

<span class="p">@</span><span class="k">media</span> <span class="nt">print</span>
<span class="p">{</span>
    <span class="c">/* Disappear when you are printing the document */</span>
    <span class="nt">header</span>
    <span class="p">{</span>
        <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></div><p><img alt="" loading="lazy" src="/writing/css_for_printing/mediaprint_1.png"/></p>
<p><img alt="" loading="lazy" src="/writing/css_for_printing/mediaprint_2.png"/></p>
<h2 id="width_height_margin_and_padding">Width, height, margin, and padding<a class="header_anchor_link" href="#width_height_margin_and_padding"> (§)</a></h2>
<p>You'll need to know a bit about the <a href="https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/The_box_model">box model</a> to get the margins you want without wrestling the computer too much.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/The_box_model"><img alt="" loading="lazy" src="/writing/css_for_printing/box_model.png"/></a></p>
<p>The reason I always set @page <code>margin: 0</code> is that I'd rather handle the margins on the DOM elements instead. When I tried to use @page <code>margin: 0.5in</code>, I would often accidentally wind up with double-margins that squash the content smaller than I expected, and my one-page design spilled onto a second page.</p>
<p>If I wanted to use @page margin, then the actual page content would need to be laid out all the way up against the edges of the DOM, which is harder for me to think about and harder to preview before printing. It is mentally easier for me to remember that <code>&lt;html&gt;</code> occupies the entire physical paper and my margins are within the DOM instead of beyond it.</p>
<div class="highlight css"><pre><span class="p">@</span><span class="k">page</span>
<span class="p">{</span>
    <span class="nt">size</span><span class="o">:</span> <span class="nt">Letter</span> <span class="nt">portrait</span><span class="n">;</span>
    <span class="nt">margin</span><span class="o">:</span> <span class="nt">0</span><span class="n">;</span>
<span class="p">}</span>
<span class="nt">html</span><span class="n">,</span>
<span class="nt">body</span>
<span class="p">{</span>
    <span class="k">width</span><span class="p">:</span> <span class="mf">8.5</span><span class="kt">in</span><span class="p">;</span>
    <span class="k">height</span><span class="p">:</span> <span class="mi">11</span><span class="kt">in</span><span class="p">;</span>
<span class="p">}</span>
</pre></div><p>When it comes to multi-page print generators, you're going to want a separate DOM element representing each page. Since you can't have multiple <code>&lt;html&gt;</code> or <code>&lt;body&gt;</code>, you're going to need another element. I like <code>&lt;article&gt;</code>. Even for single-page generators, you may as well always use an article.</p>
<p>Since each <code>&lt;article&gt;</code> represents one page, I don't want any margins or padding on <code>&lt;html&gt;</code> or <code>&lt;body&gt;</code>. We're pushing the logic one step further — it is easier for me to let the article occupy the entire physical page and put my margins within it.</p>
<div class="highlight css"><pre><span class="p">@</span><span class="k">page</span>
<span class="p">{</span>
    <span class="nt">size</span><span class="o">:</span> <span class="nt">Letter</span> <span class="nt">portrait</span><span class="n">;</span>
    <span class="nt">margin</span><span class="o">:</span> <span class="nt">0</span><span class="n">;</span>
<span class="p">}</span>
<span class="nt">html</span><span class="n">,</span>
<span class="nt">body</span>
<span class="p">{</span>
    <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">article</span>
<span class="p">{</span>
    <span class="k">width</span><span class="p">:</span> <span class="mf">8.5</span><span class="kt">in</span><span class="p">;</span>
    <span class="k">height</span><span class="p">:</span> <span class="mi">11</span><span class="kt">in</span><span class="p">;</span>
<span class="p">}</span>
</pre></div><p>When I talk about adding margin within my article, I'm not using the <code>margin</code> property, I'm using <code>padding</code>. That's because <code>margin</code> goes outside and around your element in the box model. If you use a <code>margin</code> of 0.5in, you'll have to set the article to 7.5×10 so that the article plus 2×margin equals 8.5×11. And if you want to adjust that margin you'll have to adjust the other dimensions.</p>
<p>Instead, <code>padding</code> goes on the inside of the element, so I can define the article to be 8.5×11 with 0.5in padding, and all the elements inside the article will stay on the page.</p>
<p>A lot of intuition about element dimensions is easier when you set <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing"><code>box-sizing: border-box</code></a>. It makes it so that the outer dimensions of the article are locked in while you adjust the inner padding. This is my snippet:</p>
<div class="highlight css"><pre><span class="nt">html</span>
<span class="p">{</span>
    <span class="k">box-sizing</span><span class="p">:</span> <span class="kc">border-box</span><span class="p">;</span>
<span class="p">}</span>
<span class="o">*</span><span class="n">,</span> <span class="o">*</span><span class="p">:</span><span class="nd">before</span><span class="nd">,</span> <span class="o">*</span><span class="p">:</span><span class="nd">after</span>
<span class="p">{</span>
    <span class="k">box-sizing</span><span class="p">:</span> <span class="kc">inherit</span><span class="p">;</span>
<span class="p">}</span>
</pre></div><p>Let's put this all together:</p>
<div class="highlight css"><pre><span class="p">@</span><span class="k">page</span>
<span class="p">{</span>
    <span class="nt">size</span><span class="o">:</span> <span class="nt">Letter</span> <span class="nt">portrait</span><span class="n">;</span>
    <span class="nt">margin</span><span class="o">:</span> <span class="nt">0</span><span class="n">;</span>
<span class="p">}</span>

<span class="nt">html</span>
<span class="p">{</span>
    <span class="k">box-sizing</span><span class="p">:</span> <span class="kc">border-box</span><span class="p">;</span>
<span class="p">}</span>
<span class="o">*</span><span class="n">,</span> <span class="o">*</span><span class="p">:</span><span class="nd">before</span><span class="nd">,</span> <span class="o">*</span><span class="p">:</span><span class="nd">after</span>
<span class="p">{</span>
    <span class="k">box-sizing</span><span class="p">:</span> <span class="kc">inherit</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">html</span><span class="n">,</span>
<span class="nt">body</span>
<span class="p">{</span>
    <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">article</span>
<span class="p">{</span>
    <span class="k">width</span><span class="p">:</span> <span class="mf">8.5</span><span class="kt">in</span><span class="p">;</span>
    <span class="k">height</span><span class="p">:</span> <span class="mi">11</span><span class="kt">in</span><span class="p">;</span>
    <span class="k">padding</span><span class="p">:</span> <span class="mf">0.5</span><span class="kt">in</span><span class="p">;</span>
<span class="p">}</span>
</pre></div><p><img alt="" loading="lazy" src="/writing/css_for_printing/margins_padding.png"/></p>
<h2 id="element_positioning">Element positioning<a class="header_anchor_link" href="#element_positioning"> (§)</a></h2>
<p>Once you've got your articles and margins set up, the space inside the article is yours to do with as you please. Design your document using whatever HTML/CSS you feel is appropriate for the project. Sometimes this means laying out elements with flex or grid because you've been given some leeway with the output. Sometimes it means creating squares of a specific size to fit on a certain brand of sticker paper. Sometimes it means absolutely positioning absolutely everything to the millimeter because the user needs to feed a special piece of pre-labeled paper through the printer to get your data on top of it, and you're not in control of that special paper.</p>
<p>I'm not here to give a tutorial on how to write HTML in general, so you'll need to be able to do that. All I can say is be mindful of that fact that you're dealing with the limited real estate of a piece of paper, unlike a browser window which can scroll and zoom to any length or scale. If your document will contain an arbitrary number of items, be ready to paginate by creating more <code>&lt;article&gt;</code>.</p>
<h2 id="multipage_documents_with_repeating_elements">Multi-page documents with repeating elements<a class="header_anchor_link" href="#multipage_documents_with_repeating_elements"> (§)</a></h2>
<p>A lot of the print generators I write contain tabular data, like an invoice full of line items. If your <code>&lt;table&gt;</code> is large enough to go onto a second page, the browser will automatically duplicate the <code>&lt;thead&gt;</code> at the top of each page.</p>
<div class="highlight html"><pre><span class="p">&lt;</span><span class="nt">table</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">thead</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">th</span><span class="p">&gt;</span><span class="">Sample text</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">th</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">th</span><span class="p">&gt;</span><span class="">Sample text</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">th</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="p">/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="p">/</span><span class="nt">thead</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tbody</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span><span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span><span class="">0</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">td</span><span class="p">&gt;</span><span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span><span class="">0</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">td</span><span class="p">&gt;</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">tr</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span><span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span><span class="">1</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">td</span><span class="p">&gt;</span><span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span><span class="">1</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">td</span><span class="p">&gt;</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">tr</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span><span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span><span class="">2</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">td</span><span class="p">&gt;</span><span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span><span class="">4</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">td</span><span class="p">&gt;</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">tr</span><span class="p">&gt;</span><span class="">
        ...
    </span><span class="p">&lt;</span><span class="p">/</span><span class="nt">tbody</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="p">/</span><span class="nt">table</span><span class="p">&gt;</span>
</pre></div><p><img alt="" loading="lazy" src="/writing/css_for_printing/multipage_table_1.png"/></p>
<p>That's great if you're just printing a <code>&lt;table&gt;</code> with no frills, but in a lot of real scenarios it's not that simple. The document I'm recreating often has a letterhead on the top of each page, a footer on the bottom, and other custom elements that need to be explicitly repeated on each page. If you just print a single long table across pages, you don't have much ability to place other elements above, below, and around it on intermediate pages.</p>
<p>So, I generate the pages using javascript, splitting the table into several smaller ones. The general approach here is this:</p>
<ol>
<li>Treat the <code>&lt;article&gt;</code> elements as disposable and be ready to regenerate them at any time from objects in memory. All user input and configuration should take place in a separate header / options box, outside of the articles.</li>
<li>Write a function called <code>new_page</code> that creates a new article element with the necessary repeating header/footer/etc.</li>
<li>Write a function called <code>render_pages</code> that creates the articles from the base data, calling <code>new_page</code> every time it fills up the previous one. I usually use <code>offsetTop</code> to see when the content is getting far along the page, though you could <strong>definitely</strong> use smarter techniques to get the perfect fit on each page.</li>
<li>Call <code>render_pages</code> whenever the base data changes.</li>
</ol>
<div class="highlight javascript"><pre><span class="kd">function</span> <span class="nx">delete_articles</span><span class="p">(</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">article</span> <span class="k">of</span> <span class="nb">Array</span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementsByTagName</span><span class="p">(</span><span class="s2">"article"</span><span class="p">)</span><span class="p">)</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">removeChild</span><span class="p">(</span><span class="nx">article</span><span class="p">)</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">new_page</span><span class="p">(</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kd">const</span> <span class="nx">article</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">"article"</span><span class="p">)</span><span class="p">;</span>
    <span class="nx">article</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="sb">`</span><span class="sb">
    &lt;header&gt;...&lt;/header&gt;
    &lt;table&gt;...&lt;/table&gt;
    &lt;footer&gt;...&lt;/footer&gt;
    </span><span class="sb">`</span><span class="p">;</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="nx">article</span><span class="p">)</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">article</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">render_pages</span><span class="p">(</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nx">delete_articles</span><span class="p">(</span><span class="p">)</span><span class="p">;</span>

    <span class="kd">let</span> <span class="nx">page</span> <span class="o">=</span> <span class="nx">new_page</span><span class="p">(</span><span class="p">)</span><span class="p">;</span>
    <span class="kd">let</span> <span class="nx">tbody</span> <span class="o">=</span> <span class="nx">page</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="s2">"table tbody"</span><span class="p">)</span><span class="p">;</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">line_item</span> <span class="k">of</span> <span class="nx">line_items</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// I usually pick this threshold by experimentation but you can probably</span>
        <span class="c1">// do something more rigorously correct.</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">tbody</span><span class="p">.</span><span class="nx">offsetTop</span> <span class="o">+</span> <span class="nx">tbody</span><span class="p">.</span><span class="nx">offsetParent</span><span class="p">.</span><span class="nx">offsetTop</span> <span class="o">&gt;</span> <span class="mf">900</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="nx">page</span> <span class="o">=</span> <span class="nx">new_page</span><span class="p">(</span><span class="p">)</span><span class="p">;</span>
            <span class="nx">tbody</span> <span class="o">=</span> <span class="nx">page</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="s2">"table tbody"</span><span class="p">)</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="kd">const</span> <span class="nx">tr</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">"tr"</span><span class="p">)</span><span class="p">;</span>
        <span class="nx">tbody</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="nx">tr</span><span class="p">)</span><span class="p">;</span>
        <span class="c1">// ...</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></div><p>It is usually good to include a "page X of Y" counter on your pages. Since the number of pages is not known until all pages are generated, I can't do this during the for loop. I call a function like this at the end:</p>
<div class="highlight javascript"><pre><span class="kd">function</span> <span class="nx">renumber_pages</span><span class="p">(</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kd">let</span> <span class="nx">pagenumber</span> <span class="o">=</span> <span class="mf">1</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">pages</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementsByTagName</span><span class="p">(</span><span class="s2">"article"</span><span class="p">)</span><span class="p">;</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">page</span> <span class="k">of</span> <span class="nx">pages</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nx">page</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s2">".pagenumber"</span><span class="p">)</span><span class="p">.</span><span class="nx">innerText</span> <span class="o">=</span> <span class="nx">pagenumber</span><span class="p">;</span>
        <span class="nx">page</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s2">".totalpages"</span><span class="p">)</span><span class="p">.</span><span class="nx">innerText</span> <span class="o">=</span> <span class="nx">pages</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
        <span class="nx">pagenumber</span> <span class="o">+=</span> <span class="mf">1</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></div><h2 id="portrait_landscape_mode">Portrait / Landscape mode<a class="header_anchor_link" href="#portrait_landscape_mode"> (§)</a></h2>
<p>I've shown that the @page rule helps inform the browser's default print settings, but the user can override it if they want to. If you set @page to portrait mode and the user overrides it to landscape mode, your layout and pagination might look wrong, especially if you are hardcoding any page thresholds.</p>
<p>You can accommodate them by creating separate <code>&lt;style&gt;</code> elements for portrait and landscape, and using javascript to switch between them. There might be a better way to do this, but at-rules like @page behave differently than normal CSS properties so I'm not sure. You should also save some variable that can help your <code>render_pages</code> function do the right thing.</p>
<p>You could also stop hardcoding thresholds, but then I'd have to follow my own advice.</p>
<div class="highlight html"><pre><span class="p">&lt;</span><span class="nt">select</span> <span class="na">onchange</span><span class="o">=</span><span class="s">"return page_orientation_onchange(event);"</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">option</span> <span class="na">selected</span><span class="p">&gt;</span><span class="">Portrait</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">option</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">option</span><span class="p">&gt;</span><span class="">Landscape</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">option</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="p">/</span><span class="nt">select</span><span class="p">&gt;</span>
</pre></div><div class="highlight html"><pre><span class="p">&lt;</span><span class="nt">style</span> <span class="na">id</span><span class="o">=</span><span class="s">"style_portrait"</span> <span class="na">media</span><span class="o">=</span><span class="s">"all"</span><span class="p">&gt;</span>
<span class="p">@</span><span class="k">page</span>
<span class="p">{</span>
    <span class="nt">size</span><span class="o">:</span> <span class="nt">Letter</span> <span class="nt">portrait</span><span class="n">;</span>
    <span class="nt">margin</span><span class="o">:</span> <span class="nt">0</span><span class="n">;</span>
<span class="p">}</span>
<span class="nt">article</span>
<span class="p">{</span>
    <span class="k">width</span><span class="p">:</span> <span class="mf">8.5</span><span class="kt">in</span><span class="p">;</span>
    <span class="k">height</span><span class="p">:</span> <span class="mi">11</span><span class="kt">in</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">&lt;</span><span class="p">/</span><span class="nt">style</span><span class="p">&gt;</span>

<span class="p">&lt;</span><span class="nt">style</span> <span class="na">id</span><span class="o">=</span><span class="s">"style_landscape"</span> <span class="na">media</span><span class="o">=</span><span class="s">"not all"</span><span class="p">&gt;</span>
<span class="p">@</span><span class="k">page</span>
<span class="p">{</span>
    <span class="nt">size</span><span class="o">:</span> <span class="nt">Letter</span> <span class="nt">landscape</span><span class="n">;</span>
    <span class="nt">margin</span><span class="o">:</span> <span class="nt">0</span><span class="n">;</span>
<span class="p">}</span>
<span class="nt">article</span>
<span class="p">{</span>
    <span class="k">width</span><span class="p">:</span> <span class="mi">11</span><span class="kt">in</span><span class="p">;</span>
    <span class="k">height</span><span class="p">:</span> <span class="mf">8.5</span><span class="kt">in</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">&lt;</span><span class="p">/</span><span class="nt">style</span><span class="p">&gt;</span>
</pre></div><div class="highlight javascript"><pre><span class="kd">let</span> <span class="nx">print_orientation</span> <span class="o">=</span> <span class="s2">"portrait"</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">page_orientation_onchange</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nx">print_orientation</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">toLocaleLowerCase</span><span class="p">(</span><span class="p">)</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">print_orientation</span> <span class="o">==</span> <span class="s2">"portrait"</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"style_portrait"</span><span class="p">)</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s2">"media"</span><span class="p">,</span> <span class="s2">"all"</span><span class="p">)</span><span class="p">;</span>
        <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"style_landscape"</span><span class="p">)</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s2">"media"</span><span class="p">,</span> <span class="s2">"not all"</span><span class="p">)</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">print_orientation</span> <span class="o">==</span> <span class="s2">"landscape"</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"style_landscape"</span><span class="p">)</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s2">"media"</span><span class="p">,</span> <span class="s2">"all"</span><span class="p">)</span><span class="p">;</span>
        <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"style_portrait"</span><span class="p">)</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s2">"media"</span><span class="p">,</span> <span class="s2">"not all"</span><span class="p">)</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="nx">render_printpages</span><span class="p">(</span><span class="p">)</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">render_printpages</span><span class="p">(</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">print_orientation</span> <span class="o">==</span> <span class="s2">"portrait"</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// ...</span>
    <span class="p">}</span>
    <span class="k">else</span>
    <span class="p">{</span>
        <span class="c1">// ...</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></div><h2 id="data_source">Data source<a class="header_anchor_link" href="#data_source"> (§)</a></h2>
<p>There are a couple of ways to get your data onto the page. Sometimes, I pack all of the data into the URL parameters, so the javascript just does <code>const url_params = new URLSearchParams(window.location.search);</code> and then a bunch of <code>url_params.get("title")</code>. This has some advantages:</p>
<ul>
<li>The page loads very fast.</li>
<li>It's easy to debug and experiment by changing the URL.</li>
<li>The generator works offline.</li>
</ul>
<p>This also has some disadvantages:</p>
<ul>
<li>The URLs become very long and unweildy, people cannot comfortably email them to each other. See sample links at the top of this article.</li>
<li>If the URL does get sent in an email, that data is "locked in", even if the source record in your database changes later.</li>
<li>Browsers do have limits on URL length. The limits are pretty high but not infinite and might vary per client.</li>
</ul>
<p>Sometimes I instead use javascript to fetch our database records over the API, so the URL parameters just contain the record's primary key and maybe a mode setting.</p>
<p>This has some advantages:</p>
<ul>
<li>The URLs are much shorter.</li>
<li>The data is always fresh.</li>
</ul>
<p>and disadvantages:</p>
<ul>
<li>The user has to wait a second while the data is being fetched.</li>
<li>You have to write more code.</li>
</ul>
<p>Sometimes I set <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable"><code>contenteditable</code></a> on the articles so the user can make small changes before printing. I also like to use real, live checkbox inputs they can click before printing. These features add some convenience, but in most cases it would be wiser to make the user change the source record in the database first. Also, they limit your ability to treat the article elements as disposable.</p>
<h2 id="essentials_cheatsheet">Essentials cheatsheet<a class="header_anchor_link" href="#essentials_cheatsheet"> (§)</a></h2>
<p><a href="/writing/css_for_printing/sample_cheatsheet.html">sample_cheatsheet.html</a></p>
<div class="highlight html"><pre><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span>
<span class="p">@</span><span class="k">page</span>
<span class="p">{</span>
    <span class="nt">size</span><span class="o">:</span> <span class="nt">Letter</span> <span class="nt">portrait</span><span class="n">;</span>
    <span class="nt">margin</span><span class="o">:</span> <span class="nt">0</span><span class="n">;</span>
<span class="p">}</span>
<span class="nt">html</span>
<span class="p">{</span>
    <span class="k">box-sizing</span><span class="p">:</span> <span class="kc">border-box</span><span class="p">;</span>
<span class="p">}</span>
<span class="o">*</span><span class="n">,</span> <span class="o">*</span><span class="p">:</span><span class="nd">before</span><span class="nd">,</span> <span class="o">*</span><span class="p">:</span><span class="nd">after</span>
<span class="p">{</span>
    <span class="k">box-sizing</span><span class="p">:</span> <span class="kc">inherit</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">html</span><span class="n">,</span>
<span class="nt">body</span>
<span class="p">{</span>
    <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">background-color</span><span class="p">:</span> <span class="kc">lightblue</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">header</span>
<span class="p">{</span>
    <span class="k">background-color</span><span class="p">:</span> <span class="kc">white</span><span class="p">;</span>
    <span class="k">max-width</span><span class="p">:</span> <span class="mf">8.5</span><span class="kt">in</span><span class="p">;</span>
    <span class="k">margin</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span> <span class="kc">auto</span><span class="p">;</span>
    <span class="k">padding</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">article</span>
<span class="p">{</span>
    <span class="k">background-color</span><span class="p">:</span> <span class="kc">white</span><span class="p">;</span>
    <span class="k">padding</span><span class="p">:</span> <span class="mf">0.5</span><span class="kt">in</span><span class="p">;</span>
    <span class="k">width</span><span class="p">:</span> <span class="mf">8.5</span><span class="kt">in</span><span class="p">;</span>
    <span class="k">height</span><span class="p">:</span> <span class="mi">11</span><span class="kt">in</span><span class="p">;</span>

    <span class="c">/* For centering the page on the screen during preparation */</span>
    <span class="k">margin</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span> <span class="kc">auto</span><span class="p">;</span>
<span class="p">}</span>

<span class="p">@</span><span class="k">media</span> <span class="nt">print</span>
<span class="p">{</span>
    <span class="nt">html</span><span class="n">,</span>
    <span class="nt">body</span>
    <span class="p">{</span>
        <span class="k">background-color</span><span class="p">:</span> <span class="kc">white</span> <span class="cp">!important</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="nt">body</span> <span class="o">&gt;</span> <span class="nt">header</span>
    <span class="p">{</span>
        <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="nt">article</span>
    <span class="p">{</span>
        <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span> <span class="cp">!important</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="p">&lt;</span><span class="p">/</span><span class="nt">style</span><span class="p">&gt;</span>

<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">header</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span><span class="">Some help text to explain the purpose of this generator.</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">p</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span><span class="p">&lt;</span><span class="nt">button</span> <span class="na">onclick</span><span class="o">=</span><span class="s">"return window.print();"</span><span class="p">&gt;</span><span class="">Print</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">button</span><span class="p">&gt;</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="p">/</span><span class="nt">header</span><span class="p">&gt;</span>

    <span class="p">&lt;</span><span class="nt">article</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span><span class="">Sample page 1</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">h1</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span><span class="">sample text</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="p">/</span><span class="nt">article</span><span class="p">&gt;</span>

    <span class="p">&lt;</span><span class="nt">article</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span><span class="">Sample page 2</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">h1</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span><span class="">sample text</span><span class="p">&lt;</span><span class="p">/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="p">/</span><span class="nt">article</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="p">/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="p">/</span><span class="nt">html</span><span class="p">&gt;</span>
</pre></div>

<hr/>
<p><a href="https://git.voussoir.net/voussoir/voussoir.net/commits/master/voussoir.net/writing/css_for_printing/css_for_printing.md">View this document's history</a></p>
<ul>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/8f9c6b6f8a44789a2731886e03975f8422f245fd"><time datetime="2024-03-03T15:01:40-08:00">2024-03-03</time> Oops, add missing padding from "put this all together".</a></li>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/cf815afed1f59f56847a6677d5c53b493cc6dad2"><time datetime="2024-03-03T12:10:06-08:00">2024-03-03</time> Add css_for_printing.md.</a></li>
</ul>
<address>Contact me: writing@voussoir.net</address>
<p>If you would like to subscribe for more, add this to your RSS reader: <a href="/writing/writing.atom" rel="alternate" type="application/atom+xml">https://voussoir.net/writing/writing.atom</a></p>
</article>
            
            


guid: ee1a034b8a185f4c56d58f287b37625a137674d5/pointing_devices
title: Pointing devices
url: https://voussoir.net/writing/pointing_devices
feedurl: https://voussoir.net/writing/writing.atom
content_mime_type: text/html
content:
            
            <article>
<p><a href="/writing">Back to writing</a></p>

<h1>Pointing devices</h1><ol id="table_of_contents">Table of contents<li><a href="#introduction">Introduction</a></li><li><a href="#mice">Mice</a><ol><li><a href="#microsoft_mobile_mouse_3000">Microsoft Mobile Mouse 3000</a></li><li><a href="#hp_m859pu">HP M859PU</a></li><li><a href="#vogek_vkm42">Vogek VKM42</a></li></ol></li><li><a href="#gamepad_joytokey">Gamepad + JoyToKey</a></li><li><a href="#graphics_tablets">Graphics tablets</a><ol><li><a href="#wacom_bamboo">Wacom Bamboo</a></li></ol></li><li><a href="#xppen_deco_03">XP-Pen Deco 03</a></li><li><a href="#trackballs">Trackballs</a><ol><li><a href="#elecom_relacon">Elecom Relacon</a></li><li><a href="#kensington_slimblade_pro">Kensington Slimblade Pro</a></li></ol></li><li><a href="#dualwielding">Dual-wielding</a></li></ol>
<h2 id="introduction">Introduction<a class="header_anchor_link" href="#introduction"> (§)</a></h2>
<p>Here is a list of computer pointing devices I've used throughout my life. I'll try to keep this updated.</p>
<p>Why? I dunno. I feel like I have something to say and maybe it will encourage you to try something new. In the world of pointing device enthusiasts this would be a teeny tiny baby list, but in the ordinary world most people have never considered their options. Maybe this isn't very interesting, but maybe in ten years I'll be glad I kept track of it.</p>
<p>Writing this list has reminded me that it's important to take a picture of your room/environment/stuff every once in a while. You don't realize the incremental changes happening around you until they pile up. For the cost of a few kilobytes it's better to have it than not.</p>
<p>There is no single best way to organize this list because the timelines overlap, and when I want to tell you about using two devices in conjunction with each other, or the transition from one to the other, I'm not sure which section I should write that in. Let's see what happens.</p>
<p>If you have interesting suggestions of other form factors, you can send me an email. I've always been curious about the <a href="https://3dconnexion.com/us/spacemouse/">3DConnexion SpaceMouse</a>, for one.</p>
<h2 id="mice">Mice<a class="header_anchor_link" href="#mice"> (§)</a></h2>
<h3 id="microsoft_mobile_mouse_3000">Microsoft Mobile Mouse 3000<a class="header_anchor_link" href="#microsoft_mobile_mouse_3000"> (§)</a></h3>
<p>ca. 2011-2016</p>
<p>~$30</p>
<p>Wireless, USB-A receiver, 1×AA battery</p>
<p>This was the first mouse I bought for myself when I bought my own laptop and stopped using the shared family computer. It always worked great and the size fit my hand well.</p>
<p>I had to stop using it because the rubber grip coating on the left and right sides, and to some extent the scroll wheel, was degrading into a sticky mess. This process is sometimes called rubber reversion. RIP.</p>
<p><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/microsoft_mobile_backthen.jpg" title="2016-07-03"/></p>
<h3 id="hp_m859pu">HP M859PU<a class="header_anchor_link" href="#hp_m859pu"> (§)</a></h3>
<p>2016-2021</p>
<p>$0</p>
<p>Wired, captive USB-A</p>
<p>This was already in our house somewhere, so I started using it after tossing the Microsoft mouse. It's not special.</p>
<p>The scroll wheel on this mouse also succumbed to rubber reversion, so I just tore it off, leaving only the plastic bone beneath. I thought this would make it uncomfortable to use, but surprisingly it was fine, though ugly.</p>
<p>This still works and there's nothing wrong with it, but after I got the Vogek mouse for free I wanted to use a wireless mouse again.</p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/hp_wired.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/hp_wired.jpg" title="2024-04-21"/></a></p>
<p><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/hp_wired_backthen.jpg" title="2016-12-05"/></p>
<p><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/hp_wired_backthen2.jpg" title="2021-03-31"/></p>
<h3 id="vogek_vkm42">Vogek VKM42<a class="header_anchor_link" href="#vogek_vkm42"> (§)</a></h3>
<p>2021-2022</p>
<p>$0</p>
<p>Wireless, USB-A receiver, internal battery, USB Micro-B charging</p>
<p>I got this for free in a box of electronics someone handed off to me. You've never heard of this brand because it's cheap junk. It is not very ergonomically pleasant, and I only used it for a few months. Not noteworthy.</p>
<p>Instead of switching back to the HP wired mouse, I decided to stop using a mouse at all, and just use my tablet as my sole pointing device at home for about a year.</p>
<p>I replaced the Micro-B charging port with a USB-C port and wrote <a href="/writing/mouse_usbc_mod">an article</a> showing that process. That article is not very impressive and sometimes I think about deleting it. But now that I'm linking to it I'm a little more obligated to keep it.</p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/vogek_wireless.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/vogek_wireless.jpg" title="2024-04-21"/></a></p>
<h2 id="gamepad_joytokey">Gamepad + JoyToKey<a class="header_anchor_link" href="#gamepad_joytokey"> (§)</a></h2>
<p>This is something I only did a few times, so I can't say it's a full mouse replacement. But sometimes I'd play a game and then wonder if I could keep using the controller for ordinary computer tasks. <a href="https://joytokey.net/en/">JoyToKey</a> is a program that lets you do that. You can also use it to play PC games that don't have gamepad support.</p>
<p>With so many buttons to bind and actual analog controls... it's worth a try?</p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/gamepad.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/gamepad.jpg" title="2024-04-21"/></a></p>
<h2 id="graphics_tablets">Graphics tablets<a class="header_anchor_link" href="#graphics_tablets"> (§)</a></h2>
<h3 id="wacom_bamboo">Wacom Bamboo<a class="header_anchor_link" href="#wacom_bamboo"> (§)</a></h3>
<p>2014-2018</p>
<p>Wired/Wireless, USB Micro-B, internal battery</p>
<p>My dad gave / long-term-lent this to me. He used it for photo editing, but I found that I really enjoyed using it as a general-purpose pointing device. I would seriously encourage anyone who wants to mix up their pointing devices to try a graphics tablet. The pencil-grip posture is intuitive and comfortable for the hand.</p>
<p>If you want to use a tablet you'll probably have to install the manufacturer's driver software. This is always a downside for me. I thought there was no HID standard for digitizer devices — <a href="https://learn.microsoft.com/en-us/windows-hardware/design/component-guidelines/supporting-usages-in-digitizer-report-descriptors">maybe there is</a> but I don't think you can rely on plug-and-play compatibility in this market.</p>
<p>The pen has a rocker button on the side that can do right-click and middle-click. The middle-click is important for scrolling on webpages.</p>
<p>I had to stop using this because it started having issues either not registering clicks or else registering a single click as two. It got to the point where it was too difficult to use and I had to replace it, though we didn't throw it away.</p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/wacom_bamboo.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/wacom_bamboo.jpg" title="2024-04-21"/></a></p>
<p><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/wacom_bamboo_backthen.jpg" title="2016-07-03"/></p>
<h2 id="xppen_deco_03">XP-Pen Deco 03<a class="header_anchor_link" href="#xppen_deco_03"> (§)</a></h2>
<p>2018-present</p>
<p>$100</p>
<p>Wired/Wireless, USB-C, internal battery</p>
<p>I bought this tablet to replace the Wacom. I really like that it has an integrated scroll wheel, which most of the time is more comfortable to use than the pen's middle-click. It has served me well for almost 6 years now and continues to function just fine, except that I broke the scroll wheel a few months ago in early 2024. I don't see any actually broken pieces of plastic, and I tried hard to put the scroll wheel back together but I just couldn't get it to work again. For several weeks my muscle memory had me reaching up to the corner of the tablet to scroll, only to be disheartened by its absence. Now, the Kensington has picked up the slack here.</p>
<p>I broke the scroll wheel by opening up the tablet. The reason for <em>that</em> is I started to hear a high-pitched whine coming from a spot just below the buttons on the left side. It was subtle but annoying enough that I had to do away with it. I didn't take any pictures of the offending part and I don't want to open the tablet again to remember what it was — I do remember expecting to see an obvious transformer coil and it was not as obvious as I hoped. I put down a glob of RTV silicone on the board hoping to dampen the sound and put it back together. I don't hear the whine any more, so I guess it's ok but I'd rather chalk it up to luck than say I fixed it.</p>
<p>The pen came with 8 nibs and I'm still using my first one.</p>
<p>I have two monitors on my computer, and the tablet spans both of them. You may think the tablet space needs to be mapped to only one monitor, but after getting acclimated it's perfectly comfortable to use across both. However I imagine that with more monitors it would start to feel a bit cramped.</p>
<p>At one point, I thought it was malfunctioning because the cursor would jitter uncontrollably about once per second when I held the pen near the tablet. I even emailed their support back and forth a few times. Embarrassingly, it turned out this was caused by a wireless Qi charger I had put on my desk to charge my phone. Apparently Qi chargers send out a regular pulse while idle so they can detect when you set your device on top of it. This electromagnetic pulse interferred with the pen tablet.</p>
<p>Sometimes I look at other graphics tablets on the market to see what's new these days, but this one's working fine and I don't have any need to replace it.</p>
<p><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/xppen_deco03_backthen.jpg" title="2019-08-11"/></p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/xppen_deco03.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/xppen_deco03.jpg" title="2024-04-21"/></a></p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/xppen_deco03_use.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/xppen_deco03_use.jpg" title="2024-04-21"/></a></p>
<h2 id="trackballs">Trackballs<a class="header_anchor_link" href="#trackballs"> (§)</a></h2>
<h3 id="elecom_relacon">Elecom Relacon<a class="header_anchor_link" href="#elecom_relacon"> (§)</a></h3>
<p>2022-present</p>
<p>$60×2</p>
<p>Wireless, USB-A receiver, 2×AAA battery</p>
<p>I have two Relacons now; one at work and one at home.</p>
<p>I bought the first one specifically to use at work. I was issued a company laptop but no mouse, and I decided I wanted to research and buy my own instead of asking the IT department for whatever was on hand. Initially I was looking at ordinary wireless mice, but I fell into the <a href="https://old.reddit.com/r/trackballs">/r/trackballs</a> rabbit hole and discovered this very unique option from Elecom and decided to give it a try.</p>
<p>Overall I am very satisfied with this device. Here are the main benefits:</p>
<ul>
<li>Ambidextrous — almost all trackball mice are for right-handed use only.</li>
<li>Lots of buttons — I'm able to do a lot of common tasks without touching the keyboard at all.</li>
<li>Handheld — you don't need to keep your arm/wrist on the desk. You can hold it on your lap, at your sides, lean back, or stand up and stretch.</li>
</ul>
<p>Most of the time, I do keep my arm on the desk while using this, but the natural posture keeps the thumb upward, which is similar to vertical ergonomic mice and is generally considered better than the palm-down posture.</p>
<p>The Relacon works as a HID device out of the box, but the media controls are not useful to me. Elecom offers their own driver software to remap them, which I tried for a few weeks, but I did not enjoy it. Their software was clunky and didn't offer all the customization I was looking for, especially ball-scrolling.</p>
<p>I found this fantastic project called <a href="https://github.com/jfedor2/hid-remapper">HID Remapper</a> which you install on a Raspberry Pi Pico and plug the receiver into. The Pi intercepts the controls before they reach the computer and translates them to something else. The downside of this is you have a dongle hanging out of your computer instead of just the little receiver, but the upside is that you don't need any driver software. You can plug your HID Remapper into any computer and bring all your customization with you, as it appears as just an ordinary HID device to the PC.</p>
<p>Here is my configuration file: <a href="/writing/pointing_devices/hidremapper_relacon.json">hidremapper relacon.json</a></p>
<ul>
<li>Big left arrow = ctrl+pageup to change tabs</li>
<li>Big right arrow = ctlr+pagedown to change tabs</li>
<li>Media up = ctrl+alt+tab to change applications</li>
<li>Media left = Browser back navigation</li>
<li>Media down = ctrl+w to close tabs</li>
<li>Media right = ball becomes a scroll wheel</li>
<li>Media center = Enter/Return, mainly used to confirm alt-tab application</li>
</ul>
<p>I bought the second Relacon about 16 months later to keep at home. This was after I had tried the Kensington and found it too painful to use. I looked at lots of other trackball options, but there are so few ambidextrous/left-handed trackballs that I felt I was better off getting another of these.</p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/elecom_relacon.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/elecom_relacon.jpg" title="2024-04-21"/></a></p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/elecom_relacon_use.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/elecom_relacon_use.jpg" title="2024-04-21"/></a></p>
<h3 id="kensington_slimblade_pro">Kensington Slimblade Pro<a class="header_anchor_link" href="#kensington_slimblade_pro"> (§)</a></h3>
<p>2023-present</p>
<p>$110</p>
<p>Wireless, USB-A receiver, internal battery, USB-C charging</p>
<p>From 2022 to 2023, after retiring the Vogek, I used the XP-Pen as my sole pointing device at home. At this point I don't play any video games, and for all my other computing tasks the tablet alone is perfectly fine. However, after a year of this, I was starting to experience pain in my right wrist. I knew that I needed to bring back something on the left to balance out my usage. Since I liked using the Relacon at work, I spent more time looking at other trackballs.</p>
<p>Almost all trackballs on the market are shaped for right-hand use only. Since I use my left hand for the mouse, they are all automatically disqualified, unfortunately. I'd like to try another Elecom model, but the ones that interest me are not available in lefty.</p>
<p>That leaves the category of ambidextrous trackballs with the ball in the center. One of the most famous of these is the <a href="/writing/pointing_devices/logitech_trackman_marble.jpg">Logitech Trackman Marble</a> which, shockingly, offers no means of scrolling, so that's disqualified for me. I came very close to buying an <a href="/writing/pointing_devices/xkeys_ltrac.jpg">X-Keys L-Trac</a> but I just don't have enough confidence in that arrangement of the scrollwheel and middle button.</p>
<p>I looked at lots and lots of options and finally decided to try the Kensington Slimblade. I was concerned about the ergonomics of the upward angle this puts the palm, but it seemed like the least-worst compared to all the other ones I considered. The scrolling mechanism is unique — you rotate the ball around the Z axis and the optical sensors detect this as a scroll motion; this turns out to be very intuitive and comfortable to do.</p>
<p>Unfortunately, the ergonomics turned out to be a big problem and I found the Slimblade to be outright painful to use after giving it a few weeks of genuine attempts to enjoy it. Overall I <strong>do not</strong> recommend buying this. Some people have used 3D-printed parts or blocks of wood to adjust the angle of the Slimblade and thus the wrist posture. I didn't do that, but I tried a pad/rest to help elevate the wrist and bring the palm to a flatter angle with respect to the mouse, but this wasn't worth it.</p>
<p>For some reason I am the kind of person that doesn't like to return anything, ever, so I put the Kensington back in its box and shelved it for a few months. Then the scroll wheel on my XP-Pen broke and I thought hey, I'll bring it back even if only to use it as a scroller. This is working well for me and sometimes I'll use the Kensington for a few minutes of normal work. It's just not good enough to use it on its own for a long time.</p>
<p>Kensington also offers a driver software to remap the buttons, but since I don't like installing more drivers I'm not using that.</p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/kensington_slimblade.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/kensington_slimblade.jpg" title="2024-04-21"/></a></p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/kensington_slimblade_use.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/kensington_slimblade_use.jpg" title="2024-04-21"/></a></p>
<h2 id="dualwielding">Dual-wielding<a class="header_anchor_link" href="#dualwielding"> (§)</a></h2>
<p>Ever since I started using graphics tablets, I've enjoyed keeping multiple pointing devices on my desk at the same time: a mouse for the left hand and a pen for the right. Each is better suited to different tasks. I highly recommend keeping multiple devices in rotation so you can use whatever is most comfortable for the task and reduce the chance of <a href="https://en.wikipedia.org/wiki/Repetitive_strain_injury">RSI</a>.</p>
<p>An ordinary mouse is best for playing first-person games. A tablet is best for drawing, photo editing, or <a href="https://www.openstreetmap.org">openstreetmapping</a>. A trackball can be more comfortable and ergonomically healthy if you get one that's right for you.</p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/xppen_deco03_kbnearer.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/xppen_deco03_kbnearer.jpg" title="2021-05-27"/></a></p>
<p><a href="https://files.voussoir.net/writing/pointing_devices/xppen_relacon_slimblade.jpg"><img alt="" loading="lazy" src="https://files.voussoir.net/writing/pointing_devices/thumbs/xppen_relacon_slimblade.jpg" title="2024-04-21"/></a></p>

<hr/>
<p><a href="https://git.voussoir.net/voussoir/voussoir.net/commits/master/voussoir.net/writing/pointing_devices/pointing_devices.md">View this document's history</a></p>
<ul>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/ee1a034b8a185f4c56d58f287b37625a137674d5"><time datetime="2024-04-25T17:18:39-07:00">2024-04-25</time> Add pointing_devices.md.</a></li>
</ul>
<address>Contact me: writing@voussoir.net</address>
<p>If you would like to subscribe for more, add this to your RSS reader: <a href="/writing/writing.atom" rel="alternate" type="application/atom+xml">https://voussoir.net/writing/writing.atom</a></p>
</article>
            
            


guid: 168f899630936e1a53e5716808bdf21e4276b8b7/ricoh_gr_bag
title: Ricoh GR carrying bag
url: https://voussoir.net/writing/ricoh_gr_bag
feedurl: https://voussoir.net/writing/writing.atom
content_mime_type: text/html
content:
            
            <article>
<p><a href="/writing">Back to writing</a></p>

<p><a class="tag_link" data-qualname="today_i_did_this" href="/writing/tags/today_i_did_this">[today_i_did_this]</a></p>
<p><a class="tag_link" data-qualname="photography" href="/writing/tags/photography">[photography]</a></p>
<h1>Ricoh GR carrying bag</h1>
<p>In May, I bought a Ricoh GRIII. I'll write more about that in my next article. Today, we're just talking about how to carry it.</p>
<p>The GR is famously pocketable. Indeed, it does fit in a jeans pocket.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_23-21-25.jpg"/></p>
<p>But the thing is, you really have to devote the entire pocket to it, because it is somewhat of a tight fit on its own and I wouldn't want other stuff banging around or scratching the screen or buttons. Also, it looks pretty awkward.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_23-22-45.jpg"/></p>
<p>It is "convenient" to carry this way, in the sense that it doesn't require learning anything new, but I found it extremely inconvenient to extract from the pocket and take a picture. The specifics of all this will depend on your pants, of course.</p>
<p>But I'm the type of person that carries a lot of stuff in both of my pockets already, so I can't just move everything else over to the other side. So although the GR is the internet's favorite pocket camera, it won't be staying in my pocket.</p>
<p>I also tried putting some split rings through the holes in the camera body (scratching it up in the process — the lugs are so tiny) and strapping directly to it.</p>
<p>This works ok as long as the camera is concealed under a jacket, but without the jacket I would feel weird wearing an exposed camera like this.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-05-21_22-27-06.jpg"/></p>
<p>Also, I found the strap inconvenient when actually taking pictures. You see, the strap needs to be short enough that the camera hangs around waist level during carry, but long enough that when taking a picture you can comfortably maneuver the camera out in front of you. I found that when the strap was the right length for carrying, it was too tight for actually taking pictures. And furthermore, the split rings being up against the palm and near the shutter button make the camera less pleasant to the touch.</p>
<p>Before we go any further, I'll let you know that I did go ahead and purchase a crossbody sling bag (man purse) which I now bring with me on day trips.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-30_18-40-03.jpg"/></p>
<p>However, on a typical workday I don't need to carry a purse worth of stuff, I really just want to carry the camera. I wanted a carry method that can fit discretely under a jacket, and also doesn't look totally silly without the jacket. A little silly I can tolerate.</p>
<p>I was inspired by the simple construction of a drawstring pouch, and I decided to practice my sewing skills and make a custom bag specifically for the GR. Then, I use a wrist lanyard when shooting so I have free mobility.</p>
<p>In this article I'll be showing you my third draft of the pouch.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_23-06-45.jpg"/></p>
<p>I went to the fabric store and bought some ripstop nylon for the exterior and polyester satin for the interior. Later, I also bought some adhesive basting spray after I realized how hard proper pinning and basting is.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-30_22-35-01.jpg"/></p>
<p>The first draft of this bag was just the nylon and satin layers, with split rings stitched in at the top.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-06-16_23-03-28.jpg"/></p>
<p>There were two things I did not like about this draft. First, because these are thin materials, the buttons were too easy to feel and press through the bag, and I thought they should be better protected. Second, when I'm done taking a picture and I'm trying to put the camera back into the pouch, the pouch has already self-closed, again because the materials are thin and light. Then it becomes a fiddly operation instead of a smooth holstering.</p>
<p>In the second draft, I added a layer of felt between the other layers. This padding gives protection from bumps and makes the bag a little less floppy, the top staying open by itself more easily for holstering.</p>
<p>The problem with the second draft was that I did not feel comfortable putting the camera in the bag without its aluminum lens cap. Even with the felt padding, I imagine a nice bump against the corner of a table could punch the lens protector blades in a bad way.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-23_22-57-35.jpg"/></p>
<p>The lens cap is a seemingly small but significant <a href="/writing/friction">friction</a> against taking pictures. It takes longer to get the camera ready to shoot, and then the lens cap must itself be pocketed for as long as I want to use the camera. I found it was really discouraging me from taking smaller opportunities.</p>
<p>So, for the third draft, I made a 60mm × 2mm disc of plastic on the 3D printer, and I integrated this directly into the front face of the bag by patching over it and sewing it tight. I feel much more confident that the lens is protected from impact now.</p>
<p>Please sign up for my six-month CAD course and you too can become a modeling master like me.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-21_01-19-30.png"/></p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_23-08-27.jpg"/></p>
<p>The felt layer is the one that has to be sized accurately to align the plastic disc with the lens, then the other layers are just trimmed to that. I had estimated 4in × 12.5in but it turned out more like 4.75in × 11.5in.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_15-29-07.jpg"/></p>
<p>I laid out where the camera would be on the inside, then prepared the patch for the disc which goes facing outward. I spray-basted the disc and the patch together, then amateurishly sewed two laps around the circle.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_15-29-36.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_15-36-25.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_15-51-40.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_16-13-11.jpg"/></p>
<p>I sprayed the satin and felt together, then trimmed the satin to size. I used an electric arc lighter to melt the edges of the satin to prevent it from fraying. The arc lighter has a much smaller area of effect than a flame lighter and is much more controllable when targeting the edges.</p>
<p>I made sure to cut the satin so the weave is facing longways. When you rub your finger against the fabric, you can feel it is more slippery in one direction than the other, and I wanted that along the length of the interior instead of the width.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_16-27-18.jpg"/></p>
<p>I sprayed the nylon and felt together, trimming the width to size but leaving some extra length to be folded over the top and bottom, which are sewn together.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_16-42-35.jpg"/></p>
<p>My stitches are much straighter on this third draft than they were on the first two.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_17-04-59.jpg"/></p>
<p>In the previous two bags, I hand-stitched the split rings in after the bag was already formed. This was functional but somewhat ugly. I took this opportunity to do away with the split rings by locking in folded-over pieces of paracord with the inner strands removed, just at the last inch of the side seams. I crudely backstitched over them several times so hopefully it will be sturdy.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_17-47-28.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_17-47-37.jpg"/></p>
<p>When the bag is turned right-side out, the paracord does not look as professional as I hoped because I didn't run my seam tightly enough against the edge that matters, so it sticks out a bit off the sides. But it will have to do until draft four.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_17-53-24.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_17-53-56.jpg"/></p>
<p>It almost looks like an intentional design element, and it would probably look good if it ran all the way to the bottom.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-23_22-48-09.jpg"/></p>
<p>Finally, it was time to box the corners. Without boxing, the two bottom corners of the bag flare out stupidly when the camera is inside. This has something to do with the world being three dimensional.</p>
<p>I had to do this stitch by hand because the bag does not fit through the machine at that angle, especially not with the big plastic disc inside of it. I drew a line 0.75in from the tip of the triangle. I spent a lot of time pinning and pre-visualising this stitch because it's very important for it to be accurately perpendicular to the main body line. Luckily it went ok.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_22-53-22.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-23_22-50-00.jpg"/></p>
<p>It takes a minute to turn this bag out due to the puffy felt layer and the big plastic disc that at first feels like it won't go through the mouth of the bag. It comes out looking like a wrinkly old raisin, but after some massaging it smooths out.</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_22-56-20.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_22-57-06.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_22-57-52.jpg"/></p>
<p>The carrying strap is a single piece of paracord with <a href="https://www.aliexpress.us/item/2251832811586187.html">mini carabiners</a> on the ends. It doesn't need to be adjustable when it's bespoke!</p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_22-59-23.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_23-00-38.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/ricoh_gr_bag/thumbs/2024-07-20_23-03-21.jpg"/></p>
<p>I've been carrying this third draft for ten days now and I can definitely feel the improvement over the first and second. It is easy to withdraw and holster the camera and although the lens cap cost me $15 I'm glad not to use it. Now the goal is to keep this up for... ever.</p>
<p>I am comfortable wearing this out in public since most people do not pay enough attention to strangers to think that it's weird. However, when I'm at work I leave it on my desk instead of wearing it all day because I cannot shake the feeling that I'm playing some kind of character by trying to carry a camera literally all the time in front of everyone. When I wear a jacket I do feel comfortable carrying the camera under the jacket all day since it's out of sight, but it's summer now and it's too hot for that.</p>
<p>1,500 words and 30 pictures for a project on par with Baby's First Sewing Pattern? I still feel like this kind of thing doesn't deserve to be posted as if it's some kind of accomplishment. But I'll say more about that in the next article, too.</p>

<hr/>
<p><a href="https://git.voussoir.net/voussoir/voussoir.net/commits/master/voussoir.net/writing/ricoh_gr_bag/ricoh_gr_bag.md">View this document's history</a></p>
<ul>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/168f899630936e1a53e5716808bdf21e4276b8b7"><time datetime="2024-07-30T23:21:24-07:00">2024-07-30</time> Add ricoh_gr_bag.md.</a></li>
</ul>
<address>Contact me: writing@voussoir.net</address>
<p>If you would like to subscribe for more, add this to your RSS reader: <a href="/writing/writing.atom" rel="alternate" type="application/atom+xml">https://voussoir.net/writing/writing.atom</a></p>
</article>
            
            


guid: 34430fa3c647efdf5b3d0d655460e395eb59864d/hobby_photography_2
title: Further thoughts on hobby photography
url: https://voussoir.net/writing/hobby_photography_2
feedurl: https://voussoir.net/writing/writing.atom
content_mime_type: text/html
content:
            
            <article>
<p><a href="/writing">Back to writing</a></p>

<p><a class="tag_link" data-qualname="photography" href="/writing/tags/photography">[photography]</a></p>
<p><a class="tag_link" data-qualname="introspection" href="/writing/tags/introspection">[introspection]</a></p>
<h1>Further thoughts on hobby photography</h1><ol id="table_of_contents">Table of contents<li><a href="#introduction">Introduction</a></li><li><a href="#edc">EDC</a></li><li><a href="#black_and_white">Black and white</a></li><li><a href="#subject_matter_subjects_matter_subject_matters">Subject matter / subjects matter / subject matters</a></li><li><a href="#originality">Originality</a></li><li><a href="#what_if_everyone_just_has_poor_taste">What if everyone just has poor taste</a></li><li><a href="#street_photography">Street photography</a></li><li><a href="#privileged_eye_outsider_eye">Privileged eye, outsider eye</a></li><li><a href="#videography">Videography</a></li><li><a href="#money_and_recognition">Money and recognition</a></li><li><a href="#zooming_out">Zooming out</a></li><li><a href="#website_portfolio_gallery">Website, portfolio, gallery</a></li><li><a href="#instxgrxm">Instxgrxm</a></li><li><a href="#how">how</a></li><li><a href="#fuji_dial_slipups">Fuji dial slip-ups</a></li><li><a href="#conclusion">Conclusion</a></li></ol>
<h2 id="introduction">Introduction<a class="header_anchor_link" href="#introduction"> (§)</a></h2>
<p>It has been two years since I wrote down <a href="/writing/hobby_photography">some early thoughts on hobby photography</a> and I thought it would be worth following up as a separate article.</p>
<p>This article contains <a href="/writing/advertixing/#hailcorporate">brand names</a> and I'm sorry about that. While there's plenty to say about the philosophy and ideals of photography in the abstract, I also want to give a few thoughts about the specific items I'm using.</p>
<h2 id="edc">EDC<a class="header_anchor_link" href="#edc"> (§)</a></h2>
<p>I have not been able to convince myself to bring the Fuji camera everywhere. Although it is not humongous by any means, it is too large and conspicuous for me to go and do normal stuff without looking like I'm trying to be a "photographer" all the time, especially since it must be worn on a shoulder strap as I'm not in the habit of carrying a backpack, which seems to be a key factor amongst those who do carry these cameras daily. I want to increase the quality and quantity of life memories that I capture, but I don't want to feel like every day needs to be a photo shoot or like I'm playing a character. I want photography to be available to me whenever opportunity presents itself, but I don't want it to consume my entire personality and perception.</p>
<p>So, I bought a Ricoh GR III.</p>
<p>I told myself that even if I fail to EDC this one too, at least it can act as a B camera for when I've got the telephoto lens on my A camera, and indeed it has been a helpful addition there.</p>
<p>I tried a couple different ways of carrying the GR, and wound up making my own little bag for it. This became <a href="/writing/ricoh_gr_bag">its own article</a>.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2024-07-20_23-03-21.jpg"/></p>
<p>I have been carrying it this way for a few months, even though I am constantly nervous that people will ask me "what is that you're wearing?". Surprisingly, this really has not happened. I wore it under my jacket back when the weather was cooler, and for some reason none of my coworkers, who would not be used to seeing this suspicious line of paracord, ever asked about it. Maybe they think it's a piece and if they ask too many questions I'll pop 'em.</p>
<p>One of the final big pushes that got me to buy the GR was <a href="https://youtube.com/watch?v=tI9qHg2yGtw" title="My First Digital Camera 24 Years Later! Intel PC Pocket Cam">this video</a> by Clint of Lazy Game Reviews about an Intel pocket camera. Based on the pictures he shows here, it's clear he was in the habit of carrying this camera with him frequently, possibly every single day for some time.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/lgr_camera_1.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/lgr_camera_3.jpg"/></p>
<p>Most of the scenes he took pictures of are just ordinary, but that's what makes them interesting. After 24 years, even "ordinary" is different. A picture of a retail store shelf will make you say "wow, they used to look like that?" or "wow, they still look like that!". It's a winner either way.</p>
<p>This video really made me feel like I've been missing out by not carrying a camera every day. A lot of times we think "I don't need to take this picture today, it can wait until tomorrow", but that will be a picture of tomorrow, not today. If you want a picture of today, this really is your only chance. By tomorrow you'll have missed it. So although I knew I would still deal with discomfort and embarrassment, I felt like I had to try.</p>
<p>So I've been practicing a little bit of this lifestyle photography thing with some of my coworkers. Like when a group of us goes for a walk to stretch or we have a Thursday pizza party, I've taken a few casual pictures then. I keep expecting people to ask me questions about this camera, but they don't. I keep telling myself how abnormal it is to carry any kind of camera at all these days, and especially how abnormal it is to wear it on a homemade sling. Yet no one questions it. That's a good thing and all, but wow does it make me feel like the autistic one.</p>
<p>Even still, I sometimes see pictures online of people working at their desk / cubicles in what seem to be candid or spontaneous circumstances, and I can't figure out how exactly those moments come about.</p>
<p>I can feel that my mindset is still lagging behind the physical act of EDC. It is not enough to carry the camera if you do not use it. Last weekend I went out to see a band, and unfortunately the show got cancelled. I got to hang out with the band for about an hour anyway, and yet I didn't take even a single picture of them. It would have been so great to have a "We tried!" photo, and I can't believe this thought didn't cross my mind at the time. I just completely missed it. I still feel like I need some kind of clear event or scene to grant me permission to take photos — I need to get better at realizing the value of the present moment even if it is not what I came looking for.</p>
<p>I said last time that I don't like PASM dials, but the GR is a PASM camera, so has my opinion changed? No. I still don't like PASM, and I consider it a downside of this purchase. This will spend most of its time in P anyway. In general, the shooting experience of the GR is a lot worse than that of a bigger camera. Even though it is simple, and it is a big step above a phone camera, I would not recommend it to photography beginners because it sacrifices a lot in the name of EDC. I miss my dials and focus ring. The on-screen preview always looks uglier than the photo, so you have to learn to ignore the preview and trust the camera. The blackout interval is so bad that in burst mode you literally don't know what you're getting after the first shot. The face-detection does not work in continuous autofocus, only single autofocus, which is weird since the face detection algorithm runs continuously during live view to present the white box in the first place. There are all around little touches to the firmware that could be improved — to give a hyper specific example, when the Fuji is in burst mode you can still chimp if you release the shutter button quickly enough, but if you keep it half-pressed it stays in live view mode, so you get the best of both single and burst shooting in a single mode. On the GR, it does not go back to live view when half-pressing the shutter in burst mode unless you turn off chimping entirely. I'd rather at least the chimp setting was stored separately across single and burst shooting.</p>
<p>Maybe I didn't do enough research before buying, because the Sony RX100 VII apparently slashes a lot of these concerns with better continuous AF, no blackout time, a zoom lens, and even a flip-out screen, though with a smaller sensor to compromise. I can't say for sure I would have picked that if starting fresh, but darn. Of course, if there was such a thing as a Fuji X-T Mini I probably would have bought that.</p>
<p>So far, I haven't addressed the elephant in the room, which is that my cell phone is already an everyday carry camera. Why not use that? I don't think I'll be able to give very convincing answers because it really is more of a psychological block than a practical one. I'll say first of all that I don't like the pictures my phone produces very much: they are simultaneously oversharpened and soupy despite what you'd think is a sufficient 12mp resolution.</p>
<p>I don't want to buy a new phone just to prioritize cameras, because I feel like that's what almost everybody is doing these days and it has led to the severe atrophy of other good qualities and features: the 3.5mm headphone jack, the SD card slot, removable batteries, lay-flat back design, overall software UI and UX, cloud dependencies, and the squandering of local processing power. It's not that these things can't coexist with good cameras. They're all completely unrelated, technically. But I feel that the top producers of cell phones are able to abuse their customer from every direction and the customers put up with it because "the cameras are better than last time". I don't want to be stuck in the same cattle line.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/doing_my_part.jpg"/></p>
<p>Hypocrisy alert: there are not many phones that still include a 3.5mm jack, so my next phone might be a Sony Xperia which happens to be definitively camera-centric. I don't want to get into the Chinese brands or Samsung so there aren't too many good choices.</p>
<h2 id="black_and_white">Black and white<a class="header_anchor_link" href="#black_and_white"> (§)</a></h2>
<p>I like black and white. There are many stereotypes about noobs / losers who think that black and white is a one-click cheat code to make any picture a masterpiece work of art. Am I one of those losers? It's possible. Personally, I think I actually like black and white, but of course that's exactly what one of the losers would say, wouldn't they.</p>
<blockquote><p>The wife of a friend of mine decided one day that she was going to be a "professional photographer".</p>
<p>She bought a cheap digital SLR and then she ran around taking random pictures of everyday bullshit, then spent a few minutes de-saturating them in PS or applying sepia-tone filters to them.</p>
<p>She proudly displays them on her "professional photographer" website for all to see.</p>
<p>What a disgrace to the profession.</p>
<p><a href="https://old.reddit.com/r/AskReddit/comments/gclsg/_/c1mnxwx/">https://old.reddit.com/r/AskReddit/comments/gclsg/_/c1mnxwx/</a></p>
</blockquote>
<p>Oops!</p>
<p>I like black and white artistically because it accentuates geometry and texture. I like that it places distance between the photo and reality, allowing it to act as fiction. Once you get used to looking at black and white, you start to forget that anything is even missing at all, it just seems normal. Black and white pictures give off what is perhaps an illusion that they are sharper and more defined, which makes them more fun to zoom in and pixel peep and be amazed at what's resolved — every time I zoom in on a picture and see the individual threads of a person's shirt I am amazed. But I also like black and white pragmatically because at maximum ISO, black and white pictures look acceptably grainy but color pictures look hellishly bad. So when I'm shooting in the dark without a flash, I'll get better results in black and white.</p>
<p>Oftentimes, I think I want to only shoot black and white forever. For the early decades of photography, that was the only option, and people got good results, so I probably could too. I feel like when I switch into color I am not being true to myself or my vision.</p>
<p>When I shoot pictures that are going out to other people, though, I know that everybody else wants and expects color. I'm the resident event photographer at my workplace and it would be pretty dumb of me to send out pictures of a corporate event in black and white. A couple bands have paid me for my photos, and of course they want them in color.</p>
<blockquote><p>For a long time I was shooting both black and white and color, and trying to figure out how to mix them when I grouped photos together. It felt like two different photographers or maybe more like five different photographers.</p>
<p>I took a workshop with Alex Webb and his wife Rebecca. They looked at the pictures and moved them around and whispered to each other, and then they both turned to me and Rebecca said "yeah, you don't really ever need to shoot color again the rest of your life".</p>
<p>And I was like "thank you, so much". It was the greatest gift, I mean I still get chills when I think about it because it was such a defining moment. I became a black and white photographer in that moment and never looked back.</p>
<p><a href="https://www.youtube.com/watch?v=2KMTzS1M1-M&amp;t=3m15s">Reuben Radding</a></p>
</blockquote>
<p>Frankly, black and white is very often easier than color. Color is hard because it adds an extra dimension for failure, another opportunity to make things look bad. Even in good conditions, the colors of your subject might look bad against those of the background, or there'll be someone wearing a distracting neon-yellow sweater that ruins your shot. And in poor conditions you're fighting with the white balance or low-<a href="https://en.wikipedia.org/wiki/Color_rendering_index">CRI</a> indoor lights to keep people's skin from looking pale or sickly green.</p>
<p>If color is hard, then black and white is a crutch, and to say "I will only shoot black and white forever" is to say "I'm not willing to take on a challenge and learn new skills". So it's probably important to do some portion of color photography pretty consistently, in order to be sure that the decision to use black and white is actually a decision and not a tar pit.</p>
<p>Likewise, of course, color can also be a crutch. It's a people-pleaser, because the majority of people are normies content with "ooh, look at the pretty colors!". Color is an easy source of praise, so it can be difficult to stick to a black and white vision when others are involved. But remember also the difference between taking a beautiful picture and taking a picture of a beautiful thing; I often think to myself that if a scene doesn't look good in black and white then it's not a good scene, so there's nothing to lose by always shooting B&amp;W. But I've never actually said that out loud because I don't have these kinds of conversations with anyone. One thing I know for certain is that if I'm shooting black and white and I see something very colorful so I switch the camera back to color, that'll be my worst photo of the day. "Look at the pretty colors" is the least convincing reason to shoot color.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/colors.jpg"/></p>
<p>When I shoot color, am I betraying myself to satisfy others? Yes. Is it good for me to betray myself, to force myself to try new things and learn? Yes. What does it mean to stay true to yourself, then?</p>
<p>The fantasy of noble self-martyrdom tells me to reject everyone's expectation for color photos; to tell them that if they want color photos they'll have to take them themselves; to tell them that if they want to hire me they'd better be aware they're only getting black and white. The less-stupid side of me tells me that's stupid. If the people around me don't like the pictures I take or find it weird/uncomfortable that I insist on a black and white identity, then they won't want me around, and won't want me to take pictures of them, and then I'll just be alone with the camera, which defeats the point. It's only been two years, so it's too early to say I've found what I'm best at, yet. It's like a child watching the same movie on repeat dozens of times because they haven't discovered or can't process anything else yet.</p>
<p>I took pictures at a 4th of July party in 2023 in black and white, and I regret it. I never sent the pictures out to anyone because I am embarrassed and I do not want to be confronted with the question "why are they in black and white?", since I don't have a reason. Noble self-martyrdom? It would be nice if I was at least personally satisfied with them, but I'm not. When I look at those photos, the only feeling they give me is that I was sticking dogmatically to the identity of black and white photographer as if that was important somehow. In 2024 I shot the same party in color.</p>
<p>I think most of us assume that the general public doesn't like black and white, but for what it's worth, when I've shared my photos with strangers I've never gotten the sense that they were disappointed by it.</p>
<p>I'll be leaving the GR in color mode by default. The purpose of the EDC is to capture life memories and documentary, which simply makes more sense in color. B&amp;W might look better artistically, but it is pretentious to insist that every waking moment of life be a piece of art. Sometimes you want to remember something as it actually appeared. I think this will help me keep my use of black and white deliberate.</p>
<p>I would be willing to buy a monochrome-only camera body, despite what I just said. My reasoning here has changed over time. I used to imagine myself buying a monochrome body to remove the option, temptation, and doubt to use color; to enforce the decision to shoot B&amp;W at all times. So that when people ask me for the color copy of a photo I can tell them "ha, this camera can't even take color pictures". But after my experiences and reflections, I don't want that any more.</p>
<p>I still would buy a monochrome body because when it's time for some deliberate black and white, a true monochrome sensor would feel like the right tool for the job. I would like to fully embrace the black and white power when the situation calls for it. Now, monochrome sensors do have a technical advantage in sharpness and low-light performance since there's no <a href="https://en.wikipedia.org/wiki/Bayer_filter">bayer filter</a> in front of the pixels, but I feel that's not actually a big enough point and only provides the guise of rationality to an otherwise lusty desire.</p>
<p>Despite being a reduction in featureset, monochrome sensors of course cost more because the consumer demand is smaller. There are people out there who do monochrome conversions of color cameras by <a href="https://www.youtube.com/watch?v=6PkXZMvzZHk">physically destroying the bayer filter</a> off the sensor, but because the camera's firmware is still programmed to be a color camera, this essentially breaks the in-camera jpeg engine and requires all shots to be done in raw and processed on the computer. That ain't for me. If Fuji released a monochrome X-T body with all the dials and switches to my satisfaction (see below), it would pretty much be an instabuy for me at a price up to about $2,000.</p>
<p>I must acknowledge the effect of gear on this so-called black and white identity, for a lot of things that we think are innate about ourselves are really just a product of circumstance. I think the fact that I did not start photography until the age of mirrorless cameras with EVFs is absolutely critical to my use of black and white. If I had started with a DSLR, looking through an optical viewfinder, I'm certain I would have taken all my pictures in color. And I mean that's exactly what happened when I borrowed my dad's DSLR at the rennaisance faire. And if I had grown up in the days of film, perhaps the time, effort, and cost would have kept me out of the hobby entirely.</p>
<h2 id="subject_matter_subjects_matter_subject_matters">Subject matter / subjects matter / subject matters<a class="header_anchor_link" href="#subject_matter_subjects_matter_subject_matters"> (§)</a></h2>
<p>I said last time that pictures of plants and flowers were not meaningful enough, and at this point I am staunchly against taking landscape and nature photos. I'm not saying you shouldn't, I'm just saying I don't want to. When I go for a hike I'll bring my camera, but I'll only take a few pictures to represent the day. I feel that 99% of people can't possibly care about a photo, like truly care beyond its moment of eyecandy, if it doesn't have a person in it. And before you say I'm submitting to people-pleasing, yeah, I'm part of that 99%. Documentary photos of places are still important historical artifacts that our society should continue to produce. But as an individual, while I might find place photos interesting to look at, they rarely evoke any deeper feelings. When they do, those feelings are usually envy that I don't live in a more interesting and beautiful place. So the emotional range of place photos is typically neutral to negative. Instead, I really focus on taking pictures of people and giving them away.</p>
<p>For me there is the intrinsic satisfaction of creating art and memories that will last a long time, and the extrinsic satisfaction of giving people something they enjoy. To put it selfishly, I want people to like me? I use photography to get myself out of the house and into places I would not otherwise be, so that I can meet new people, have something with which to break the ice, and have a reason to keep them in my contacts. I do photography to socialize and connect. A lot of photographers say that they do it to "spread the love" or "tell our story" and... I don't really identify with this. When I talk about making connections, I don't mean it in the 1960s flower-power hippie <em>looove, man</em> kind of way, more like the 2020s computer-science major "I don't have enough friends" kind of way.</p>
<p>I no longer take photo walks through residential areas, either. It's just not interesting enough in a car-dependent place and the pictures do not mean anything to me. I do still take walks through the local colleges because at least the architecture and scenery is more interesting to look at. Still, the photos I take there usually aren't too great or meaningful. For me it is a chance to practice with different camera settings or styles I haven't tried before, so that I am more ready to use them in a real shoot. And every once in a while I come across something that might have lasting cultural value.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2024-03-24_12-10-31.jpg"/></p>
<p>By chance, I have spent a great deal of time in the genre of music photography. I personally don't play any music, and I never considered myself to be <em>that</em> interested in live music. But music photography is available in a way that some other genres aren't. First of all, there's kind of a lot of people that play music. You can probably find at least one or a few people in your circle of family, friends, or acquaintances that play. Secondly, music provides a clear and obvious reason to take pictures of your subjects, whereas asking family, friends, or strangers to do posed studio or environmental portraits might feel embarrassing, fake, or forced. Music, like photography, intersects art and business — most people get into it for the love of the art, but also acknowledge the need to promote themselves in order to make the financials work out — so bands are glad to have pictures to use in their own promotion.</p>
<p>I feel I am indebted to the city of Claremont for putting on the Friday Nights Live series of free outdoor concerts. I stumbled upon them in early August 2023, just days after <a href="https://www.youtube.com/watch?v=xN4Ev8AfV3E">publicly ruminating</a> over my lack of meaningful subjects to shoot. I decided to go for a walk through downtown after work, and heard the sound of music, and I've come back every concert since. I've met a lot of kind and encouraging people here, and it has brought me opportunities to shoot with some really great performers and receive positive feedback, not only from the bands but also from the regulars. On a few occasions, I have brought stacks of 4x6 prints of pictures I've taken of the dancers and given them to the dancers as gifts, and I've got some thank you cards in return.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/thankyou1.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/thankyou2.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/thankyou3.jpg"/></p>
<p>I made it my goal to attend every single Friday in 2024, and I stuck to that, but I don't think I will do the same in 2025. These last few weeks, my motivation to cull, edit, and publish has dropped like a stone. I think I am burnt out on shooting music for a while. Most of the bands in the Friday rotation I have seen two, three, or four times at this point. I don't want to keep shooting the same thing over and over again, and I don't think the bands need hundreds of pictures of themselves, either. I think I have hit a ceiling where my photos are not improving week to week and I am not learning. Partly this is because I am just an ordinary guest at these concerts, though a regular, with no particular authority or control. So when the band seats themselves in a way that is difficult to photograph, it is not my place to ask them to rearrange themselves, or pose, or move their lights and speakers out of the way. It's not a controlled shoot and while it's great to "challenge myself" looking for creative solutions to these problems, and the skills should be transferable to shooting in the real, uncontrolled world of event and street photography, it does put a limit on how good my photos can be that week.</p>
<p>Furthermore, I want to spend more time in crowds a little closer to my own age, not to disparage the crowd here which has been so generous to me. I will have to look for a new way to use my Friday evenings, though I am not sure how to break the news to the dancers.</p>
<center>* * *</center><p>A lot of my photos are not great. When I look back at the older ones, even and especially the orchestra concert that I said I was "very proud of" two years ago, I think a lot of them are downright terrible. I'll still give myself the excuses that it was a dark room and not under my control. If I was exclusively publishing my pictures online for the satisfaction and praise of random internet strangers, this would be a bit of a travesty. But because I prioritize sharing my photos directly with the subjects, I've quickly learned that even if my photos aren't great, it's possible they're the best they've ever had of themselves, and they'll tell me they love it. With the vast proliferation of cameras in the world, one might naturally assume that every single person has already been photographed to death, but the reality is you might be giving someone the best non-selfie photo they've ever had, and especially so for men. Sometimes I think about this and I worry it will make me complacent with mediocrity, but when I do look back at older pictures and I can see reasons they're bad, I know that means I must still be improving at least in taste if not in execution.</p>
<p>It's okay to keep some bad photos, anyway, for a few reasons. First of all, and maybe this is just a coping mechanism, I'll say that it's exhausting when every single picture in an album is trying to be the crown jewel. Like if every photo looks like it's trying to wow you, it comes off as tryhard. In narrative media like books and film, the audience is given breaks between the setpieces, and a photo album needs that too.</p>
<p>I think most photographers will agree that the real "wow" photos are rare; maybe one in a thousand or one in ten thousand in the age of burst shooting. I tend to think that my albums are a mixture of artwork and document. Yes I want to get some wows when I can, but I also want to document a place and event so people can see what it was like to be there. It's just not possible for every single one of those to be a wow, and that's ok because they still serve a purpose. It would be silly to delete everything but the wows and say yep, that's what I got out of tonight's performance: one picture, or maybe zero.</p>
<p>I get some wide-angled establishing shots of the environment and key characters before going in for the closeups. I'm not very good yet at making these wides artistic (more on that later) but they too are critical to the album which would otherwise be heads floating in space. Some basic facts-of-the-matter boring pictures set the context for what comes. And finally, sometimes I keep a boring or bad picture because it might be all I got of a particular person and I feel it would be disrespectful to erase them entirely. When people look at the album, they'll say "oh hey there's Susan!" and it will make them happy even if it's not an expert photo. Obviously if it's really bad it might be worse than nothing, but that's a judgement call.</p>
<p>In a music shoot, your favorite type of picture might be the lead singer belting out a big note, but I swear that you <strong>must</strong> get some pictures of them with their mouth closed, too, or else you make them look like their jaw's stuck open. Even the pictures that aren't your favorite still play an important grounding role in the album.</p>
<p>Yes, rules are meant to be broken. All that about composition, exposure, and focus are really just suggestions and you don't have to obey any of it. But nobody is unique, and it turns out that nobody is really even that different, and after 200 years of people taking pictures it's not surprising that we mostly agree certain things look good and others look bad.</p>
<p>Of course it matters who you're taking pictures for. If it's just a job to pay the bills, then the client's opinion is all that matters. If it's just a hobby for yourself alone then it's the complete opposite. And in reality most of us will be somewhere in between, hoping to be liked and recognized but without selling our soul.</p>
<p>Anyway, I think I actually do a good job, at least some of the time. I've got photos that still impress myself. I never feel like I'm doing perfectly, and that's partly because I keep getting myself into new types of shoots I've never done before. In that sense I am glad to be eternally amateur — it means I am not stagnating.</p>
<center>* * *</center><p>When you read my blog articles, not only about photography but also about <a href="/writing/obsessed_with_gpx">GPX recording</a> and <a href="/writing/notes_on_paper">document archiving</a>, for example, you'll see a recurring theme of recording and capturing things to be enjoyed in the future. The word "memories" comes up a lot in the discussion of photography, in particular. We're all out here capturing memories, aren't we.</p>
<p>I don't consider myself to be excessively nostalgic, and I really am not the type to earnestly use the phrase "the good old days", but if something is valuable and enjoyable today it'll probably be valuable and enjoyable in the future, too. I watch a lot of movies from the 70s and 80s not because I have nostalgia for a time I did not experience, but because the movies are well-made and still fun to watch — I'm sure there are just as many well-made movies happening today, they're just buried under a lot of junk and the great sieve of time needs to separate them out. But the fact of the matter is there are people I used to know and places I used to go that I cannot even picture anymore. I feel like I should do a better job capturing memories of these things.</p>
<p>Photography is a tool for timeless, high-quality art just as much as it is for personal/family memories. A studio portrait with dramatic pose and lighting might rank highly on the art scale, but not so high on the memory scale except insofar as you remember the creative process that inspired or brought you to it. A moment captured at a birthday party or baby shower might not be so artistic, but could be cherished as a family memory worth framing on the wall.</p>
<p>Other types of photography can fall elsewhere on these scales. When I shoot music I try to take photos that are good, artistically, but I also hope they evoke fun memories of a fun time, even if that means some technical misses. Street photography aims to be artistic while contributing to the history books of tomorrow with scenes of today. Contrast all this with product photography or promotional photography used in advertising, streamlined towards short-term financial interests, not carrying any emotional weight for anybody involved, except perhaps for the young fledgling model getting their first big break.</p>
<p>How much does this focus on "memories" or "the future" indicate a dissatisfaction with, or insecurity about the present? Is it the case that the time I spend now, capturing for the future, is not fully enjoyed in the present? I'll admit that when I'm shooting music, I'm not really listening to the music, and I might not be able to tell you which song was my favorite after the set ends. And in the nebulous future when I am looking back, will I be failing to enjoy <em>that</em> present for being too engrossed in the past? When exactly is the right time for all this? This is a lot of rhetorical exaggeration, of course — if we can waste a few hours today watching TV or scrolling the internet without an existential worry that we're failing to enjoy the present, then surely we can afford to spend a few minutes looking at an old photo. But as the stack of photos gets bigger, so too does the question.</p>
<center>* * *</center><p>We visited my grandfather in the hospital about thirty six hours before he passed away. I brought my camera knowing this could be the last chance for any pictures. My cousin gave him a shave using a disposable razor and a bowl of soapy water. At his wake, it came up: "did anyone take a picture of that?".</p>
<p>But I hadn't, and nobody else had either. I took a couple of pictures of the exterior of the hospital building on our way in, but I did not take a single picture inside the room for the two hours I was around. It's one thing to take a picture of someone with the thought of "you're going to die some day so I need to do this while I have the chance" echoing in my mind, but it's quite another when that person is literally about to die tomorrow and we both know it. He was still completely lucid and conversational, though struggling; he never did lose his mind. I imagined myself in his position: physically unable to perform a basic task and being made a photographic spectacle of so everybody can remember my moment of weakness for the rest of time, like a zoo animal for amusement.</p>
<p>I'm sure a lot of family members would have been happy to see that picture. I'm sure that if it had brought him any embarrassment, shame, or feelings of mortal fear, those feelings would have been erased the next day anyway. Funerals are for the living, as is everything. But I am one of those living and the feelings of that moment would have persisted through me. I didn't think it was a worthwhile exchange. By writing it here I am preserving that moment in another way.</p>
<center>* * *</center><p>In a lot of ways, photography is one of the "easy" arts. When an artist wants to make a picture in ink or paint, they have to sit down and actually <strong>make</strong> it appear on the page. Like, by hand. And the musicians I shoot demonstrate great skill with their instruments and voice, these skills acquired through many hours of practice and hard work. All I do is find a neat place to stand or crouch and press a button.</p>
<p>When I watch street photography videos I'll sometimes hear the phrase "making photographs", and I really dislike it. I know I'll sound like a jerk, but this phrase makes me think less of the person saying it. Especially in the context of street photography where most pictures are candid, and even the non-candid ones are only minimally posed from how they were found, pressing a button on a magic box for 1/250 of a second seems like an exceptionally low bar to be called "making" anything, and I feel it discounts the effort that goes into most other forms of making. I think the phrase "making a photograph" only sounds acceptable in a fully controlled environment where every element of the scene is deliberate. Otherwise, just admit the fact that you're taking what is given to you, and say "taking".</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2022-09-10_13-15-42.jpg"/></p>
<p>I put a tally on the bottom of my portfolio that adds up the exposure time from all my pictures according to the EXIF data. With over 3,000 published photos, I'm coming up on just 40 seconds, and 10 of that was light painting long exposure.</p>
<p>This tally serves two purposes. The first is the willful self-deprecation of photography as the easy art that doesn't take any effort, so that's out of the way. The second is a reflection on the strange feeling that I get when I think about the nature of photography, and how it locks in a particular millisecond out of the 86,400,000 milliseconds that occured that day to be preserved for all time. What made that one so special? The moments captured by a typical photograph are so brief that we truly have no other way to perceive them. When I am talking to someone face to face, I can see their gaze shift and their brow wrinkle and their mouth move and their hands gesture, but I never see the totality of their person at once because human vision is still limited by <a href="https://en.wikipedia.org/wiki/Foveated_rendering">foveated rendering</a>. When you look at a person, you never really see the whole person. But the camera captures the whole thing, all at once, for a level of scrutiny not available through any other means. It's pretty weird if you think about it.</p>
<h2 id="originality">Originality<a class="header_anchor_link" href="#originality"> (§)</a></h2>
<p>This section was drafted after I watched one of Paulie B's videos with Reuben Radding where they talked about <a href="https://www.youtube.com/watch?v=MFSVsDrrkX8&amp;t=9m14s" title="How to be original in street photography -- Office Hours with Reuben Radding">how to be original in street photography</a>. I thought I would publish this as its own article, but let's just lump it in here. I like when these articles get long.</p>
<p>There is a beautiful and illuminating irony in the act of submitting the question "how can I be original?" to a Q&amp;A session like this. Surely once you're on the internet and you have a search bar under your fingertips, you're able to find hundreds of thousands of articles, youtube videos, think pieces, and motivational speeches on how to be original from people who think they're qualified to tell you. Instead, you choose to submit it into a Q&amp;A for one particular artist to answer, forcing them to retread a territory so often trod before them, definitively unoriginal, not because their answer is the only one available to you, but because you respect that particular artist's opinion over the opinions of others, and hearing what <em>they</em> think is more important to you than hearing what just <em>someone</em> thinks. And now here I am being reached and influenced by it, instead of all the other opinions out there from other people that have never reached me. We've blown so far past full circle we're coming up on spherical.</p>
<p>Turn this around on yourself and you'll see what I mean. You want to hear Reuben's opinions because you like him. So if someone likes you, they'll want to see your work and hear your opinions, and they will not care if you're being "original". Ta-da, it's that easy.</p>
<p>The internet allows us instant access to many of the best works ever made in every category, and if you compare your work against the greatest of all time, of course you're going to feel like you're behind. But I'm going to keep hammering this same point over and over again: when you give the photos to the group of people that actually matters instead of trying to impress internet strangers, all that infinity of better works vanishes in a puff of smoke. The notion that Winnogrand took better pictures is preposterously irrelevant when you have the opportunity to give someone pictures of themselves and their friends, which no Winnogrand can do for them. How unoriginal!, they'll surely complain.</p>
<p>There are eight <a href="https://www.youtube.com/watch?v=HZmafy_v8g8">billion</a> people in the world, and every second that passes in your life is eight billion seconds passing for everybody else. Everybody is missing out on 99.999% of everything that happens at all times. So even if you take a picture that you feel is trite and cliche, someone will say it's the first time they've seen anything like it.</p>
<center>* * *</center><p>I'd like to specifically comment on the originality of travel and architecture photos. There is a certain kind of light mocking people get when they take a picture of a famous place. "Could have saved yourself the effort and taken one from google images, and it would look better too".</p>
<p>The <a href="/writing/hobby_photography_2/camera_restricta.html">Camera Restricta</a> is an art/tech project that encourages creative photography by physically disallowing you from taking a picture in places where too many pictures have already been taken.</p>
<blockquote><p>The project is not only a piece about censorship in a political sense, but also questions our photographic practice. With digital photography displacing film, taking pictures has essentially become free, resulting in an infinite stream of imagery.</p>
<p>Camera Restricta introduces new limitations to prevent an overflow of digital imagery. As a byproduct, these limitations also bring about new sensations like the thrill of being the first or last person to photograph a certain place.</p>
</blockquote>
<p>The Camera Restricta is itself a creative work, and I'm not here to criticize it. They're not saying every camera should behave this way, but it's a deliberate limitation you can give yourself by wielding the Restricta. Taking on deliberate limitations is an important part of being an artist. But the idea that "too many people have taken this picture" and "this has been done before" is what I'm scrutinizing.</p>
<p>Taking pictures of a place that has been "done before" is ok, because change over time is imperceptible. If you say that today is not different enough from yesterday to warrant a new picture; and tomorrow is not different enough from today, then you'll have to prescribe some minimum interval that you think is allowed — and I might disagree with you. If you wait for change to happen, you'll miss it.</p>
<p>This kind of rule only works because the person issuing the rule knows that people will break it. Where the heck do you think the pictures on google images came from? They're not mocking the idea of <em>someone</em> taking a place photo, they're just mocking the idea of you taking it. If everyone did follow the rule, and everyone diligently stopped taking pictures of the overdone place, there would come a day when the building is demolished and the most recent photos are decades old. For what benefit? Just a perception of originality, or not being a sheep.</p>
<p>You may have heard the phrase "if you outlaw guns, only outlaws will have guns". The type of people who obey laws in general are probably more responsible, cooperative, social, and less likely to commit crime, but the type of people who commit crime don't care if guns are outlawed because they're already committing crime anyway. So then the responsible people are less able to defend themselves, is the point. I feel that the same kind of effect can apply to creativity as well. If there's a supposed rule about pictures you're not allowed to take, it is more likely that the thoughtful and considerate people will obey the rule while the self-centered "don't tell me what to do" types will ignore it. Which one would you rather see pictures from? As another example, if you convince all photographers that candid street photos are immoral and should be stopped, you'll be left with nothing but Bruce Gildens who don't care.</p>
<h2 id="what_if_everyone_just_has_poor_taste">What if everyone just has poor taste<a class="header_anchor_link" href="#what_if_everyone_just_has_poor_taste"> (§)</a></h2>
<p>Wizards With Guns made a sketch that gave me some long-lasting anxiety. <a href="https://www.youtube.com/watch?v=AxE04GId49s">"I seduce the dragon"</a> is about a group of DnD friends having a laugh before their game. It's a feast of hyper-wholesome bro-love moments that'll make your friendships look pallid by comparison. On one hand, it's nothing but great that bros can get together and reinforce one another. But on the other hand, the backbone of the entire sketch is that they're obviously super lame. Too easily impressed, too exaggerated in praise ("that belongs in a museum", "you should write a book", "you should open a bakery", "you should be an actor"), too deep into their uncool hobby. The characters are played with, to put it indiscreetly, speech patterns and mannerisms associated with being mentally retarded. There's no other way to say it. Since this is fiction and <a href="https://youtube.com/watch?v=AxV8gAGmbtk" title="Folding Ideas - The Thermian Argument">everything in fiction is there because the authors wrote it on purpose</a>, this suggests that Wizards With Guns believes positivity and uplifting compliments are correlated with being retarded. The characters in other WWG sketches don't talk like this, if that counts for anything. At the very least, we can be sure that positivity correlates with having poor taste.</p>
<p>One of them busts out some drawings that blows the others' minds:</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/wwg1.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/wwg2.jpg"/></p>
<p>Is that me? I hope that's not me.</p>
<p>The comments on the video are, rightfully, very positive themselves:</p>
<blockquote><p>Need more sketches where it genuinely feels like the joke is on me. Watched 6 minutes of cringe goofballs and in the end was reminded that they don't care, they love each other, and they're happy. I'm the fool. 10/10</p>
</blockquote>
<blockquote><p>I'm almost glad there wasn't a punchline - these characters deserve to be happy</p>
</blockquote>
<blockquote><p>The whole time I had this sense of unease, "When are these harmless goofs gonna have the joke put on them?," but then it never came. Instead just humanity</p>
</blockquote>
<blockquote><p>20/10, I seriously expected a brutal dunking to occur, or for something to go wrong, but instead it’s bros being bros, and loving each other. It hits me right in my softest bones</p>
</blockquote>
<blockquote><p>These guys are true homies. They Gas each other up, rally, and encourage each others potential. This is beautiful.</p>
</blockquote>
<blockquote><p>This was the most stressful video I've seen in awhile, I just kept waiting for the punchline and it never came. So wholesome it hurts</p>
</blockquote>
<blockquote><p>Is it cringe if everything they said made me feel genuinely happy and nostlagic? thats the life man, thats the goal. Its a great phase in your life, being cringe is mandatory</p>
</blockquote>
<blockquote><p>Even if this is super cringe it's also peak friendship goals. So much positivity.</p>
</blockquote>
<p>Because of course we all wish we could be so unabashedly happy with ourselves. But I find these comments inherently patronizing. These comments only exist because the commenters enjoy the perspective of an external observer, and comfortably perceive the characters as being more lame than themselves. If the characters in the skit were a bunch of sexy people having a good time, nobody would feel the need to congratulate them on "not caring", because of course what are you implying there is to care about? It is only by virtue of the fact that lame people having fun is considered not normal, and that we imagine we'd be unhappy with ourselves if <em>we</em> were that lame, that it is even worth mentioning. It's like looking at an ugly couple and saying "I'm so glad you found someone that thinks you're beautiful".</p>
<p>Then the video ends, and you go back to your own life, and you assume that you are not that lame, and when you receive a compliment on your work you assume the giver is not retarded. But what if you're on the inside of the joke, and the external observers are patting you on the back, congratulating you on "not caring" despite how lame you are.</p>
<p>The things I'm saying here are not very nice, so I hope you realize I'm writing this because I'm thinking about it and struggling with it, not because it's the right way to think.</p>
<p>By the way, WWG is great. <a href="https://www.youtube.com/watch?v=SAhTpJmzcog" title="TV shows where the detective has a cool gimmick">Here's my favorite video by them</a>.</p>
<h2 id="street_photography">Street photography<a class="header_anchor_link" href="#street_photography"> (§)</a></h2>
<p>I am still interested in the genre of street photography. But, like I said last time, the area around me is not good for it, so I don't even try at home. I've been making trips to Venice Beach, where I serendipitously discovered the Sunday drum circle, and that's been where I've got a lot of my favorites.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2024-07-07_19-47-35.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2024-07-28_19-02-50.jpg"/></p>
<p>Throughout this article you'll see that I'm mentioning <a href="https://www.youtube.com/channel/UCAS6u29fObpCHYgHQCGaSUQ/videos">Paulie B's</a> Walkie Talkie series a lot. On one hand you might say it's shallow or narrow-minded to rely so heavily on one source for influence, and I agree with that. There's more to street photography than the NYC Leica/Portra gang. But on the other hand, Walkie Talkie is probably the single best thing to happen to street photography in a long time and it deserves more recognition than I can give it.</p>
<p>Looking at the older videos on Paulie B's channel, you can see he was mostly doing POV snap-and-dash street photography. With all due respect, I'm glad he's moved away from that. Walkie Talkie, documenting the thoughts, motivations, and inspirations of a community of artists, is so much more important and meaningful than collecting yet another photo of a random dude smoking a cigar.</p>
<p>When we talk about the philosophy of street photography, we often say we are "finding beauty in the ordinary" or "capturing life's simple moments", or something like that. It sounds very pretty and noble, but I think it's worth asking if this is more than a bit disingenuous, considering that the hotspots for street photography as a genre are some of the most busy and exciting places in the world, NYC being the American epicenter, Tokyo another. In July 2023 I made a POV street photography parody video called <a href="https://www.youtube.com/watch?v=xN4Ev8AfV3E">YTSPPOVVLOG #49</a>. It's pretty hilarious if I do say so myself, but the humor comes from a place of deep personal dissatisfaction. You can't convince me there is beauty in this ordinary.</p>
<center>* * *</center><p>Street photography is somewhere between art and documentary. Well, perhaps everything is. We're looking for compositions, expressions, and gestures that are artistically interesting to look at, but also capture the facts of the present that will distinguish the photo from future and past. The architecture, fashion, transportation, commerce, and public events of today will someday be studied as "the way things used to be".</p>
<p>But documentary isn't really documentary if it only captures the pretty parts, is it? If you want to show "what life was like" at this time and place, you'll have to capture the bad parts too in order to do the story justice. Homelessness, drug addiction, conflict, hate.</p>
<p>Everybody wants documentary to exist, but not everybody wants to experience it happening. We as the public want to be able to read about and see the hard times that happened in the past so that we can avoid them in the future, but nobody wants to personally be that face of homelessness or poverty or addiction; to be spotted at a low point in life, captured, and pasted up for everyone to see.</p>
<p><a href="https://en.wikipedia.org/wiki/Migrant_Mother"><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/Lange-MigrantMother02.jpg"/></a></p>
<p>Respectful and empathetic street photographers recognize that even with the legal right to photograph anybody in a public place, it's just not polite to take advantage of struggling people for the benefit of your "portfolio". We can say the same about people struggling with obesity, disability, or other physical differences.</p>
<p>Does this lead to erasure? Erasure of homelessness, addiction, obesity, and the wheelchair-bound? Isn't it wrong to sweep under the rug everybody that may not enjoy being photographed, to preserve their fleeting feelings of shame at the expense of everlasting documentary authenticity, to falsely portray a world in which everybody is attractive and clean and well put together, instead of the real world?</p>
<p>...probably not. This line of thought is tempting, but before you are hooked and reeled in, you should ask yourself if you're really just being a slacktivist. These days (and in the past, but <a href="https://en.wikipedia.org/wiki/Mitch_Hedberg">these days, too</a>) a lot of us want to feel like we're doing something important, but don't actually want to put a lot of effort into it. There are actual documentarians out there dedicating their full-time careers to researching, interviewing, working with organizations for access, and spending substantial amounts of time with their subjects before publishing their faces. You can watch a lifetime supply of these on <a href="https://old.reddit.com/r/documentaries">/r/documentaries</a> or <a href="https://documentaryheaven.com/">documentaryheaven.com</a> and elsewhere. Pointing your Fuji in all directions and pressing the magic button is an awfully low bar to call yourself a documentarian, and it's not exactly a great oppression to say that you should leave the more sensitive subjects to the people who are willing to put in the work and do it right. Despite what I said in the Originality section about doing your thing in spite of a saturated genre, it might actually be a good thing to stay out of the genres that take advantage of others' hard times.</p>
<center>* * *</center><p>There are lots of arguments on the internet about whether a photo must be candid in order to be called "street photography". If you ever ask someone for permission to take their photo, it is no longer "street photography" and instead it is "street portraiture", or at least that's what people will tell you.</p>
<p>To me this is a lot of pearl clutching. It is like arguing whether the word "guitar" inherently refers to an acoustic or an electric, and forcing the other group to specify that theirs is the non-default. A power play over language rather than an actual benefit to the art. A fight between two groups over who gets to own the implicit adjective. This whole argument goes away if you just specify "candid street photography" when it matters, and also if you recognize that it doesn't usually matter.</p>
<p>When I do street, I like to be somewhere in between. I think some of my best pictures come from people that I was trying to get candid, but they noticed the camera and hammed it up.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2024-07-28_18-44-41.jpg"/></p>
<p>Do you want to call these "street portraits" simply because they are technically not candid at that particular moment? I didn't give them any instructions and oftentimes I don't say anything at all — after I get the shot I put the camera down and smile, wave, or give a thumbs up, and usually that's the end of that unless they want to talk or exchange phone numbers to get a copy. They were already there doing their thing, and they shared a second of it with me. That counts as street photography as far as I'm concerned. All I'm saying is this terminology debate and dogmatic obsession with candidness is not helpful so let's just stop.</p>
<p>When we talk about street photography, we spend a lot of time talking about people who don't want to have their picture taken, and how to be respectful about that. I myself don't enjoy being on the wrong side of the lens very much. With all of these preconceived notions coming from discourse on the internet about unwanted photography, it is a genuine surprise when, out of nowhere, a stranger calls out to have their picture taken (shown below), or when the basketball players say "keep the pictures going, we like that". So if you're interested in doing street photography, but don't want to come across as predatory, creepy, or invasive, you should give it a try and you might find plenty of willing subjects. You just won't come home with quite as many photos as the ones who snap and dash.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2022-08-28_19-07-20.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2022-09-05_17-58-37.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2024-05-26_15-58-24.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2024-08-08_14-08-59.jpg"/></p>
<h2 id="privileged_eye_outsider_eye">Privileged eye, outsider eye<a class="header_anchor_link" href="#privileged_eye_outsider_eye"> (§)</a></h2>
<p>Taking pictures of people is easier when everyone is comfortable with the photographer's presence, and that comes from trust. Trust that through choice of angle, lighting, and shot selection, you'll show everyone looking good. When you're capturing individual milliseconds of someone's facial expressions, even a supermodel can be made to look bad, but it's ok because I won't make you look bad.</p>
<p>I think it is worth reflecting on this privilege. I am the one that is allowed to see the derp faces, the sneezes, the unflattering angles of rear ends suspended in the motion of dance. I am the one with the magic box that I can point in any direction and freeze you doing whatever you're doing. I am allowed to spend as much time as I want looking at these moments, locked in time, that nobody else will ever see... And I delete them.</p>
<p>Perhaps this is not such a meaningful privilege, since everybody knows that if you pause a video of a person talking you'll almost certainly freeze them looking derpy. By publishing a video of themselves speaking, the host has already granted every viewer the ability to see their weird intermediate faces.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/derpface.jpg"/></p>
<p>But I don't want to halt this train of thought just yet.</p>
<p>For this reason, I have difficulty photographing people that I think might not be happy to see themselves in pictures at all, even if they don't say it out loud. Bluntly, I'm talking about people who are overweight or have a physical deformity or maybe just don't look that great for some other reason. These are the kinds of things you're not supposed to say because it's not polite. It would be great if I could say that I'm just capturing reality and I'm not here to judge, but that isn't true. I have to judge. By mere virtue of the fact that I'm taking 500 pictures and delivering 50, it is inherently the case that I have to use my judgement to decide which of your smiles and facial expressions you'll be most satisfied with and which ones you won't. Sometimes my judgement tells me that you will not be satisfied with any of the pictures I've taken of you, so what do I do? Where is the line between projecting my standards onto someone else and thoughtfully applying the <a href="https://en.wikipedia.org/wiki/Golden_Rule">golden rule</a>? My trustworthiness and privilege are on the line here.</p>
<p>One easy answer is that you should always get everybody's consent before taking their picture. We've been over street photography so I'll skip that kind of consent here. But even in an event photography setting, getting consent is not so clear. Either you're pre-surveying every attendee and memorizing their answers which I would say is quite infeasible, or you're asking them on the spot. And if you ask them on the spot then their answer is contaminated by peer pressure from those around them.</p>
<p>You can say I am way totally overthinking it, and that my implication that a person could possibly be ugly is itself the most inappropriate and offensive part of my thinking, but I don't have any way to exist that does not involve overthinking. Either I publish the photo or I don't, and I have to have a reason either way. It's either that, or you imply that the final selection of photos is made without reason. This is not an imaginary problem — I have specifically heard people say they don't like having their picture taken or don't like seeing themselves in pictures. And since we don't have all of our insecurities written on our sleeves, it must be the case that more people feel this way than will openly say it, and it has to be my judgement call.</p>
<p>Another easy answer is to say let's just stop taking pictures entirely, to avoid offending those who don't like it. But on the other hand, plenty of people absolutely love being in pictures, and we know definitively that people want to have these memories, so we're not going to let this be the death of photography, either. Can't be walking on eggshells all the time.</p>
<p>To some extent I have to operate on the assumption that since you wake up and see yourself in the mirror every day, you're already used to how you look even if you don't, per se, love it. It's not like the photo is going to be a surprise to you. It is altogether worse to be the subject of erasure — as if you were never there at all — than to be shown as less than perfect.</p>
<p>This is why I'm taken aback when I see street photographers publishing what I would say are unflattering pictures of strangers. I've been describing event photography where there is some level of obligation to include and represent everybody. But when photographing complete strangers on the street, I don't see a lot of artistic merit in getting people in unflattering positions, especially if the composition, framing, and background management are otherwise completely uninspired.</p>
<p><a href="https://www.youtube.com/watch?v=7LwnTdrX9Vg&amp;t=2m36s"><img alt="" loading="lazy" src="/writing/hobby_photography_2/unflattering.jpg"/></a></p>
<p>I think one of the reasons candid photography appeals to me, and perhaps other creative types, is that I've always been a behind-the-scenes kind of person. There's a reason movies come with special features, directors commentaries, and behind-the-scenes footage: people want to see what goes into the preparation and production of the main feature; they want to see the things that only the exclusive set of cast and crew got to see. Just as a boom operator does his best to move invisibly across a set, unseen and unknown but crucial to the production of a great work, I try to move invisibly through a crowd and produce great work of my own.</p>
<p>But there is a double edge here. Being behind the scenes and invisible is both a good and a bad thing. When I place myself in the center of a crowd of dancing people to take their picture, you know what I'm not doing? Dancing with them. I am certainly no good at dancing anyway — guilty feet have got no rhythm — but I still want to engage and socialize when I go out, and I'm finding that this doesn't work very well after I've established myself to the crowd as the candid photographer. Once you've trained them to let you get close without drawing their attention, it's hard to get their attention. Oops. And when I'm photographing my cousin's baby shower or my employer's Christmas party, I'm undoubtedly doing less socializing than most of the other attendees. I'm fine with that because I have more fun taking awesome photos than I do socializing, most of the time. But everybody knows that the photographer usually has the fewest appearances in the final album.</p>
<p>The camera has demonstrably brought me quite a lot of positive interactions with people I would otherwise not have met, and by sharing my photos with everyone my rolodex has quickly grown. But I have come to recognize that being the privileged eye often comes with being the outsider eye, and if I want to go out with the express purpose of meeting people or making friends, it probably has to be sans camera.</p>
<p>And don't tell me I'm artistic.</p>
<h2 id="videography">Videography<a class="header_anchor_link" href="#videography"> (§)</a></h2>
<p>Photography and videography seem pretty similar to most people outside of the hobby, but wow they are completely different skillsets. Some bands have asked me to shoot video for them, and of course I can't say no. Video is a lot harder because every little bump, wobble, or tilt of the camera is recorded permanently, and you can't just delete the bad milliseconds like you can with stills, so the stakes are a lot higher. The temporal element of video means you have to do the right things at the right time in the right order, whereas with stills you can play fast and loose with the presentation order if you so choose. Video is also more difficult for a few technical reasons. For example, your shutter speed can't be too fast or too slow, especially if you want to stick to the traditional <a href="https://en.wikipedia.org/wiki/Rotary_disc_shutter">180 degree shutter</a>, so you lose access to one third of your exposure triangle.</p>
<p>Shooting photo and video as a hybrid is kind of stressful for me because whenever I'm in video I feel like I'm missing out on good photo moments, and whenever I'm in photo I feel like I'm missing out on good video moments. And if I start a video even a few seconds too late and miss the beginning of a song, I feel like the whole thing is soiled, so I'm constantly on edge trying to figure out when to switch. And then the whole time I'm not sure if I'm getting the best angle or focusing on the best person.</p>
<p>One of my biggest sources of music videography inspiration is Naver Onstage which took (RIP) a very mobile approach to camerawork. The camera operator basically <a href="https://www.youtube.com/watch?v=SmTRaSg2fTQ">never</a> stops <a href="https://youtube.com/watch?v=3SBb0LbvrrY">moving</a>, breaking into and out of the artists' personal space and sometimes eliciting their hammy reactions. Unfortunately this is REALLY HARD to do well when you're hand-holding the camera without any stabilization equipment. And it's also really hard to point the camera at the right person at the right time when you don't know how they're going to move and don't especially know when they're going to do a solo. I've come to appreciate the value of a wide, static vantage point which lets the viewer focus on whoever they want the whole time.</p>
<p><video controls="" poster="https://files.voussoir.net/photography/2024-03-17 HotHead St Patricks Day/small_2024-03-17_17-33-35 Creep.jpg" preload="none" src="https://files.voussoir.net/photography/2024-03-17 HotHead St Patricks Day/2024-03-17_17-33-35 Creep.mkv"></video></p><p><video controls="" poster="https://files.voussoir.net/photography/2024-06-15 Van Sutton/small_2024-06-15_21-28-41 Drum Solo.jpg" preload="none" src="https://files.voussoir.net/photography/2024-06-15%20Van%20Sutton/2024-06-15_21-28-41%20Drum%20Solo.mov"></video></p><p><video controls="" poster="https://files.voussoir.net/photography/2024-08-16 Craic Haus/small_2024-08-16_19-23-00 Drunken Sailor.jpg" preload="none" src="https://files.voussoir.net/photography/2024-08-16%20Craic%20Haus/2024-08-16_19-23-00%20Drunken%20Sailor.mkv"></video></p><p>I am still learning what works and what doesn't. I try not to rule something out until I give it a shot. One thing that I have tried several times and almost always regretted is turning the camera away from the band to get the audience reaction. In my stills, the audience photos are often some of the best, because I think it's important to show the effect the band has on their crowd — that's the whole point of live performance. But I don't like the way it translates to video for a single-camera shot. In the moment, it always seems like an important thing to do, but when I watch the video back again I always feel like I missed out on seeing whatever the band did during those seconds, or else the whip-pan across the audience is so hurried as to not miss out that it winds up nauseating instead. I think audience reactions are better done with multiple camera operators. The exception to this is when you can get an angle from beside or behind the band so you get both at once.</p>
<p>Oh and the gigabytes. So many gigabytes.</p>
<h2 id="money_and_recognition">Money and recognition<a class="header_anchor_link" href="#money_and_recognition"> (§)</a></h2>
<p>One of the biggest reasons I do photography at all is because I want to make connections with people. Most of the photos I take are given away for free because it makes people feel good and doesn't cost me anything except some time.</p>
<p>The Friday night concerts have become my <a href="https://www.youtube.com/watch?v=VvdQ381K5xg" title="Not Just Bikes - The Great Places Erased by Suburbia (the Third Place)">third place</a> for what feels like the first time in my life. I know the names of maybe three dozen regulars now.</p>
<p><audio controls="" src="/writing/hobby_photography_2/nationalgeographic.flac"></audio></p><p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/cards.jpg"/></p>
<p>In July, I finally got cards made. Throughout the month of June I got more "do you have a card?"s than ever before. And I hesitated for a long time because I still don't <em>really</em> want photography to be a business or a hustle for me, and I don't want to give the impression that it is by handing out cards. But I finally went ahead for two main reasons:</p>
<p>First, because people are literally asking me for it. They want to see my photos and I want to show them. If I were handing out cards to strangers unprompted, it would be spam and solicitation, which I want to avoid. But if they're the ones asking me for it, and I tell them "I don't have any cards because I don't want to give the impression that I'm soliciting for business", that would really make the interaction more weird than just "yes, here you go". It really is so much faster to hand someone a card than to exchange phone numbers verbally or have them scan a QR code off of my phone screen, which they will lose in their browser history and ask for again next week. Ask me how I know.</p>
<p>Second, even though photography is not my hustle and I don't solicit for business, I'm not going to turn down cool opportunities that present themselves to me. If someone asks to hire me and it sounds fun, yeah I'll take them up on it. I keep telling myself this isn't what I came here to do, but if I stop pushing my boundaries, I'll stagnate. More outings, more new experiences, more connections with more new people. <a href="https://en.wikipedia.org/wiki/Yes_Man_(film)">I'm not just gonna say no</a>. One week before this publication date, I've just shot my first wedding — something I said from the beginning I would not do — and I think I did just fine.</p>
<p>One boundary I'd like to maintain is I don't want to take a job that prevents me from self-publishing the pictures to my website, unless it's something like a private party. For anything in the more public, performative, or documentative sphere, exclusivity and blackholing my work into somebody else's pockets are not in line with my ethos.</p>
<p>The fact of the matter is the money I get from paid shoots is not a big deal to me. My day job pays me well enough and I am not relying on photography for income the way categorical professionals do. I also don't want to charge too much because I'm not really confident I'll do a good job, especially in a difficult shooting environment like a dimly lit bar, or for a type of work I've never done before. I can barely trust myself to shoot street on a sunny day because dynamic range is hard. And finally, everybody knows that musicians are starving artists, probably not making tons of money from a gig in the first place. I have heard first-hand from bands that they're not getting paid by tonight's venue, or maybe it's only $100 a head. Without sounding too magnanimous, I don't feel like I need to take too much from them.</p>
<p>Does it hurt professional photographers when people like me are out there charging hobbyist prices? I can imagine the answer is yes. But part of the question is whether these bands would have sought a professional photographer in my absence. The reality I'm seeing so far is that a lot of local bands don't have many good pictures of themselves because they've never hired a photographer before. And I'm not soliciting them for business, they're the ones asking to hire me. What this tells me is they had a latent hunger for photos, and are willing to pay for them, but never reached out to hire a professional until I (an amateur) fell into their lap.</p>
<p>I am normally quite critical of any rhetoric that speaks of preserving jobs. The example I've used before is the <a href="https://en.wikipedia.org/wiki/Switchboard_operator">telephone switchboard operator</a> — sometimes jobs become obsolete, and although I'm sure the switchboard operators were unhappy to lose their job, life goes on. So if amateur photographers can run around doing shoots for free or cheap and professionals go out of business, that might be seen as the free market at work. The rebuttal is that professionals will deliver pictures of significantly higher quality in terms of lighting, posing, composition and blocking, and tasteful editing, and that this is all worth paying for. The counter-rebuttal is that the majority of the population are normies without taste, and they will be satisfied by amateur pictures just as they are satisfied by sloppy fast food, and they the market will not bear the cost of higher quality work. Sometimes I give people pictures that I think are lousy because that's all I had available, and they tell me they love them. As an agent in this market, I can accelerate this effect by doing a mediocre job for a mediocre price. But since I do have taste and can appreciate that professionals make better work than me, I'll stay out of their way and not massively undercut them all the time. This is how I defend that I'm not hypocritically giving a job-preservationist argument, but rather this is an argument for preservation of quality in craft. Maybe the same could have been said for the switchboards, but it would have been somebody else's position to take.</p>
<p>When I attend and shoot an event on my own accord, I'll give away all the photos for free, but when someone's specifically asking me to do it, it seems essential that I charge at least a bit. As much as I like giving things away, it has to be clear that it's my decision. I don't want people to think they can call me out for free work whenever they want.</p>
<p>Well, luckily, that hasn't been a problem so far. More commonly, people are handing me money that I never asked for. One of the bands I shot gave me the contents of their tip jar, unprompted, and another caught up with me on a later Friday to give me a <a href="https://old.reddit.com/r/thathappened">hundred bucks</a>. More than once, I've taken a candid picture of a dancer or audience member, and given it to them, and they've insisted on tipping me. My instinct is to decline — I want to tell them that art is better when it's free. But I also feel that if I decline the tip, I am denying them from expressing their side of the interaction, forcing things to always go my way. In a way, it is more selfish to deny the tip than to accept it, because it comes off as virtue signaling or magnanimous or like I'm the only one who's allowed to be generous.</p>
<p>The most surprising variation of this is when a complete stranger emerges from a crowd to start this exchange:</p>
<blockquote><p>Them: Could you take a picture of us?</p>
<p>Me: Sure</p>
<p>Them: How much?</p>
<p>Me: ...Nothing?</p>
</blockquote>
<p><audio controls="" src="/writing/hobby_photography_2/2024-05-24_19-18-30.flac"></audio></p><p><audio controls="" src="/writing/hobby_photography_2/2024-07-28_16-00-00.flac"></audio></p><p>These ones have been really surprising to me. The usual narrative from people online is that no one, not even, or perhaps especially not the ones who can afford it most, want to pay for the commodity that is photography when everybody has a camera in their pocket. So I never would have expected that random people would suggest paying me for a photo on the spot. It's not worth me taking your money for that, you can have it for free, guys.</p>
<p>Now, maybe there's actually nothing to this. Maybe if I had answered anything other than free they would have rescinded their request. Even for a dollar, five dollars?</p>
<h2 id="zooming_out">Zooming out<a class="header_anchor_link" href="#zooming_out"> (§)</a></h2>
<p>Telephoto makes everything look good. Too good, maybe. I'm trying to get better at embracing wider focal lengths so I can capture more of the subject's context and environment rather than a dramatic eye-candy closeup.</p>
<p>One of the reasons I struggle with zooming out more is my perception of the image size on the viewfinder or rear screen. Because these screens are physically small, I zoom into the subject more than I should so that they are nice and prominent on the little viewfinder. Then I get home and look at the image on my 27" monitor and WOAH that's too big. These pictures usually get deleted and I miss some good ones this way.</p>
<p>It's like I'm afraid that if I don't zoom in enough, the subject will be too small in the image, even though I already know from experience that 26mp is enough to capture individual eyelashes from a surprising distance and there is absolutely no reason to worry about this. I need to trust my equipment and my ability to judge focus without overfilling the frame.</p>
<p>Here's an example of my problem. This is a cool guy doing a cool thing; I'm happy with the timing of the shot and I like how the sunlight shows off his muscles but silhouettes his face. But the crop is too tight — without seeing his feet and the ground below, you can't get an appreciation for how high he is.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/2024-04-28_16-46-44.jpg"/></p>
<p>I came back a month later to try again. That's better.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/2024-05-26_17-58-24.jpg"/></p>
<p>The other reason I struggle with wide angles, especially in black and white, is that I feel like the picture just turns to noise. One reason I go for a wide angle is to capture a large crowd. I want the viewer to see how many people were at this event. So far I have not done a good job and I think these photos are pretty terrible to look at. But I keep a few of them because that's all I've got so far.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2024-06-14_18-58-21.jpg"/></p>
<h2 id="website_portfolio_gallery">Website, portfolio, gallery<a class="header_anchor_link" href="#website_portfolio_gallery"> (§)</a></h2>
<p>Currently, <a href="https://voussoir.net/photography">my website</a> is a simple chronology of albums and a few single photos, newest first. But sooner or later I'll have to come up with a redesign. Already there are so many pictures on the page that it can be problematic scrolling through.</p>
<p>Because I might have redesigned the page by the time you're reading this, <a href="/writing/hobby_photography_2/voussoir_net_photography.html">here is an abbreviated snapshot of the homepage</a>. Each album shows a couple large highlights, and then a bunch of tiny thumbnails. The highlights are direct links to the jpg file, but the tinies are linked to the album with an anchor to that photo. I came up with this design because I wanted every photo in the album to be immediately visible without clicking into the album, lest they be never seen at all. Maybe the highlights I chose don't speak to you, but something in the tinies catches your attention. And also because I want to give everybody representation even if their picture didn't make the highlights. Once you click into the album, every picture is big.</p>
<p>Most of the people I meet at the Friday concerts are, well, older than me. As the kind of person that writes my HTML by hand and encourages scraping/downloading, I am deliberate in using simple anchors and img tags instead of obscuring the tags behind lightboxes and other javascript dynamism. But now I have seen firsthand the way my page confuses people. They tap on a highlight to see it bigger, and it goes to the direct image file (16 megabytes, sorry data plan!) and they have to use their browser back button to get out. And when they tap on a tiny, it doesn't bring them to a direct image, but still they have to use their back button to get out of the album and back to the homepage so they can see the next album. And although I'm trying to show off a couple highlights and move on to the next album, they want to see <em>every single picture</em> so they pinch-zoom on the tinies which is not a good UX. Oh yeah, did you notice I'm only using phone verbs here (tap, pinch) because this is all happening on their phone and of course these pictures, especially being mostly horizontal, look a lot better on a computer monitor in the first place.</p>
<p>The design of an archive like this is challenging. If it were just a list of links with no pictures, there'd be nothing for the viewer to judge what they might be interested in clicking on. But if it's a complete dump of every single picture on one page (like I'm doing now), then it becomes difficult to scroll through. And if it's just one or a few highlights with a "click to see the rest" link, then I feel guilty for anyone who didn't make the highlights because it says they weren't important enough.</p>
<p>I have already made my case for keeping some bad photos as part of an album, but that doesn't mean they need to be front and center as the first thing a new visitor sees. I don't feel the need to play that off as some kind of rawness or authenticity. I think I should come up with a way to show off my bests or favorites, but I don't want those to become stale while new stuff is coming in, either. I am also thinking that there will come a day when I shoot a more significant project that deserves to stay at the top of the page for a while instead of immediately falling off the chronology under the next band shoot. No offense, bands.</p>
<p>If I redesign the front page to only show a smaller selection of photos, I will always provide some kind of index/archive page with a complete list of everything. If, for some reason, you want to scrape my site and download every photo I've published, I want it to be easy for you to do that. Because that's exactly what I do when I find something I like, too. This is in stark contrast to most other photographers' websites which deliberately obfuscate their archives, or make it difficult to download the photos, or only provide low-res copies. What's even the point of capturing history if you're not going to share it.</p>
<p>My portfolio site is <a href="https://git.voussoir.net/voussoir/voussoir.net/src/branch/master/voussoir.net/photography/generate_site.py">statically generated</a> from my photo database. I'm happy with that, but it's always been a barrier to adding any bespoke elements or sorting things in a non-chronological way. Now that I'm thinking through this out loud, what I'll probably do is use the generator to make the archive page, and handwrite the front page as its own HTML file. Then I'll have the freedom to adjust the front page as often as I want without disturbing the steadiness and reliability of the archive.</p>
<p>So far, I have taken pride in the fact that my photo gallery contains no text on the page except the album titles. No descriptions, no context, no genre tags, no apologies for the photos that aren't great because that would be way too many apologies. I just let the photos speak for themselves, the only editorialization being my selection of a few highlights from each album to be full-size over the rest of the thumbnails. But when I want to write something about it, I put it in /writing. But I think I will have to add genre categories in some way, because if someone (including my future self) just wants to see one genre it can be a nuisance to scroll past all the other stuff. The deliberate lack of categorization is an artistic stance that says "you can't label me" which is cool in a punk kind of way for a few minutes, but it stops being cool when it hinders the usability of the page. I mean, hindering usability for the sake of a message is exactly what an artist would do.</p>
<p>When people publish photobooks, they usually cover several months or even years with only a few dozen pictures. Meanwhile, on my site I publish a few dozen pictures from every single outing. I often get the feeling that I'm publishing way too much, and it makes it harder to find the good ones amongst the bad and middling ones. But my answer to this feeling is the same answer I've given so many times already — I want to publish albums that the subjects themselves would be glad to have, and I make the effort to share it with them directly. I don't want to cull everything down to the 0.1% greatest hits for the benefit of an imaginary, remote, anonymous internet audience.</p>
<h2 id="instxgrxm">Instxgrxm<a class="header_anchor_link" href="#instxgrxm"> (§)</a></h2>
<p>In the past year I've learned something that I really did not expect. It seems that for people under the age of about 40, instagram is by far the number one way they want to exchange contacts. Some older people have asked if I'm on Facebook, but besides that it has all been "what's your instagram?". Nobody has ever asked to connect on any other platform, and I mean <strong>never ever</strong>. It's really crazy how dominant it is. My answer is of course that I don't use instagram and we'll have to trade phone numbers instead. Or, nowadays, here's my card.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/2024-07-10_22-32-32.png"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/2024-07-10_22-32-17.png"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/2024-07-10_22-36-10.png"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/2024-08-01_00-17-00.png"/></p>
<p>One time I asked a girl out to lunch, and she said no, but she offered that we could add each other on instagram. That had nothing to do with photography — that was just instagram being used as a phone number substitute.</p>
<p>On one hand I understand the desire for a phone number substitute because giving out your phone number feels like a sensitive thing, but on the other hand if you exchange instagram with everyone you meet then doesn't instagram become the most sensitive thing?</p>
<p>I already knew that people still use instagram in 2024, but I had assumed that between the aggressive prioritization of video reels, the rise of influencer culture, and the engagement-maximizing algorithmic changes, that instagram was just a place of consumption / doomscrolling at this point. I really had no idea that for so many people this was their primary way to communicate.</p>
<h2 id="how">how<a class="header_anchor_link" href="#how"> (§)</a></h2>
<p>I started drafting this article in May so that I would have plenty of time to think and write before publishing in October. Immediately, I wrote "I leave my camera in manual focus because I've given up on the X-T3's autofocus". But I realized it would be a much better use of these five months to challenge myself and learn how to use the autofocus better, instead of leaving that willfully ignorant stance just sitting on this page the whole time.</p>
<p>... so anyway, I leave my camera in manual focus because I've given up on the X-T3's autofocus.</p>
<p>My first assumption was that I don't need AF-S because I use M with a function button for focusing. I thought that M+FBF was equal to or better than AF-S because I can focus when I want to and let go of the shutter button without refocusing. But there were two things I hadn't considered: AF-S with face detect will also expose for the face which I'd have to do myself in M; and AF-S with focus priority will wait until the picture is in focus before firing, whereas in M I'd try my best and maybe get it wrong and not know it was blurry until later. So I gave AF-S a try for a while to see if I had just been narrowmindedly been stuck in M this whole time... and now I'm back already. Fuji's AF still misses a lot and it goes hunting for faces in the background. Believe me, my success rate with M+FBF is also less than 100%, but at least then I know it's my fault and not the camera's.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/captcha.png"/></p>
<p>My second assumption was that I don't need AF-C because when the subject is moving quickly I won't be able to trust the camera to keep up, and if they're moving slowly then I'll be fine with M+FBF. And, yeah, I wasn't able to crack this one either. All I know is that even when I'm shooting what I'd consider an easy subject, the face/eye detect will jump all around, off into deep space, and it'll spend maybe 20-40% of its time not in correct focus. Then you throw sunglasses or hats into the mix and it gets harder, and then the person turns around so the camera finds something else to focus on, and by the time they face forward again now you're starting from scratch.</p>
<p>Keep in mind the X-T3 is now two generations behind, so I don't have experience with Fuji's AF of today. But everyone knows that Fuji is behind competitors in terms of AF, so if I was using one of the other leaders I might have the totally opposite stance here and use AF-C for everything.</p>
<p>I know very well that trying a tool for a few minutes and quickly giving up is not a good-faith evaluation of the tool. I know that there is a learning period, and you have to <a href="https://www.youtube.com/watch?v=Q6mU7A0a3Mg" title="The Brood opening scene">go all the way through it to the end</a>. The problem is I don't feel like I have opportunities for low-stakes practice on real people long enough to get through it. What I mean is that if I try to shoot an entire Friday Night concert in AF-S or AF-C without falling back to M, I'm going to ruin or miss out on a bunch of shots and I'm going to disappoint that band by giving them my worst album of the season. Or if I try to do this at the beach I'll ruin all the shots there and feel like I wasted the day. And I don't have anyone that I feel comfortable asking to model for me for practice.</p>
<p>Excuses, excuses. I don't pretend any of this is justified. I am lazy, stubborn, and undisciplined and trying to rationalize it.</p>
<center>* * *</center><p>I listened to an audio recording I made last year and was genuinely surprised to hear the autofocus beep. It's been so long since I've heard it, I forgot it ever existed. I turned it off one night while shooting an orchestra concert and off it stayed.</p>
<p>I am still not very interested in editing. I only do basic levels adjustments to fix over/under exposure, and occasionally I'll crop to fix a bad composition or remove a distracting background figure. I don't like to crop, because it represents a failure to compose right the first time, and because it throws away hard-earned megapixels, but sometimes you just have to.</p>
<p>If I'm shooting something very important then I'll shoot raw+jpeg, because yes the raws will absolutely rescue an exposure that is unfixable in jpeg. I just prefer to not need rescuing.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/bouquet.jpg"/></p>
<p>Underexposed is better than blurry. When the sun is setting, crank the ISO and open the aperture and hold on to your shutter speed for as long as you can. A dark photo can be brightened in post, but a blurry photo is ruined forever. This works better in black and white than in color.</p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2024-07-28_19-58-27_before.jpg"/></p>
<p><img alt="" loading="lazy" src="/writing/hobby_photography_2/thumbs/2024-07-28_19-58-27_after.jpg"/></p>
<p>By the way guys raw is not an acronym.</p>
<h2 id="fuji_dial_slipups">Fuji dial slip-ups<a class="header_anchor_link" href="#fuji_dial_slipups"> (§)</a></h2>
<p>At some point, my camera will be old and battered enough to warrant replacing, and naturally I'll look for a new Fuji. But some of Fuji's recent changes have me worried that the X-T3 might wind up being the peak of their design, and they might be on a downtrend for a while.</p>
<p>In the transition from X-T3 to X-T4 they replaced the photometry/metering dial with the stills-movie switch. In the X-T3, movie mode is part of the drive dial.</p>
<p><a href="https://fujifilm-dsc.com/en/manual/x-t3/about_this_camera/parts/index.html"><img alt="" loading="lazy" src="/writing/hobby_photography_2/xt3_photometry.png"/></a></p>
<p><a href="https://fujifilm-dsc.com/en/manual/x-t4/about_this_camera/parts/index.html"><img alt="" loading="lazy" src="/writing/hobby_photography_2/xt4_moviemode.png"/></a></p>
<p>I can't help but feel that this is an obviously poor decision and that movie mode belongs on the drive dial. The drive dial is the master switch that sets the context for every other feature on the camera. The drive modes are mutually exclusive with one another, not only because the switch cannot physically be in two positions at once, but because it is logical that you can take photos or videos but not both at once. By pulling movie mode out into its own dial, now there is a matrix of Drive/Movie dial positions that are all meaningless. What is Movie+CL? Nothing. What is Movie+Bracket? Nothing. What is Movie+Panorama? Nothing. If the movie mode is going to continue being mutually exclusive with all other modes, it should have stayed on the drive dial.</p>
<p>Then, in the X-T5 they kept it this way despite the marketing fluff about "coming back to our roots" and being "photography first", explicitly acknowledging that <a href="https://www.youtube.com/watch?v=0MTCbj_VDa0&amp;t=6m20s" title="X Summit Tokyo 2022">the X-H series are the hybrids, not the X-T series</a>.</p>
<p><a href="https://fujifilm-x.com/en-us/products/cameras/x-t5/"><img alt="" loading="lazy" src="/writing/hobby_photography_2/xt5_photography_first.png"/></a></p>
<p><a href="https://fujifilm-x.com/en-us/series/fujifilm-x-t5/ready-to-party/"><img alt="" loading="lazy" src="/writing/hobby_photography_2/xt5_roots.png"/></a></p>
<p>I have no problem saying that the photometry dial is the least used dial on my X-T3. But there is no point throwing away a seldom-used feature to make way for a switch that is mutually exclusive with all other choices on the drive dial, and especially not in the product line that claims to be photo-first.</p>
<p>And now, in the X-T50, they've replaced the ISO dial with a film simulation dial.</p>
<p><a href="https://fujifilm-dsc.com/en-int/manual/x-t50/about_this_camera/parts/"><img alt="" loading="lazy" src="/writing/hobby_photography_2/xt50_filmdial.png"/></a></p>
<p>The X-T*0 line is meant to be more amateur than the X-T* line, but wow, I really do not think people need to change their film simulation more often than their ISO. I don't know what kind of psychedelic photo albums Fuji is expecting people to shoot, where every photo in the sequence is a completely different color tone. I'm not sure if that's low art or high art. Deprioritizing the exposure triangle like this is not cool, even in the amateur product line, because you're supposed to give the artist room to grow, not pad them in. Besides, most people probably have one or two favorite film sims, and the rest will never get used. Hey instead of a stills-movie switch can I get a Provia-Acros switch?</p>
<p>So, yeah, I'm not sure what my options will be when it's time for a new camera. To be honest I'd like to get into medium format, i.e. the GFX line, but the dials there are even less impressive. X-T3 peak?</p>
<h2 id="conclusion">Conclusion<a class="header_anchor_link" href="#conclusion"> (§)</a></h2>
<p>In the <a href="/writing/hobby_photography">previous article</a> I said I might look back on those opinions in three months and find them unrecognizable. Here we are at two years and that's not the case. Hooray! Seems like my core beliefs are pretty stable, and right now it's just a matter of improving my skills and seeking bigger, more important work.</p>
<p>That's all I've got to say for now.</p>

<hr/>
<p><a href="https://git.voussoir.net/voussoir/voussoir.net/commits/master/voussoir.net/writing/hobby_photography_2/hobby_photography_2.md">View this document's history</a></p>
<ul>
<li><a href="https://git.voussoir.net/voussoir/voussoir.net/commit/34430fa3c647efdf5b3d0d655460e395eb59864d"><time datetime="2024-10-15T21:47:59-07:00">2024-10-15</time> Add hobby_photography_2.md.</a></li>
</ul>
<address>Contact me: writing@voussoir.net</address>
<p>If you would like to subscribe for more, add this to your RSS reader: <a href="/writing/writing.atom" rel="alternate" type="application/atom+xml">https://voussoir.net/writing/writing.atom</a></p>
</article>
            
            


title: Why the Circular Specification Problem and the Observer Effect Are Distinct
feedurl: https://tratt.net/laurie/news.rss
guid:
https://tratt.net/laurie/blog/2024/why_the_circular_specification_problem_and_the_observer_effect_are_distinct.html

url:
https://tratt.net/laurie/blog/2024/why_the_circular_specification_problem_and_the_observer_effect_are_distinct.html


title: Can We Retain the Benefits of Transitive Dependencies Without Undermining Security?
feedurl: https://tratt.net/laurie/news.rss
guid:
https://tratt.net/laurie/blog/2024/can_we_retain_the_benefits_of_transitive_dependencies_without_undermining_security.html

url:
https://tratt.net/laurie/blog/2024/can_we_retain_the_benefits_of_transitive_dependencies_without_undermining_security.html


guid: tag:blog.golang.org,2013:blog.golang.org/go1.23
title: Go 1.23 is released
author: Dmitri Shuralyov, on behalf of the Go team
url: https://go.dev/blog/go1.23
feedurl: https://go.dev/blog/feed.atom
content_mime_type: text/html
content:
<div id="blog"><div id="content">
  <div id="content">

    <div class="Article" data-slug="/blog/go1.23">
    
    <h1 class="small"><a href="/blog/">The Go Blog</a></h1>
    

    <h1>Go 1.23 is released</h1>
      
      <p class="author">
      Dmitri Shuralyov, on behalf of the Go team<br>
      13 August 2024
      </p>
      
      <p>Today the Go team is happy to release Go 1.23,
which you can get by visiting the <a href="/dl/">download page</a>.</p>
<p>If you already have Go 1.22 or Go 1.21 installed on your machine,
you can also try <code>go get toolchain@go1.23.0</code> in an existing module.
This will download the new toolchain and let you begin using it
in your module right away. At some later point, you can follow up
with <code>go get go@1.23.0</code> when you&rsquo;re ready to fully switch to Go 1.23
and have that be your module&rsquo;s minimum required Go version.
See <a href="/doc/toolchain#get">Managing Go version module requirements with go get</a>
for more information on this functionality.</p>
<p>Go 1.23 comes with many improvements over Go 1.22. Some of the highlights include:</p>
<h2 id="language-changes">Language changes</h2>
<ul>
<li>
<p>Range expressions in a &ldquo;for-range&rdquo; loop may now be iterator functions,
such as <code>func(func(K) bool)</code>.
This supports user-defined iterators over arbitrary sequences.
There are several additions to the standard <code>slices</code> and <code>maps</code>
packages that work with iterators, as well as a new <code>iter</code> package.
As an example, if you wish to collect the keys of a map <code>m</code> into a slice
and then sort its values, you can do that in Go 1.23 with <code>slices.Sorted(maps.Keys(m))</code>.</p>
<p>Go 1.23 also includes preview support for generic type aliases.</p>
<p>Read more about <a href="/doc/go1.23#language">language changes</a> and <a href="/doc/go1.23#iterators">iterators</a>
in the release notes.</p>
</li>
</ul>
<h2 id="tool-improvements">Tool improvements</h2>
<ul>
<li>Starting with Go 1.23, it&rsquo;s possible for the Go toolchain to collect usage and breakage
statistics to help understand how the Go toolchain is used, and how well it is working.
This is Go telemetry, an <em>opt-in system</em>. Please consider opting in to help us keep Go
working well and better understand Go usage.
Read more on <a href="/doc/go1.23#telemetry">Go telemetry</a> in the release notes.</li>
<li>The <code>go</code> command has new conveniences. For example, running <code>go env -changed</code> makes it easier to
see only those settings whose effective value differs from the default value, and
<code>go mod tidy -diff</code> helps determine the necessary changes to the go.mod and go.sum files
without modifying them.
Read more on the <a href="/doc/go1.23#go-command">Go command</a> in the release notes.</li>
<li>The <code>go vet</code> subcommand now reports symbols that are too new for the intended Go version.
Read more on <a href="/doc/go1.23#tools">tools</a> in the release notes.</li>
</ul>
<h2 id="standard-library-improvements">Standard library improvements</h2>
<ul>
<li>Go 1.23 improves the implementation of <code>time.Timer</code> and <code>time.Ticker</code>.
Read more on <a href="/doc/go1.23#timer-changes">timer changes</a> in the release notes.</li>
<li>There are a total of 3 new packages in the Go 1.23 standard library: <code>iter</code>, <code>structs</code>, and <code>unique</code>.
Package <code>iter</code> is mentioned above.
Package <code>structs</code> defines marker types to modify the properties of a struct.
Package <code>unique</code> provides facilities for canonicalizing (&ldquo;interning&rdquo;) comparable
values.
Read more on <a href="/doc/go1.23#new-unique-package">new standard library packages</a>
in the release notes.</li>
<li>There are many improvements and additions to the standard library enumerated
in the <a href="/doc/go1.23#minor_library_changes">minor changes to the library</a>
section of the release notes.
The “Go, Backwards Compatibility, and GODEBUG” documentation
enumerates <a href="/doc/godebug#go-123">new to Go 1.23 GODEBUG settings</a>.</li>
<li>Go 1.23 supports the new <code>godebug</code> directive in <code>go.mod</code> and <code>go.work</code> files to
allow separate control of the default GODEBUGs and the “go” directive of <code>go.mod</code>,
in addition to <code>//go:debug</code> directive comments made available two releases ago (Go 1.21).
See the updated documentation on <a href="/doc/godebug#default">Default GODEBUG Values</a>.</li>
</ul>
<h2 id="more-improvements-and-changes">More improvements and changes</h2>
<ul>
<li>Go 1.23 adds experimental support for OpenBSD on 64-bit RISC-V (<code>openbsd/riscv64</code>).
There are several minor changes relevant to Linux, macOS, ARM64, RISC-V, and WASI.
Read more on <a href="/doc/go1.23#ports">ports</a> in the release notes.</li>
<li>Build time when using profile-guided optimization (PGO) is reduced, and performance
with PGO on 386 and amd64 architectures is improved.
Read more on <a href="/doc/go1.23#runtime">runtime, compiler, and linker</a> in the release notes.</li>
</ul>
<p>We encourage everyone to read the <a href="/doc/go1.23">Go 1.23 release notes</a> for the
complete and detailed information on these changes, and everything else that&rsquo;s
new to Go 1.23.</p>
<p>Over the next few weeks, look out for follow-up blog posts that will go in more depth
on some of the topics mentioned here, including “range-over-func”, the new <code>unique</code> package,
Go 1.23 timer implementation changes, and more.</p>
<hr>
<p>Thank you to everyone who contributed to this release by writing code and
documentation, reporting bugs, sharing feedback, and testing the release
candidates. Your efforts helped to ensure that Go 1.23 is as stable as possible.
As always, if you notice any problems, please <a href="/issue/new">file an issue</a>.</p>
<p>Enjoy Go 1.23!</p>

    </div>

    
    <div class="Article prevnext">
    
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
        <p>
        
          
            <b>Next article: </b><a href="/blog/range-functions">Range Over Function Types</a><br>
          
        
        
          
            <b>Previous article: </b><a href="/blog/chacha8rand">Secure Randomness in Go 1.22</a><br>
          
        
        <b><a href="/blog/all">Blog Index</a></b>
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
    </div>
    

  </div>
</div>

<script src="/js/play.js"></script>


guid: tag:blog.golang.org,2013:blog.golang.org/range-functions
title: Range Over Function Types
author: Ian Lance Taylor
url: https://go.dev/blog/range-functions
feedurl: https://go.dev/blog/feed.atom
content_mime_type: text/html
content:
<div id="blog"><div id="content">
  <div id="content">

    <div class="Article" data-slug="/blog/range-functions">
    
    <h1 class="small"><a href="/blog/">The Go Blog</a></h1>
    

    <h1>Range Over Function Types</h1>
      
      <p class="author">
      Ian Lance Taylor<br>
      20 August 2024
      </p>
      
      <h2 id="introduction">Introduction</h2>
<p>This is the blog post version of my talk at GopherCon 2024.</p>
<p>Range over function types is a new language feature in the Go 1.23
release.
This blog post will explain why we are adding this new feature, what
exactly it is, and how to use it.</p>
<h2 id="why">Why?</h2>
<p>Since Go 1.18 we&rsquo;ve had the ability to write new generic container
types in Go.
For example, let&rsquo;s consider this very simple <code>Set</code> type, a generic
type implemented on top of a map.</p>
<pre><code>// Set holds a set of elements.
type Set[E comparable] struct {
    m map[E]struct{}
}

// New returns a new [Set].
func New[E comparable]() *Set[E] {
    return &amp;Set[E]{m: make(map[E]struct{})}
}
</code></pre>
<p>Naturally a set type has a way to add elements and a way to check
whether elements are present.  The details here don&rsquo;t matter.</p>
<pre><code>// Add adds an element to a set.
func (s *Set[E]) Add(v E) {
    s.m[v] = struct{}{}
}

// Contains reports whether an element is in a set.
func (s *Set[E]) Contains(v E) bool {
    _, ok := s.m[v]
    return ok
}
</code></pre>
<p>And among other things we will want a function to return the union of
two sets.</p>
<pre><code>// Union returns the union of two sets.
func Union[E comparable](s1, s2 *Set[E]) *Set[E] {
    r := New[E]()
    // Note for/range over internal Set field m.
    // We are looping over the maps in s1 and s2.
    for v := range s1.m {
        r.Add(v)
    }
    for v := range s2.m {
        r.Add(v)
    }
    return r
}
</code></pre>
<p>Let&rsquo;s look at this implementation of the <code>Union</code> function for a
minute.
In order to compute the union of two sets, we need a way to get all
the elements that are in each set.
In this code we use a for/range statement over an unexported field of
the set type.
That only works if the <code>Union</code> function is defined in the set package.</p>
<p>But there are a lot of reasons why someone might want to loop over all
the elements in a set.
This set package has to provide some way for its users to do that.</p>
<p>How should that work?</p>
<h3 id="push-set-elements">Push Set elements</h3>
<p>One approach is to provide a <code>Set</code> method that takes a function, and
to call that function with every element in the Set.
We&rsquo;ll call this <code>Push</code>, because the <code>Set</code> pushes every value to the
function.
Here if the function returns false, we stop calling it.</p>
<pre><code>func (s *Set[E]) Push(f func(E) bool) {
    for v := range s.m {
        if !f(v) {
            return
        }
    }
}
</code></pre>
<p>In the Go standard library, we see this general pattern used for cases
like the <a href="https://pkg.go.dev/sync#Map.Range" rel="noreferrer" target="_blank"><code>sync.Map.Range</code></a> method,
the <a href="https://pkg.go.dev/flag#Visit" rel="noreferrer" target="_blank"><code>flag.Visit</code></a> function, and the
<a href="https://pkg.go.dev/path/filepath#Walk" rel="noreferrer" target="_blank"><code>filepath.Walk</code></a> function.
This is a general pattern, not an exact one; as it happens, none of
those three examples work quite the same way.</p>
<p>This is what it looks like to use the <code>Push</code> method to print all the
elements of a set: you call <code>Push</code> with a function that does what you
want with the element.</p>
<pre><code>func PrintAllElementsPush[E comparable](s *Set[E]) {
    s.Push(func(v E) bool {
        fmt.Println(v)
        return true
    })
}
</code></pre>
<h3 id="pull-set-elements">Pull Set elements</h3>
<p>Another approach to looping over the elements of a <code>Set</code> is to return
a function.
Each time the function is called, it will return a value from the
<code>Set</code>, along with a boolean that reports whether the value is valid.
The boolean result will be false when the loop has gone through all
the elements.
In this case we also need a stop function that can be called when no
more values are needed.</p>
<p>This implementation uses a pair of channels, one for values in the
set and one to stop returning values.
We use a goroutine to send values on the channel.
The <code>next</code> function returns an element from the set by reading from
the element channel, and the <code>stop</code> function tells the goroutine to
exit by closing the stop channel.
We need the <code>stop</code> function to make sure that the goroutine exits when
no more values are needed.</p>
<pre><code>// Pull returns a next function that returns each
// element of s with a bool for whether the value
// is valid. The stop function should be called
// when finished calling the next function.
func (s *Set[E]) Pull() (func() (E, bool), func()) {
    ch := make(chan E)
    stopCh := make(chan bool)

    go func() {
        defer close(ch)
        for v := range s.m {
            select {
            case ch &lt;- v:
            case &lt;-stopCh:
                return
            }
        }
    }()

    next := func() (E, bool) {
        v, ok := &lt;-ch
        return v, ok
    }

    stop := func() {
        close(stopCh)
    }

    return next, stop
}
</code></pre>
<p>Nothing in the standard library works exactly this way.  Both
<a href="https://pkg.go.dev/runtime#CallersFrames" rel="noreferrer" target="_blank"><code>runtime.CallersFrames</code></a>
and
<a href="https://pkg.go.dev/reflect#Value.MapRange" rel="noreferrer" target="_blank"><code>reflect.Value.MapRange</code></a>
are similar, though they return values with methods rather than
returning functions directly.</p>
<p>This is what it looks like to use the <code>Pull</code> method to print all the
elements of a <code>Set</code>.
You call <code>Pull</code> to get a function, and you repeatedly call that
function in a for loop.</p>
<pre><code>func PrintAllElementsPull[E comparable](s *Set[E]) {
    next, stop := s.Pull()
    defer stop()
    for v, ok := next(); ok; v, ok = next() {
        fmt.Println(v)
    }
}
</code></pre>
<h2 id="standardize-the-approach">Standardize the approach</h2>
<p>We&rsquo;ve now seen two different approaches to looping over all the
elements of a set.
Different Go packages use these approaches and several others.
That means that when you start using a new Go container package you
may have to learn a new looping mechanism.
It also means that we can&rsquo;t write one function that works with several
different types of containers, as the container types will handle
looping differently.</p>
<p>We want to improve the Go ecosystem by developing standard approaches
for looping over containers.</p>
<h3 id="iterators">Iterators</h3>
<p>This is, of course, an issue that arises in many programming
languages.</p>
<p>The popular <a href="https://en.wikipedia.org/wiki/Design_Patterns" rel="noreferrer" target="_blank">Design Patterns
book</a>, first published
in 1994, describes this as the iterator pattern.
You use an iterator to &ldquo;provide a way to access the elements of an
aggregate object sequentially without exposing its underlying
representation.&rdquo;
What this quote calls an aggregate object is what I&rsquo;ve been calling a
container.
An aggregate object, or container, is just a value that holds other
values, like the <code>Set</code> type we&rsquo;ve been discussing.</p>
<p>Like many ideas in programming, iterators date back to Barbara
Liskov&rsquo;s <a href="https://en.wikipedia.org/wiki/CLU_(programming_language)" rel="noreferrer" target="_blank">CLU language</a>,
developed in the 1970&rsquo;s.</p>
<p>Today many popular languages provide iterators one way or another,
including, among others, C++, Java, Javascript, Python, and Rust.</p>
<p>However, Go before version 1.23 did not.</p>
<h3 id="forrange">For/range</h3>
<p>As we all know, Go has container types that are built in to the
language: slices, arrays, and maps.
And it has a way to access the elements of those values without
exposing the underlying representation: the for/range statement.
The for/range statement works for Go&rsquo;s built-in container types (and
also for strings, channels, and, as of Go 1.22, int).</p>
<p>The for/range statement is iteration, but it is not iterators as they
appear in today&rsquo;s popular languages.
Still, it would be nice to be able to use for/range to iterate over a
user-defined container like the <code>Set</code> type.</p>
<p>However, Go before version 1.23 did not support this.</p>
<h3 id="improvements-in-this-release">Improvements in this release</h3>
<p>For Go 1.23 we&rsquo;ve decided to support both for/range over user-defined
container types, and a standardized form of iterators.</p>
<p>We extended the for/range statement to support ranging over function
types.
We&rsquo;ll see below how this helps loop over user-defined containers.</p>
<p>We also added standard library types and functions to support using
function types as iterators.
A standard definition of iterators lets us write functions that work
smoothly with different container types.</p>
<h3 id="range-over-some-function-types">Range over (some) function types</h3>
<p>The improved for/range statement doesn&rsquo;t support arbitrary function
types.
As of Go 1.23 it now supports ranging over functions that take a
single argument.
The single argument must itself be a function that takes zero to two
arguments and returns a bool; by convention, we call it the yield
function.</p>
<pre><code>func(yield func() bool)

func(yield func(V) bool)

func(yield func(K, V) bool)
</code></pre>
<p>When we speak of an iterator in Go, we mean a function with one of
these three types.
As we&rsquo;ll discuss below, there is another kind of iterator in the
standard library: a pull iterator.
When it is necessary to distinguish between standard iterators and
pull iterators, we call the standard iterators push iterators.
That is because, as we will see, they push out a sequence of values by
calling a yield function.</p>
<h3 id="standard-push-iterators">Standard (push) iterators</h3>
<p>To make iterators easier to use, the new standard library package iter
defines two types: <code>Seq</code> and <code>Seq2</code>.
These are names for the iterator function types, the types that can be
used with the for/range statement.
The name <code>Seq</code> is short for sequence, as iterators loop through a
sequence of values.</p>
<pre><code>package iter

type Seq[V any] func(yield func(V) bool)

type Seq2[K, V any] func(yield func(K, V) bool)

// for now, no Seq0
</code></pre>
<p>The difference between <code>Seq</code> and <code>Seq2</code> is just that <code>Seq2</code> is a
sequence of pairs, such as a key and a value from a map.
In this post we&rsquo;ll focus on <code>Seq</code> for simplicity, but most of what we
say covers <code>Seq2</code> as well.</p>
<p>It&rsquo;s easiest to explain how iterators work with an example.
Here the <code>Set</code> method <code>All</code> returns a function.
The return type of <code>All</code> is <code>iter.Seq[E]</code>, so we know that it returns
an iterator.</p>
<pre><code>// All is an iterator over the elements of s.
func (s *Set[E]) All() iter.Seq[E] {
    return func(yield func(E) bool) {
        for v := range s.m {
            if !yield(v) {
                return
            }
        }
    }
}
</code></pre>
<p>The iterator function itself takes another function, the yield
function, as an argument.
The iterator calls the yield function with every value in the set.
In this case the iterator, the function returned by <code>Set.All</code>, is a
lot like the <code>Set.Push</code> function we saw earlier.</p>
<p>This shows how iterators work: for some sequence of values, they call
a yield function with each value in the sequence.
If the yield function returns false, no more values are needed, and
the iterator can just return, doing any cleanup that may be required.
If the yield function never returns false, the iterator can just
return after calling yield with all the values in the sequence.</p>
<p>That&rsquo;s how they work, but let&rsquo;s acknowledge that the first time you
see one of these, your first reaction is probably &ldquo;there are a lot of
functions flying around here.&rdquo;
You&rsquo;re not wrong about that.
Let&rsquo;s focus on two things.</p>
<p>The first is that once you get past the first line of this function&rsquo;s
code, the actual implementation of the iterator is pretty simple: call
yield with every element of the set, stopping if yield returns false.</p>
<pre><code>        for v := range s.m {
            if !yield(v) {
                return
            }
        }
</code></pre>
<p>The second is that using this is really easy.
You call <code>s.All</code> to get an iterator, and then you use for/range to
loop over all the elements in <code>s</code>.
The for/range statement supports any iterator, and this shows how easy
that is to use.</p>
<pre><code>func PrintAllElements[E comparable](s *Set[E]) {
    for v := range s.All() {
        fmt.Println(v)
    }
}
</code></pre>
<p>In this kind of code <code>s.All</code> is a method that returns a function.
We are calling <code>s.All</code>, and then using for/range to range over the
function that it returns.
In this case we could have made <code>Set.All</code> be an iterator function
itself, rather than having it return an iterator function.
However, in some cases that won&rsquo;t work, such as if the function that
returns the iterator needs to take an argument, or needs to do some
set up work.
As a matter of convention, we encourage all container types to provide
an <code>All</code> method that returns an iterator, so that programmers don&rsquo;t
have to remember whether to range over <code>All</code> directly or whether to
call <code>All</code> to get a value they can range over.
They can always do the latter.</p>
<p>If you think about it, you&rsquo;ll see that the compiler must be adjusting
the loop to create a yield function to pass to the iterator returned
by <code>s.All</code>.
There&rsquo;s a fair bit of complexity in the Go compiler and runtime to
make this efficient, and to correctly handle things like <code>break</code> or
<code>panic</code> in the loop.
We&rsquo;re not going to cover any of that in this blog post.
Fortunately the implementation details are not important when it comes
to actually using this feature.</p>
<h3 id="pull-iterators">Pull iterators</h3>
<p>We&rsquo;ve now seen how to use iterators in a for/range loop.
But a simple loop is not the only way to use an iterator.
For example, sometimes we may need to iterate over two containers in
parallel.
How do we do that?</p>
<p>The answer is that we use a different kind of iterator: a pull
iterator.
We&rsquo;ve seen that a standard iterator, also known as a push iterator, is
a function that takes a yield function as an argument and pushes each
value in a sequence by calling the yield function.</p>
<p>A pull iterator works the other way around: it is a function that is
written such that each time you call it, it returns the next value in
the sequence.</p>
<p>We&rsquo;ll repeat the difference between the two types of iterators to help
you remember:</p>
<ul>
<li>A push iterator pushes each value in a sequence to a yield
function.
Push iterators are standard iterators in the Go standard library,
and are supported directly by the for/range statement.</li>
<li>A pull iterator works the other way around.
Each time you call a pull iterator, it pulls another value from a
sequence and returns it.
Pull iterators are <em>not</em> supported directly by the for/range
statement; however, it&rsquo;s straightforward to write an ordinary for
statement that loops through a pull iterator.
In fact, we saw an example earlier when we looked at using the
<code>Set.Pull</code> method.</li>
</ul>
<p>You could write a pull iterator yourself, but normally you don&rsquo;t have
to.
The new standard library function
<a href="https://pkg.go.dev/iter#Pull" rel="noreferrer" target="_blank"><code>iter.Pull</code></a> takes a standard iterator,
that is to say a function that is a push iterator, and returns a pair
of functions.
The first is a pull iterator: a function that returns the next value
in the sequence each time it is called.
The second is a stop function that should be called when we are done
with the pull iterator.
This is like the <code>Set.Pull</code> method we saw earlier.</p>
<p>The first function returned by <code>iter.Pull</code>, the pull iterator, returns
a value and a boolean that reports whether that value is valid.
The boolean will be false at the end of the sequence.</p>
<p><code>iter.Pull</code> returns a stop function in case we don&rsquo;t read through the
sequence to the end.
In the general case the push iterator, the argument to <code>iter.Pull</code>,
may start goroutines, or build new data structures that need to be
cleaned up when iteration is complete.
The push iterator will do any cleanup when the yield function returns
false, meaning that no more values are required.
When used with a for/range statement, the for/range statement will
ensure that if the loop exits early, through a <code>break</code> statement or
for any other reason, then the yield function will return false.
With a pull iterator, on the other hand, there is no way to force the
yield function to return false, so the stop function is needed.</p>
<p>Another way to say this is that calling the stop function will cause
the yield function to return false when it is called by the push
iterator.</p>
<p>Strictly speaking you don&rsquo;t need to call the stop function if the pull
iterator returns false to indicate that it has reached the end of the
sequence, but it&rsquo;s usually simpler to just always call it.</p>
<p>Here is an example of using pull iterators to walk through two
sequences in parallel.
This function reports whether two arbitrary sequences contain the same
elements in the same order.</p>
<pre><code>// EqSeq reports whether two iterators contain the same
// elements in the same order.
func EqSeq[E comparable](s1, s2 iter.Seq[E]) bool {
    next1, stop1 := iter.Pull(s1)
    defer stop1()
    next2, stop2 := iter.Pull(s2)
    defer stop2()
    for {
        v1, ok1 := next1()
        v2, ok2 := next2()
        if !ok1 {
            return !ok2
        }
        if ok1 != ok2 || v1 != v2 {
            return false
        }
    }
}
</code></pre>
<p>The function uses <code>iter.Pull</code> to convert the two push iterators, <code>s1</code>
and <code>s2</code>, into pull iterators.
It uses <code>defer</code> statements to make sure that the pull iterators are
stopped when we are done with them.</p>
<p>Then the code loops, calling the pull iterators to retrieve values.
If the first sequence is done, it returns true if the second sequence
is also done, or false if it isn&rsquo;t.
If the values are different, it returns false.
Then it loops to pull the next two values.</p>
<p>As with push iterators, there is some complexity in the Go runtime to
make pull iterators efficient, but this does not affect code that
actually uses the <code>iter.Pull</code> function.</p>
<h2 id="iterating-on-iterators">Iterating on iterators</h2>
<p>Now you know everything there is to know about range over function
types and about iterators.
We hope you enjoy using them!</p>
<p>Still, there are a few more things worth mentioning.</p>
<h3 id="adapters">Adapters</h3>
<p>An advantage of a standard definition of iterators is the ability to
write standard adapter functions that use them.</p>
<p>For example, here is a function that filters a sequence of values,
returning a new sequence.
This <code>Filter</code> function takes an iterator as an argument and returns a
new iterator.
The other argument is a filter function that decides which values
should be in the new iterator that <code>Filter</code> returns.</p>
<pre><code>// Filter returns a sequence that contains the elements
// of s for which f returns true.
func Filter[V any](f func(V) bool, s iter.Seq[V]) iter.Seq[V] {
    return func(yield func(V) bool) {
        for v := range s {
            if f(v) {
                if !yield(v) {
                    return
                }
            }
        }
    }
}
</code></pre>
<p>As with the earlier example, the function signatures look complicated
when you first see them.
Once you get past the signatures, the implementation is
straightforward.</p>
<pre><code>        for v := range s {
            if f(v) {
                if !yield(v) {
                    return
                }
            }
        }
</code></pre>
<p>The code ranges over the input iterator, checks the filter function,
and calls yield with the values that should go into the output
iterator.</p>
<p>We&rsquo;ll show an example of using <code>Filter</code> below.</p>
<p>(There is no version of <code>Filter</code> in the Go standard library today, but
one may be added in future releases.)</p>
<h3 id="binary-tree">Binary tree</h3>
<p>As an example of how convenient a push iterator can be to loop over a
container type, let&rsquo;s consider this simple binary tree type.</p>
<pre><code>// Tree is a binary tree.
type Tree[E any] struct {
    val         E
    left, right *Tree[E]
}
</code></pre>
<p>We won&rsquo;t show the code to insert values into the tree, but naturally
there should be some way to range over all the values in the tree.</p>
<p>It turns out that the iterator code is easier to write if it returns a
bool.
Since the function types supported by for/range don&rsquo;t return anything,
the <code>All</code> method here returns a small function literal that calls the
iterator itself, here called <code>push</code>, and ignores the bool result.</p>
<pre><code>// All returns an iterator over the values in t.
func (t *Tree[E]) All() iter.Seq[E] {
    return func(yield func(E) bool) {
        t.push(yield)
    }
}

// push pushes all elements to the yield function.
func (t *Tree[E]) push(yield func(E) bool) bool {
    if t == nil {
        return true
    }
    return t.left.push(yield) &amp;&amp;
        yield(t.val) &amp;&amp;
        t.right.push(yield)
}
</code></pre>
<p>The <code>push</code> method uses recursion to walk over the whole tree, calling
yield on each element.
If the yield function returns false, the method returns false all the
way up the stack.
Otherwise it just returns once the iteration is complete.</p>
<p>This shows how straightforward it is to use this iterator approach to
loop over even complex data structures.
There is no need to maintain a separate stack to record the position
within the tree; we can just use the goroutine call stack to do that
for us.</p>
<h3 id="new-iterator-functions">New iterator functions.</h3>
<p>Also new in Go 1.23 are functions in the slices and maps packages that
work with iterators.</p>
<p>Here are the new functions in the slices package.
<code>All</code> and <code>Values</code> are functions that return iterators over the
elements of a slice.
<code>Collect</code> fetches the values out of an iterator and returns a slice
holding those values.
See the docs for the others.</p>
<ul>
<li><a href="https://pkg.go.dev/slices#All" rel="noreferrer" target="_blank"><code>All([]E) iter.Seq2[int, E]</code></a></li>
<li><a href="https://pkg.go.dev/slices#Values" rel="noreferrer" target="_blank"><code>Values([]E) iter.Seq[E]</code></a></li>
<li><a href="https://pkg.go.dev/slices#Collect" rel="noreferrer" target="_blank"><code>Collect(iter.Seq[E]) []E</code></a></li>
<li><a href="https://pkg.go.dev/slices#AppendSeq" rel="noreferrer" target="_blank"><code>AppendSeq([]E, iter.Seq[E]) []E</code></a></li>
<li><a href="https://pkg.go.dev/slices#Backward" rel="noreferrer" target="_blank"><code>Backward([]E) iter.Seq2[int, E]</code></a></li>
<li><a href="https://pkg.go.dev/slices#Sorted" rel="noreferrer" target="_blank"><code>Sorted(iter.Seq[E]) []E</code></a></li>
<li><a href="https://pkg.go.dev/slices#SortedFunc" rel="noreferrer" target="_blank"><code>SortedFunc(iter.Seq[E], func(E, E) int) []E</code></a></li>
<li><a href="https://pkg.go.dev/slices#SortedStableFunc" rel="noreferrer" target="_blank"><code>SortedStableFunc(iter.Seq[E], func(E, E) int) []E</code></a></li>
<li><a href="https://pkg.go.dev/slices#Repeat" rel="noreferrer" target="_blank"><code>Repeat([]E, int) []E</code></a></li>
<li><a href="https://pkg.go.dev/slices#Chunk" rel="noreferrer" target="_blank"><code>Chunk([]E, int) iter.Seq([]E)</code></a></li>
</ul>
<p>Here are the new functions in the maps package.
<code>All</code>, <code>Keys</code>, and <code>Values</code> returns iterators over the map contents.
<code>Collect</code> fetches the keys and values out of an iterator and returns a
new map.</p>
<ul>
<li><a href="https://pkg.go.dev/maps#All" rel="noreferrer" target="_blank"><code>All(map[K]V) iter.Seq2[K, V]</code></a></li>
<li><a href="https://pkg.go.dev/maps#Keys" rel="noreferrer" target="_blank"><code>Keys(map[K]V) iter.Seq[K]</code></a></li>
<li><a href="https://pkg.go.dev/maps#Values" rel="noreferrer" target="_blank"><code>Values(map[K]V) iter.Seq[V]</code></a></li>
<li><a href="https://pkg.go.dev/maps#Collect" rel="noreferrer" target="_blank"><code>Collect(iter.Seq2[K, V]) map[K, V]</code></a></li>
<li><a href="https://pkg.go.dev/maps#Insert" rel="noreferrer" target="_blank"><code>Insert(map[K, V], iter.Seq2[K, V])</code></a></li>
</ul>
<h3 id="standard-library-iterator-example">Standard library iterator example</h3>
<p>Here is an example of how you might use these new functions along with
the <code>Filter</code> function we saw earlier.
This function takes a map from int to string and returns a slice
holding just the values in the map that are longer than some argument <code>n</code>.</p>
<pre><code>// LongStrings returns a slice of just the values
// in m whose length is n or more.
func LongStrings(m map[int]string, n int) []string {
    isLong := func(s string) bool {
        return len(s) &gt;= n
    }
    return slices.Collect(Filter(isLong, maps.Values(m)))
}
</code></pre>
<p>The <code>maps.Values</code> function returns an iterator over the values in <code>m</code>.
<code>Filter</code> reads that iterator and returns a new iterator that only
contains the long strings.
<code>slices.Collect</code> reads from that iterator into a new slice.</p>
<p>Of course, you could write a loop to do this easily enough, and in
many cases a loop will be clearer.
We don&rsquo;t want to encourage everybody to write code in this style all
the time.
That said, the advantage of using iterators is that this kind of
function works the same way with any sequence.
In this example, notice how Filter is using a map as an input and a
slice as an output, without having to change the code in Filter at
all.</p>
<h3 id="looping-over-lines-in-a-file">Looping over lines in a file</h3>
<p>Although most of the examples we&rsquo;ve seen have involved containers,
iterators are flexible.</p>
<p>Consider this simple code, which doesn&rsquo;t use iterators, to loop over
the lines in a byte slice.
This is easy to write and fairly efficient.</p>
<pre><code>    nl := []byte{'\n'}
    // Trim a trailing newline to avoid a final empty blank line.
    for _, line := range bytes.Split(bytes.TrimSuffix(data, nl), nl) {
        handleLine(line)
    }
</code></pre>
<p>However, <code>bytes.Split</code> does allocate and return a slice of byte slices
to hold the lines.
The garbage collector will have to do a bit of work to eventually free
that slice.</p>
<p>Here is a function that returns an iterator over the lines of some
byte slice.
After the usual iterator signatures, the function is pretty simple.
We keep picking lines out of data until there is nothing left, and we
pass each line to the yield function.</p>
<pre><code>// Lines returns an iterator over lines in data.
func Lines(data []byte) iter.Seq[[]byte] {
    return func(yield func([]byte) bool) {
        for len(data) &gt; 0 {
            line, rest, _ := bytes.Cut(data, []byte{'\n'})
            if !yield(line) {
                return
            }
            data = rest
        }
    }
}
</code></pre>
<p>Now our code to loop over the lines of a byte slice looks like this.</p>
<pre><code>    for line := range Lines(data) {
        handleLine(line)
    }
</code></pre>
<p>This is just as easy to write as the earlier code, and is a bit more
efficient because it doesn&rsquo;t have allocate a slice of lines.</p>
<h3 id="passing-a-function-to-a-push-iterator">Passing a function to a push iterator</h3>
<p>For our final example, we&rsquo;ll see that you don&rsquo;t have to use a push
iterator in a range statement.</p>
<p>Earlier we saw a <code>PrintAllElements</code> function that prints out each
element of a set.
Here is another way to print all the elements of a set: call <code>s.All</code>
to get an iterator, then pass in a hand-written yield function.
This yield function just prints a value and returns true.
Note that there are two function calls here: we call <code>s.All</code> to get an
iterator which is itself a function, and we call that function with
our hand-written yield function.</p>
<pre><code>func PrintAllElements[E comparable](s *Set[E]) {
    s.All()(func(v E) bool {
        fmt.Println(v)
        return true
    })
}
</code></pre>
<p>There&rsquo;s no particular reason to write this code this way.
This is just an example to show that the yield function isn&rsquo;t magic.
It can be any function you like.</p>
<h2 id="update-gomod">Update go.mod</h2>
<p>A final note: every Go module specifies the language version that it
uses.
That means that in order to use new language features in an existing
module you may need to update that version.
This is true for all new language features; it&rsquo;s not something
specific to range over function types.
As range over function types is new in the Go 1.23 release, using it
requires specifying at least Go language version 1.23.</p>
<p>There are (at least) four ways to set the language version:</p>
<ul>
<li>On the command line, run <code>go get go@1.23</code> (or <code>go mod edit -go=1.23</code>
to only edit the <code>go</code> directive).</li>
<li>Manually edit the <code>go.mod</code> file and change the <code>go</code> line.</li>
<li>Keep the older language version for the module as a whole, but use a
<code>//go:build go1.23</code> build tag to permit using range over function
types in a specific file.</li>
</ul>

    </div>

    
    <div class="Article prevnext">
    
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
        <p>
        
          
            <b>Next article: </b><a href="/blog/unique">New unique package</a><br>
          
        
        
          
            <b>Previous article: </b><a href="/blog/go1.23">Go 1.23 is released</a><br>
          
        
        <b><a href="/blog/all">Blog Index</a></b>
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
    </div>
    

  </div>
</div>

<script src="/js/play.js"></script>


guid: tag:blog.golang.org,2013:blog.golang.org/unique
title: New unique package
author: Michael Knyszek
url: https://go.dev/blog/unique
feedurl: https://go.dev/blog/feed.atom
content_mime_type: text/html
content:
<div id="blog"><div id="content">
  <div id="content">

    <div class="Article" data-slug="/blog/unique">
    
    <h1 class="small"><a href="/blog/">The Go Blog</a></h1>
    

    <h1>New unique package</h1>
      
      <p class="author">
      Michael Knyszek<br>
      27 August 2024
      </p>
      
      <p>The standard library of Go 1.23 now includes the <a href="https://pkg.go.dev/unique" rel="noreferrer" target="_blank">new <code>unique</code> package</a>.
The purpose behind this package is to enable the canonicalization of
comparable values.
In other words, this package lets you deduplicate values so that they point
to a single, canonical, unique copy, while efficiently managing the canonical
copies under the hood.
You might be familiar with this concept already, called
<a href="https://en.wikipedia.org/wiki/Interning_(computer_science)" rel="noreferrer" target="_blank">&ldquo;interning&rdquo;</a>.
Let&rsquo;s dive in to see how it works, and why it&rsquo;s useful.</p>
<h2 id="a-simple-implementation-of-interning">A simple implementation of interning</h2>
<p>At a high level, interning is very simple.
Take the code sample below, which deduplicates strings using just a regular
map.</p>
<pre><code>var internPool map[string]string

// Intern returns a string that is equal to s but that may share storage with
// a string previously passed to Intern.
func Intern(s string) string {
    pooled, ok := internPool[s]
    if !ok {
        // Clone the string in case it's part of some much bigger string.
        // This should be rare, if interning is being used well.
        pooled = strings.Clone(s)
        internPool[pooled] = pooled
    }
    return pooled
}
</code></pre>
<p>This is useful for when you&rsquo;re constructing a lot of strings that are likely to
be duplicates, like when parsing a text format.</p>
<p>This implementation is super simple and works well enough for some cases, but it
has a few problems:</p>
<ul>
<li>It never removes strings from the pool.</li>
<li>It cannot be safely used by multiple goroutines concurrently.</li>
<li>It only works with strings, even though the idea is quite general.</li>
</ul>
<p>There&rsquo;s also a missed opportunity in this implementation, and it&rsquo;s subtle.
Under the hood, <a href="/blog/slices">strings are immutable structures consisting of a pointer
and a length</a>.
When comparing two strings, if the pointers are not equal, then we must
compare their contents to determine equality.
But if we know that two strings are canonicalized, then it <em>is</em> sufficient
to just check their pointers.</p>
<h2 id="enter-the-unique-package">Enter the <code>unique</code> package</h2>
<p>The new <code>unique</code> package introduces a function similar to <code>Intern</code> called
<a href="https://pkg.go.dev/unique#Make" rel="noreferrer" target="_blank"><code>Make</code></a>.</p>
<p>It works about the same way as <code>Intern</code>.
Internally there&rsquo;s also a global map (<a href="https://pkg.go.dev/internal/concurrent@go1.23.0" rel="noreferrer" target="_blank">a fast generic concurrent
map</a>) and <code>Make</code> looks up the
provided value in that map.
But it also differs from <code>Intern</code> in two important ways.
Firstly, it accepts values of any comparable type.
And secondly, it returns a wrapper value, a
<a href="https://pkg.go.dev/unique#Handle" rel="noreferrer" target="_blank"><code>Handle[T]</code></a>, from which the canonical value
can be retrieved.</p>
<p>This <code>Handle[T]</code> is key to the design.
A <code>Handle[T]</code> has the property that two <code>Handle[T]</code> values are equal if and
only if the values used to create them are equal.
What&rsquo;s more, the comparison of two <code>Handle[T]</code> values is cheap: it comes down
to a pointer comparison.
Compared to comparing two long strings, that&rsquo;s an order of magnitude cheaper!</p>
<p>So far, this is nothing you can&rsquo;t do in ordinary Go code.</p>
<p>But <code>Handle[T]</code> also has a second purpose: as long as a <code>Handle[T]</code> exists for
a value, the map will retain the canonical copy of the value.
Once all <code>Handle[T]</code> values that map to a specific value are gone, the
package marks that internal map entry as deletable, to be reclaimed in the near
future.
This sets a clear policy for when to remove entries from the map: when the
canonical entries are no longer being used, then the garbage collector is free
to clean them up.</p>
<p>If you&rsquo;ve used Lisp before, this may all sound quite familiar to you.
Lisp <a href="https://en.wikipedia.org/wiki/Symbol_(programming)" rel="noreferrer" target="_blank">symbols</a> are interned
strings, but not strings themselves, and all symbols&rsquo; string values are
guaranteed to be in the same pool.
This relationship between symbols and strings parallels the relationship
between <code>Handle[string]</code> and <code>string</code>.</p>
<h2 id="a-real-world-example">A real-world example</h2>
<p>So, how might one use <code>unique.Make</code>?
Look no further than the <code>net/netip</code> package in the standard library, which
interns values of type <code>addrDetail</code>, part of the
<a href="https://pkg.go.dev/net/netip#Addr" rel="noreferrer" target="_blank"><code>netip.Addr</code></a> structure.</p>
<p>Below is an abridged version of the actual code from <code>net/netip</code> that uses
<code>unique</code>.</p>
<pre><code>// Addr represents an IPv4 or IPv6 address (with or without a scoped
// addressing zone), similar to net.IP or net.IPAddr.
type Addr struct {
    // Other irrelevant unexported fields...

    // Details about the address, wrapped up together and canonicalized.
    z unique.Handle[addrDetail]
}

// addrDetail indicates whether the address is IPv4 or IPv6, and if IPv6,
// specifies the zone name for the address.
type addrDetail struct {
    isV6   bool   // IPv4 is false, IPv6 is true.
    zoneV6 string // May be != &quot;&quot; if IsV6 is true.
}

var z6noz = unique.Make(addrDetail{isV6: true})

// WithZone returns an IP that's the same as ip but with the provided
// zone. If zone is empty, the zone is removed. If ip is an IPv4
// address, WithZone is a no-op and returns ip unchanged.
func (ip Addr) WithZone(zone string) Addr {
    if !ip.Is6() {
        return ip
    }
    if zone == &quot;&quot; {
        ip.z = z6noz
        return ip
    }
    ip.z = unique.Make(addrDetail{isV6: true, zoneV6: zone})
    return ip
}
</code></pre>
<p>Since many IP addresses are likely to use the same zone and this zone is part
of their identity, it makes a lot of sense to canonicalize them.
The deduplication of zones reduces the average memory footprint of each
<code>netip.Addr</code>, while the fact that they&rsquo;re canonicalized means <code>netip.Addr</code>
values are more efficient to compare, since comparing zone names becomes a
simple pointer comparison.</p>
<h2 id="a-footnote-about-interning-strings">A footnote about interning strings</h2>
<p>While the <code>unique</code> package is useful, <code>Make</code> is admittedly not quite like
<code>Intern</code> for strings, since the <code>Handle[T]</code> is required to keep a string from
being deleted from the internal map.
This means you need to modify your code to retain handles as well as strings.</p>
<p>But strings are special in that, although they behave like values, they
actually contain pointers under the hood, as we mentioned earlier.
This means that we could potentially canonicalize just the underlying storage
of the string, hiding the details of a <code>Handle[T]</code> inside the string itself.
So, there is still a place in the future for what I&rsquo;ll call <em>transparent string
interning</em>, in which strings can be interned without the <code>Handle[T]</code> type,
similar to the <code>Intern</code> function but with semantics more closely resembling
<code>Make</code>.</p>
<p>In the meantime, <code>unique.Make(&quot;my string&quot;).Value()</code> is one possible workaround.
Even though failing to retain the handle will allow the string to be deleted
from <code>unique</code>&rsquo;s internal map, map entries are not deleted immediately.
In practice, entries will not be deleted until at least the next garbage
collection completes, so this workaround still allows for some degree of
deduplication in the periods between collections.</p>
<h2 id="some-history-and-looking-toward-the-future">Some history, and looking toward the future</h2>
<p>The truth is that the <code>net/netip</code> package actually interned zone strings since
it was first introduced.
The interning package it used was an internal copy of the
<a href="https://pkg.go.dev/go4.org/intern" rel="noreferrer" target="_blank">go4.org/intern</a> package.
Like the <code>unique</code> package, it has a <code>Value</code> type (which looks a lot like a
<code>Handle[T]</code>, pre-generics), has the notable property that entries in the
internal map are removed once their handles are no longer referenced.</p>
<p>But to achieve this behavior, it has to do some unsafe things.
In particular, it makes some assumptions about the garbage collector&rsquo;s behavior
to implement <a href="https://en.wikipedia.org/wiki/Weak_reference" rel="noreferrer" target="_blank"><em>weak pointers</em></a>
outside the runtime.
A weak pointer is a pointer that doesn&rsquo;t prevent the garbage collector from
reclaiming a variable; when this happens, the pointer automatically becomes
nil.
As it happens, weak pointers are <em>also</em> the core abstraction underlying the
<code>unique</code> package.</p>
<p>That&rsquo;s right: while implementing the <code>unique</code> package, we added proper weak
pointer support to the garbage collector.
And after stepping through the minefield of regrettable design decisions that
accompany weak pointers (like, should weak pointers track <a href="https://en.wikipedia.org/wiki/Object_resurrection" rel="noreferrer" target="_blank">object
resurrection</a>? No!), we were
astonished by how simple and straightforward all of it turned out to be.
Astonished enough that weak pointers are now a <a href="/issue/67552">public
proposal</a>.</p>
<p>This work also led us to reexamine finalizers, resulting in another proposal
for an easier-to-use and more efficient <a href="/issue/67535">replacement for
finalizers</a>.
With <a href="/issue/54670">a hash function for comparable values</a> on the way as well,
the future of <a href="/issue/67552#issuecomment-2200755798">building memory-efficient
caches</a> in Go is bright!</p>

    </div>

    
    <div class="Article prevnext">
    
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
        <p>
        
          
            <b>Next article: </b><a href="/blog/gotelemetry">Telemetry in Go 1.23 and beyond</a><br>
          
        
        
          
            <b>Previous article: </b><a href="/blog/range-functions">Range Over Function Types</a><br>
          
        
        <b><a href="/blog/all">Blog Index</a></b>
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
    </div>
    

  </div>
</div>

<script src="/js/play.js"></script>


guid: tag:blog.golang.org,2013:blog.golang.org/gotelemetry
title: Telemetry in Go 1.23 and beyond
author: Robert Findley
url: https://go.dev/blog/gotelemetry
feedurl: https://go.dev/blog/feed.atom
content_mime_type: text/html
content:
<div id="blog"><div id="content">
  <div id="content">

    <div class="Article" data-slug="/blog/gotelemetry">
    
    <h1 class="small"><a href="/blog/">The Go Blog</a></h1>
    

    <h1>Telemetry in Go 1.23 and beyond</h1>
      
      <p class="author">
      Robert Findley<br>
      3 September 2024
      </p>
      
      <style type="text/css" scoped>
  #blog #content img#prompt {
    max-width: 500px;
  }
  .centered {
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .chart {
    width: 100%;
  }
  @media (prefers-color-scheme: dark) {
    .chart {
      border-radius: 8px;
    }
  }
  figure.captioned {
    display: table;
  }
  figure.captioned figcaption {
    display: table-caption;
    caption-side: bottom;
    font-style: italic;
    font-size: small;
    text-align: center;
  }
</style>
<p>Go 1.23 provides a new way for you to help improve the Go toolchain. By
enabling <a href="/doc/go1.23#telemetry">telemetry uploading</a>, you can elect to share
data about toolchain programs and their usage with the Go team. This data will
help Go contributors fix bugs, avoid regressions, and make better decisions.</p>
<p>By default, Go telemetry data is stored only on your local computer. If you
enable uploading, a <a href="/doc/telemetry#proposals">limited</a> subset of your data
is published weekly to <a href="https://telemetry.go.dev" rel="noreferrer" target="_blank">telemetry.go.dev</a>.</p>
<p>Starting with Go 1.23, you can enable uploading of your local telemetry data
with the following command:</p>
<pre><code>go telemetry on
</code></pre>
<p>To disable even local telemetry data collection, run the following command:</p>
<pre><code>go telemetry off
</code></pre>
<p>The <a href="/doc/telemetry">telemetry documentation</a> contains a more detailed
description of the implementation.</p>
<h2 id="a-brief-history-of-go-telemetry">A brief history of Go telemetry</h2>
<p>While software telemetry is not a new idea, the Go team went through many
iterations in search of a telemetry implementation that met Go&rsquo;s requirements
for performance, portability, and transparency.</p>
<p>The initial <a href="https://research.swtch.com/telemetry-design" rel="noreferrer" target="_blank">design</a> aimed to be
so unobtrusive, open, and privacy-preserving that it would be acceptable to
enable by default, but many users raised concerns in a lengthy <a href="/issue/58409">public
discussion</a>, and the design was ultimately
<a href="https://research.swtch.com/telemetry-opt-in#campaign" rel="noreferrer" target="_blank">changed</a> to require
explicit user consent for remote uploading.</p>
<p>The new design was <a href="/issue/58894">accepted</a> in April 2023, and implemented over
that summer.</p>
<h3 id="telemetry-in-gopls">Telemetry in gopls</h3>
<p>The first iteration of Go telemetry shipped in
<a href="https://github.com/golang/tools/releases/tag/gopls%2Fv0.14.0" rel="noreferrer" target="_blank">v0.14</a>
of the Go language server
<a href="https://go.googlesource.com/tools/+/refs/heads/master/gopls/" rel="noreferrer" target="_blank"><code>gopls</code></a>, in
October 2023. Following the launch, around 100 users enabled uploading,
perhaps motivated by release notes or discussion in the
<a href="https://gophers.slack.com/messages/gopls/" rel="noreferrer" target="_blank">Gophers Slack</a> channel, and data
started to trickle in. It wasn&rsquo;t long before telemetry found its first bug in
gopls:</p>
<div class="image">
<div class="centered">
<figure class="captioned">
<img src="gotelemetry/neat.png" alt="Telemetry found its first bug" />
<figcaption>
A stack trace Dan noticed in his uploaded telemetry data led to a bug being
reported and fixed. It's worth pointing out that we had no idea who had
reported the stack.
</figcaption>
</figure>
</div>
</div>
<h3 id="ide-prompting">IDE Prompting</h3>
<p>While it was great to see telemetry working in practice, and we appreciated the
support of those early adopters, 100 participants isn&rsquo;t enough to measure the
types of things we want to measure.</p>
<p>As Russ Cox <a href="https://research.swtch.com/telemetry-opt-in#campaign" rel="noreferrer" target="_blank">pointed out</a>
in his original blog posts, a drawback of the off-by-default approach for
telemetry is the continuous need to encourage participation. It takes outreach
to maintain a sample of users that&rsquo;s big enough for meaningful quantitative
data analysis, and representative of the user population. While blog posts and
release notes can boost participation (and we&rsquo;d appreciate it if you would
enable telemetry after reading this!), they lead to a skewed sample. For
example, we received almost no data for <code>GOOS=windows</code> from early adopters of
telemetry in gopls.</p>
<p>To help reach more users, we introduced a <a href="/doc/telemetry#ide">prompt</a> in the
<a href="https://marketplace.visualstudio.com/items?itemName=golang.go" rel="noreferrer" target="_blank">VS Code Go plugin</a>
asking users if they want to enable telemetry:</p>
<div class="image">
<div class="centered">
<figure class="captioned">
<img id="prompt" src="gotelemetry/prompt.png" alt="The VS Code prompt" />
<figcaption>
The telemetry prompt, as displayed by VS Code.
</figcaption>
</figure>
</div>
</div>
<p>As of this blog post, the prompt has rolled out to 5% of VS Code Go users, and
the telemetry sample has grown to around 1800 weekly participants:</p>
<div class="image">
<div class="centered">
<figure class="captioned">
<img src="gotelemetry/uploads.png" alt="Weekly Uploads vs Prompt Rate" class="chart"/>
<figcaption>Prompting helps reach more users.</figcaption>
</figure>
</div>
</div>
<p>(The initial bump is likely due to prompting <em>all</em> users of the
<a href="https://marketplace.visualstudio.com/items?itemName=golang.go-nightly" rel="noreferrer" target="_blank">VS Code Go nightly</a>
extension).</p>
<p>However, it has introduced a noticeable skew toward VS Code users, as compared
to the <a href="survey2024-h1-results.md">most recent Go survey results</a>:</p>
<div class="image">
<div class="centered">
<figure class="captioned">
<img src="gotelemetry/vscode_skew.png" alt="Skew toward VS Code users" class="chart"/>
<figcaption>We suspect that VS Code is overrepresented in telemetry data.</figcaption>
</figure>
</div>
</div>
<p>We&rsquo;re planning to address this skew by <a href="/issue/67821">prompting all LSP-capable editors that
use gopls</a>, using a feature of the language server protocol
itself.</p>
<h3 id="telemetry-wins">Telemetry wins</h3>
<p>Out of caution, we proposed the collection of only a few basic metrics for the
initial launch of telemetry in gopls. One of those was the
<a href="/issue/62249"><code>gopls/bug</code></a> <a href="/doc/telemetry#stack-counters">stack counter</a>,
which records unexpected or &ldquo;impossible&rdquo; conditions encountered by gopls. In
effect, it&rsquo;s a kind of assertion, but instead of stopping the program, it
records in telemetry that it was reached in some execution, along with the
stack.</p>
<p>During our <a href="gopls-scalability.md">gopls scalability</a> work, we had added many
assertions of this kind, but we rarely observed them to fail in tests or in our
own usage of gopls. We expected that almost all of these assertions were
unreachable.</p>
<p>As we started prompting random users in VS Code to enable telemetry, we saw
that many of these conditions <em>were</em> reached in practice, and the context of
the stack trace was often sufficient for us to reproduce and fix long-standing
bugs. We started collecting these issues under the
<a href="https://github.com/golang/go/issues?q=is%3Aissue+label%3Agopls%2Ftelemetry-wins" rel="noreferrer" target="_blank"><code>gopls/telemetry-wins</code></a>
label, to keep track of &ldquo;wins&rdquo; facilitated by telemetry.</p>
<p>I&rsquo;ve come to think of &ldquo;telemetry wins&rdquo; with a second meaning: when comparing
gopls development with and without telemetry, <em>telemetry wins</em>.</p>
<div class="image">
<div class="centered">
<figure class="captioned">
<img src="gotelemetry/telemetry_wins.png" alt="Telemetry wins."/>
<figcaption>Thank you Paul for the suggestions.</figcaption>
</figure>
</div>
</div>
<p>The most surprising aspect of the bugs coming from telemetry was how many of
them were <em>real</em>. Sure, some of them were invisible to users, but a good number
of them were actual misbehaviors of gopls&ndash;things like missing cross
references, or subtly inaccurate completion under certain rare conditions. They
were exactly the sort of things that a user might be mildly annoyed by but
probably wouldn&rsquo;t bother to report as an issue. Perhaps the user would assume
that the behavior was intended. If they did report an issue, they might not be
sure how to reproduce the bug, or we&rsquo;d need a long back-and-forth on the issue
tracker to capture a stack trace. Without telemetry, there is
<em>no reasonable way</em> that most of these bugs would have been discovered, much
less fixed.</p>
<p>And all this was from only a few counters. We&rsquo;d only instrumented stack traces
for the potential bugs <em>we knew about</em>. What about problems we didn&rsquo;t
anticipate?</p>
<h3 id="automated-crash-reporting">Automated crash reporting</h3>
<p>Go 1.23 includes a new
<a href="/doc/go1.23#runtimedebugpkgruntimedebug"><code>runtime.SetCrashOutput</code></a> API that
can be used to implement automated crash reporting via a watchdog process.
Starting with
<a href="https://github.com/golang/tools/releases/tag/gopls%2Fv0.15.0" rel="noreferrer" target="_blank">v0.15.0</a>, gopls
reports a <code>crash/crash</code> stack counter when it crashes, <em>provided gopls itself
is built with Go 1.23</em>.</p>
<p>When we released gopls@v0.15.0, only a handful of users in our sample had built
gopls using an unreleased development build of Go 1.23, yet the new
<code>crash/crash</code> counter still found
<a href="https://github.com/golang/tools/releases/tag/gopls%2Fv0.15.2" rel="noreferrer" target="_blank">two bugs</a>.</p>
<h2 id="telemetry-in-the-go-toolchain-and-beyond">Telemetry in the Go toolchain and beyond</h2>
<p>Given how useful telemetry has proven with only a tiny amount of
instrumentation and a fraction of our target sample, the future looks bright.</p>
<p>Go 1.23 records telemetry within the Go toolchain, including the <code>go</code> command
and other tools such as the compiler, linker, and <code>go vet</code>. We&rsquo;ve added
telemetry to <code>vulncheck</code> and the VS Code Go plugin, and
<a href="/issue/68384">we propose</a> to add it to <code>delve</code> as well.</p>
<p>The original telemetry blog series brainstorms
<a href="https://research.swtch.com/telemetry-uses" rel="noreferrer" target="_blank">many ideas</a> for how telemetry could
be used to improve Go. We&rsquo;re looking forward to exploring those ideas and more.</p>
<p>Within gopls, we plan to use telemetry to improve reliability and inform
decision making and prioritization. With the automated crash reporting enabled
by Go 1.23, we expect to catch many more crashes in prerelease testing. Going
forward, we&rsquo;ll add more counters to measure the user experience&ndash;latency of key
operations, frequency of use of various features&ndash;so that we can focus our
efforts where they will most benefit Go developers.</p>
<p>Go turns 15 this November, and both the language and its ecosystem continue to
grow. Telemetry will play a critical role in helping Go contributors move
faster and more safely, in the right direction.</p>

    </div>

    
    <div class="Article prevnext">
    
    
      
    
      
    
      
    
      
    
      
    
      
    
      
        <p>
        
          
            <b>Next article: </b><a href="/blog/survey2024-h2">Share your feedback about developing with Go</a><br>
          
        
        
          
            <b>Previous article: </b><a href="/blog/unique">New unique package</a><br>
          
        
        <b><a href="/blog/all">Blog Index</a></b>
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
    </div>
    

  </div>
</div>

<script src="/js/play.js"></script>


guid: tag:blog.golang.org,2013:blog.golang.org/survey2024-h2
title: Share your feedback about developing with Go
author: Alice Merrick, for the Go team
url: https://go.dev/blog/survey2024-h2
feedurl: https://go.dev/blog/feed.atom
content_mime_type: text/html
content:
<div id="blog"><div id="content">
  <div id="content">

    <div class="Article" data-slug="/blog/survey2024-h2">
    
    <h1 class="small"><a href="/blog/">The Go Blog</a></h1>
    

    <h1>Share your feedback about developing with Go</h1>
      
      <p class="author">
      Alice Merrick, for the Go team<br>
      9 September 2024
      </p>
      
      <p>We&rsquo;re excited to announce that the biannual Go Developer Survey is now open! From today, <strong>September 9th</strong>, until <strong>September 23rd</strong>, we invite you to share your experiences, opinions, and insights about the Go programming language and its ecosystem.</p>
<p>Your responses help us understand:</p>
<ul>
<li>How Go is being used across various industries and organizations</li>
<li>The challenges you face as a Go developer</li>
<li>The features and improvements you&rsquo;d like to see in future releases</li>
<li>How we can better support the thriving Go community</li>
</ul>
<p>Your participation directly influences the future of Go and ensures that the language evolves to meet the needs of developers worldwide. Please take 10-15 minutes to <a href="https://google.qualtrics.com/jfe/form/SV_ei0CDV2K9qQIsp8?s=b" rel="noreferrer" target="_blank">take the survey now!</a></p>
<p><strong>Spread the Word!</strong></p>
<p>Help us reach as many Go developers as possible! Share the survey with your colleagues, friends, and online communities. The more voices we hear, the better we can understand the diverse needs of the Go community.</p>
<p>Thank you for your time and feedback!</p>

    </div>

    
    <div class="Article prevnext">
    
    
      
    
      
    
      
    
      
    
      
    
      
        <p>
        
          
            <b>Next article: </b><a href="/blog/llmpowered">Building LLM-powered applications in Go</a><br>
          
        
        
          
            <b>Previous article: </b><a href="/blog/gotelemetry">Telemetry in Go 1.23 and beyond</a><br>
          
        
        <b><a href="/blog/all">Blog Index</a></b>
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
    </div>
    

  </div>
</div>

<script src="/js/play.js"></script>


guid: tag:blog.golang.org,2013:blog.golang.org/llmpowered
title: Building LLM-powered applications in Go
author: Eli Bendersky
url: https://go.dev/blog/llmpowered
feedurl: https://go.dev/blog/feed.atom
content_mime_type: text/html
content:
<div id="blog"><div id="content">
  <div id="content">

    <div class="Article" data-slug="/blog/llmpowered">
    
    <h1 class="small"><a href="/blog/">The Go Blog</a></h1>
    

    <h1>Building LLM-powered applications in Go</h1>
      
      <p class="author">
      Eli Bendersky<br>
      12 September 2024
      </p>
      
      <p>As the capabilities of LLMs (Large Language Models) and adjacent tools like
embedding models grew significantly over the past year, more and more developers
are considering integrating LLMs into their applications.</p>
<p>Since LLMs often require dedicated hardware and significant compute resources,
they are most commonly packaged as network services that provide APIs for
access. This is how the APIs for leading LLMs like OpenAI or Google Gemini work;
even run-your-own-LLM tools like <a href="https://ollama.com/" rel="noreferrer" target="_blank">Ollama</a> wrap
the LLM in a REST API for local consumption. Moreover, developers who take
advantage of LLMs in their applications often require supplementary tools like
Vector Databases, which are most commonly deployed as network services as
well.</p>
<p>In other words, LLM-powered applications are a lot like other modern
cloud-native applications: they require excellent support for REST and RPC
protocols, concurrency and performance. These just so happen to be the areas
where Go excels, making it a fantastic language for writing LLM-powered
applications.</p>
<p>This blog post works through an example of using Go for a simple LLM-powered
application. It starts by describing the problem the demo application is
solving, and proceeds by presenting several variants of the application that
all accomplish the same task, but use different packages to implement it. All
the code for the demos of this post
<a href="https://github.com/golang/example/tree/master/ragserver" rel="noreferrer" target="_blank">is available online</a>.</p>
<h2 id="a-rag-server-for-qa">A RAG server for Q&amp;A</h2>
<p>A common LLM-powered application technique is RAG -
<a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation" rel="noreferrer" target="_blank">Retrieval Augmented Generation</a>.
RAG is one of the most scalable ways of customizing an LLM&rsquo;s knowledge base
for domain-specific interactions.</p>
<p>We&rsquo;re going to build a <em>RAG server</em> in Go. This is an HTTP server that provides
two operations to users:</p>
<ul>
<li>Add a document to the knowledge base</li>
<li>Ask an LLM a question about this knowledge base</li>
</ul>
<p>In a typical real-world scenario, users would add a corpus of documents to
the server, and proceed to ask it questions. For example, a company can fill up
the RAG server&rsquo;s knowledge base with internal documentation and use it to
provide LLM-powered Q&amp;A capabilities to internal users.</p>
<p>Here&rsquo;s a diagram showing the interactions of our server with the external
world:</p>
<div class="image"><div class="centered">
<figure>
<img src="llmpowered/rag-server-diagram.png" alt="RAG server diagram"/>
</figure>
</div></div>
<p>In addition to the user sending HTTP requests (the two operations described
above), the server interacts with:</p>
<ul>
<li>An embedding model to calculate <a href="https://en.wikipedia.org/wiki/Sentence_embedding" rel="noreferrer" target="_blank">vector embeddings</a>
for the submitted documents and for user questions.</li>
<li>A Vector Database for storing and retrieving embeddings efficiently.</li>
<li>An LLM for asking questions based on context collected from the knowledge
base.</li>
</ul>
<p>Concretely, the server exposes two HTTP endpoints to users:</p>
<p><code>/add/: POST {&quot;documents&quot;: [{&quot;text&quot;: &quot;...&quot;}, {&quot;text&quot;: &quot;...&quot;}, ...]}</code>: submits
a sequence of text documents to the server, to be added to its knowledge base.
For this request, the server:</p>
<ol>
<li>Calculates a vector embedding for each document using the embedding model.</li>
<li>Stores the documents along with their vector embeddings in the vector DB.</li>
</ol>
<p><code>/query/: POST {&quot;content&quot;: &quot;...&quot;}</code>: submits a question to the server. For this
request, the server:</p>
<ol>
<li>Calculates the question&rsquo;s vector embedding using the embedding model.</li>
<li>Uses the vector DB&rsquo;s similarity search to find the most relevant documents
to the question in the knowledge database.</li>
<li>Uses simple prompt engineering to reformulate the question with the most
relevant documents found in step (2) as context, and sends it to the LLM,
returning its answer to the user.</li>
</ol>
<p>The services used by our demo are:</p>
<ul>
<li><a href="https://ai.google.dev/" rel="noreferrer" target="_blank">Google Gemini API</a> for the LLM and embedding model.</li>
<li><a href="https://weaviate.io/" rel="noreferrer" target="_blank">Weaviate</a> for a locally-hosted vector DB; Weaviate
is an open-source vector database
<a href="https://github.com/weaviate/weaviate" rel="noreferrer" target="_blank">implemented in Go</a>.</li>
</ul>
<p>It should be very simple to replace these by other, equivalent services. In
fact, this is what the second and third variants of the server are all about!
We&rsquo;ll start with the first variant which uses these tools directly.</p>
<h2 id="using-the-gemini-api-and-weaviate-directly">Using the Gemini API and Weaviate directly</h2>
<p>Both the Gemini API and Weaviate have convenient Go SDKs (client libraries),
and our first server variant uses these directly. The full code of this
variant is <a href="https://github.com/golang/example/tree/master/ragserver/ragserver" rel="noreferrer" target="_blank">in this directory</a>.</p>
<p>We won&rsquo;t reproduce the entire code in this blog post, but here are some notes
to keep in mind while reading it:</p>
<p><strong>Structure</strong>: the code structure will be familiar to anyone who&rsquo;s written an
HTTP server in Go. Client libraries for Gemini and for Weaviate are initialized
and the clients are stored in a state value that&rsquo;s passed to HTTP handlers.</p>
<p><strong>Route registration</strong>: the HTTP routes for our server are trivial to set up
using the <a href="/blog/routing-enhancements">routing enhancements</a> introduced in
Go 1.22:</p>
<pre><code class="language-Go">mux := http.NewServeMux()
mux.HandleFunc(&quot;POST /add/&quot;, server.addDocumentsHandler)
mux.HandleFunc(&quot;POST /query/&quot;, server.queryHandler)
</code></pre>
<p><strong>Concurrency</strong>: the HTTP handlers of our server reach out
to other services over the network and wait for a response. This isn&rsquo;t a problem
for Go, since each HTTP handler runs concurrently in its own goroutine. This
RAG server can handle a large number of concurrent requests, and the code of
each handler is linear and synchronous.</p>
<p><strong>Batch APIs</strong>: since an <code>/add/</code> request may provide a large number of documents
to add to the knowledge base, the server leverages <em>batch APIs</em> for both
embeddings (<code>embModel.BatchEmbedContents</code>) and the Weaviate DB
(<code>rs.wvClient.Batch</code>) for efficiency.</p>
<h2 id="using-langchain-for-go">Using LangChain for Go</h2>
<p>Our second RAG server variant uses LangChainGo to accomplish the same task.</p>
<p><a href="https://www.langchain.com/" rel="noreferrer" target="_blank">LangChain</a> is a popular Python framework for
building LLM-powered applications.
<a href="https://github.com/tmc/langchaingo" rel="noreferrer" target="_blank">LangChainGo</a> is its Go equivalent. The
framework has some tools to build applications out of modular components, and
supports many LLM providers and vector databases in a common API. This allows
developers to write code that may work with any provider and change providers
very easily.</p>
<p>The full code for this variant is <a href="https://github.com/golang/example/tree/master/ragserver/ragserver-langchaingo" rel="noreferrer" target="_blank">in this directory</a>.
You&rsquo;ll notice two things when reading the code:</p>
<p>First, it&rsquo;s somewhat shorter than the previous variant. LangChainGo takes care
of wrapping the full APIs of vector databases in common interfaces, and less
code is needed to initialize and deal with Weaviate.</p>
<p>Second, the LangChainGo API makes it fairly easy to switch providers. Let&rsquo;s say
we want to replace Weaviate by another vector DB; in our previous variant, we&rsquo;d
have to rewrite all the code interfacing the vector DB to use a new API. With
a framework like LangChainGo, we no longer need to do so. As long as LangChainGo
supports the new vector DB we&rsquo;re interested in, we should be able to replace
just a few lines of code in our server, since all the DBs implement a
<a href="https://pkg.go.dev/github.com/tmc/langchaingo@v0.1.12/vectorstores#VectorStore" rel="noreferrer" target="_blank">common interface</a>:</p>
<pre><code class="language-Go">type VectorStore interface {
    AddDocuments(ctx context.Context, docs []schema.Document, options ...Option) ([]string, error)
    SimilaritySearch(ctx context.Context, query string, numDocuments int, options ...Option) ([]schema.Document, error)
}
</code></pre>
<h2 id="using-genkit-for-go">Using Genkit for Go</h2>
<p>Earlier this year, Google introduced <a href="https://developers.googleblog.com/en/introducing-genkit-for-go-build-scalable-ai-powered-apps-in-go/" rel="noreferrer" target="_blank">Genkit for Go</a> -
a new open-source framework for building LLM-powered applications. Genkit shares
some characteristics with LangChain, but diverges in other aspects.</p>
<p>Like LangChain, it provides common interfaces that may be implemented by
different providers (as plugins), and thus makes switching from one to the other
simpler. However, it doesn&rsquo;t try to prescribe how different LLM components
interact; instead, it focuses on production features like prompt management and
engineering, and deployment with integrated developer tooling.</p>
<p>Our third RAG server variant uses Genkit for Go to accomplish the same task.
Its full code is <a href="https://github.com/golang/example/tree/master/ragserver/ragserver-genkit" rel="noreferrer" target="_blank">in this directory</a>.</p>
<p>This variant is fairly similar to the LangChainGo one - common interfaces for
LLMs, embedders and vector DBs are used instead of direct provider APIs, making
it easier to switch from one to another. In addition, deploying an LLM-powered
application to production is much easier with Genkit; we don&rsquo;t implement this
in our variant, but feel free to read <a href="https://firebase.google.com/docs/genkit-go/get-started-go" rel="noreferrer" target="_blank">the documentation</a>
if you&rsquo;re interested.</p>
<h2 id="summary---go-for-llm-powered-applications">Summary - Go for LLM-powered applications</h2>
<p>The samples in this post provide just a taste of what&rsquo;s possible for building
LLM-powered applications in Go. It demonstrates how simple it is to build
a powerful RAG server with relatively little code; most important, the samples
pack a significant degree of production readiness because of some fundamental
Go features.</p>
<p>Working with LLM services often means sending REST or RPC requests to a network
service, waiting for the response, sending new requests to other services based
on that and so on. Go excels at all of these, providing great tools for managing
concurrency and the complexity of juggling network services.</p>
<p>In addition, Go&rsquo;s great performance and reliability as a Cloud-native language
makes it a natural choice for implementing the more fundamental building blocks
of the LLM ecosystem. For some examples, see projects like
<a href="https://ollama.com/" rel="noreferrer" target="_blank">Ollama</a>, <a href="https://localai.io/" rel="noreferrer" target="_blank">LocalAI</a>,
<a href="https://weaviate.io/" rel="noreferrer" target="_blank">Weaviate</a> or <a href="https://zilliz.com/what-is-milvus" rel="noreferrer" target="_blank">Milvus</a>.</p>

    </div>

    
    <div class="Article prevnext">
    
    
      
    
      
    
      
    
      
    
      
        <p>
        
          
            <b>Next article: </b><a href="/blog/alias-names">What&#39;s in an (Alias) Name?</a><br>
          
        
        
          
            <b>Previous article: </b><a href="/blog/survey2024-h2">Share your feedback about developing with Go</a><br>
          
        
        <b><a href="/blog/all">Blog Index</a></b>
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
    </div>
    

  </div>
</div>

<script src="/js/play.js"></script>


guid: tag:blog.golang.org,2013:blog.golang.org/alias-names
title: What's in an (Alias) Name?
author: Robert Griesemer
url: https://go.dev/blog/alias-names
feedurl: https://go.dev/blog/feed.atom
content_mime_type: text/html
content:
<div id="blog"><div id="content">
  <div id="content">

    <div class="Article" data-slug="/blog/alias-names">
    
    <h1 class="small"><a href="/blog/">The Go Blog</a></h1>
    

    <h1>What&#39;s in an (Alias) Name?</h1>
      
      <p class="author">
      Robert Griesemer<br>
      17 September 2024
      </p>
      
      <p>This post is about generic alias types, what they are, and why we need them.</p>
<h2 id="background">Background</h2>
<p>Go was designed for programming at scale.
Programming at scale means dealing with large amounts of data, but
also large codebases, with many engineers working on those codebases
over long periods of time.</p>
<p>Go&rsquo;s organization of code into packages enables programming at scale
by splitting up large codebases into smaller, more manageable pieces,
often written by different people, and connected through public
APIs.
In Go, these APIs consist of the identifiers exported by a package:
the exported constants, types, variables, and functions.
This includes the exported fields of structs and methods of types.</p>
<p>As software projects evolve over time or requirements change,
the original organization of the code into packages may turn out to be
inadequate and require <em>refactoring</em>.
Refactoring may involve moving exported identifiers and their respective
declarations from an old package to a new package.
This also requires that any references to the moved declarations must be
updated so that they refer to the new location.
In large codebases it may be unpractical or infeasible to make such
a change atomically; or in other words, to do the move and update all clients
in a single change.
Instead, the change must happen incrementally: for instance, to &ldquo;move&rdquo;
a function <code>F</code>, we add its declaration in a new package without
deleting the original declaration in the old package.
This way, clients can be updated incrementally, over time.
Once all callers refer to <code>F</code> in the new package, the original declaration
of <code>F</code> may be safely deleted (unless it must be retained indefinitely, for
backward compatibility).
Russ Cox describes refactoring in detail in his 2016 article on
<a href="/talks/2016/refactor.article">Codebase Refactoring (with help from Go)</a>.</p>
<p>Moving a function <code>F</code> from one package to another while also retaining it
in the original package is easy: a wrapper function is all that&rsquo;s needed.
To move <code>F</code> from <code>pkg1</code> to <code>pkg2</code>, <code>pkg2</code> declares a new function <code>F</code>
(the wrapper function) with the same signature as <code>pkg1.F</code>, and <code>pkg2.F</code>
calls <code>pkg1.F</code>.
New callers may call <code>pkg2.F</code>, old callers may call <code>pkg1.F</code>, yet in both
cases the function eventually called is the same.</p>
<p>Moving constants is similarly straightforward.
Variables take a bit more work: one may have to introduce a pointer to the
original variable in the new package or perhaps use accessor functions.
This is less ideal, but at least it is workable.
The point here is that for constants, variables, and functions,
existing language features exist that permit incremental refactoring as
described above.</p>
<p>But what about moving a type?</p>
<p>In Go, the <a href="/ref/spec#Qualified_identifiers">(qualified) identifier</a>,
or just <em>name</em> for short, determines the <em>identity</em> of types:
a type <code>T</code> <a href="/ref/spec#Type_definitions">defined</a> and exported by a package
<code>pkg1</code> is <a href="/ref/spec#Type_identity">different</a> from an <em>otherwise identical</em>
type definition of a type <code>T</code> exported by a package <code>pkg2</code>.
This property complicates a move of <code>T</code> from one package to another while
retaining a copy of it in the original package.
For instance, a value of type <code>pkg2.T</code> is not <a href="/ref/spec#Assignability">assignable</a>
to a variable of type <code>pkg1.T</code> because their type names and thus their
type identities are different.
During an incremental update phase, clients may have values and variables
of both types, even though the programmer&rsquo;s intent is for them to have the
same type.</p>
<p>To solve this problem, <a href="/doc/go1.9">Go 1.9</a> introduced the notion of a
<a href="/ref/spec#Alias_declarations"><em>type alias</em></a>.
A type alias provides a new name for an existing type without introducing
a new type that has a different identity.</p>
<p>In contrast to a regular <a href="/ref/spec#Type_definitions">type definition</a></p>
<pre><code>type T T0
</code></pre>
<p>which declares a <em>new type</em> that is never identical to the type on the right-hand side
of the declaration, an <a href="/ref/spec#Alias_declarations">alias declaration</a></p>
<pre><code>type A = T  // the &quot;=&quot; indicates an alias declaration
</code></pre>
<p>declares only a <em>new name</em> <code>A</code> for the type on the right-hand side:
here, <code>A</code> and <code>T</code> denote the same and thus identical type <code>T</code>.</p>
<p>Alias declarations make it possible to provide a new name (in a new package!)
for a given type while retaining type identity:</p>
<pre><code>package pkg2

import &quot;path/to/pkg1&quot;

type T = pkg1.T
</code></pre>
<p>The type name has changed from <code>pkg1.T</code> to <code>pkg2.T</code> but values
of type <code>pkg2.T</code> have the same type as variables of type <code>pkg1.T</code>.</p>
<h2 id="generic-alias-types">Generic alias types</h2>
<p><a href="/doc/go1.18">Go 1.18</a> introduced generics.
Since that release, type definitions and function
declarations can be customized through type parameters.
For technical reasons, alias types didn&rsquo;t gain the same ability at that time.
Obviously, there were also no large codebases exporting generic
types and requiring refactoring.</p>
<p>Today, generics have been around for a couple of years, and large codebases
are making use of generic features.
Eventually the need will arise to refactor these codebases, and with that the
need to migrate generic types from one package to another.</p>
<p>To support incremental refactorings involving generic types, the future Go 1.24 release,
planned for early February 2025, will fully support type parameters on alias types
in accordance with proposal <a href="/issue/46477">#46477</a>.
The new syntax follows the same pattern as it does for type definitions and function declarations,
with an optional type parameter list following the identifier (the alias name) on the left-hand side.
Before this change one could only write:</p>
<pre><code>type Alias = someType
</code></pre>
<p>but now we can also declare type parameters with the alias declaration:</p>
<pre><code>type Alias[P1 C1, P2 C2] = someType
</code></pre>
<p>Consider the previous example, now with generic types.
The original package <code>pkg1</code> declared and exported a generic type <code>G</code> with a type parameter <code>P</code>
that is suitably constrained:</p>
<pre><code>package pkg1

type Constraint      someConstraint
type G[P Constraint] someType
</code></pre>
<p>If the need arises to provide access to the same type <code>G</code> from a new package <code>pkg2</code>,
a generic alias type is just the ticket <a href="/play/p/wKOf6NbVtdw?v=gotip">(playground)</a>:</p>
<pre><code>package pkg2

import &quot;path/to/pkg1&quot;

type Constraint      = pkg1.Constraint  // pkg1.Constraint could also be used directly in G
type G[P Constraint] = pkg1.G[P]
</code></pre>
<p>Note that one <strong>cannot</strong> simply write</p>
<pre><code>type G = pkg1.G
</code></pre>
<p>for a couple of reasons:</p>
<ol>
<li>
<p>Per <a href="/ref/spec#Type_definitions">existing spec rules</a>, generic
types must be <a href="/ref/spec#Instantiations">instantiated</a> when they
are <em>used</em>.
The right-hand side of the alias declaration uses the type <code>pkg1.G</code> and
therefore type arguments must be provided.
Not doing so would require an exception for this case, making the spec more
complicated.
It is not obvious that the minor convenience is worth the complication.</p>
</li>
<li>
<p>If the alias declaration doesn&rsquo;t need to declare its own type parameters and
instead simply &ldquo;inherits&rdquo; them from the aliased type <code>pkg1.G</code>, the declaration of
<code>G</code> provides no indication that it is a generic type.
Its type parameters and constraints would have to be retrieved from the declaration
of <code>pkg1.G</code> (which itself might be an alias).
Readability will suffer, yet readable code is one of the primary aims of the Go project.</p>
</li>
</ol>
<p>Writing down an explicit type parameter list may seem like an unnecessary burden
at first, but it also provides additional flexibility.
For one, the number of type parameters declared by the alias type doesn&rsquo;t have to
match the number of type parameters of the aliased type.
Consider a generic map type:</p>
<pre><code>type Map[K comparable, V any] mapImplementation
</code></pre>
<p>If uses of <code>Map</code> as sets are common, the alias</p>
<pre><code>type Set[K comparable] = Map[K, bool]
</code></pre>
<p>might be useful <a href="/play/p/IxeUPGCztqf?v=gotip">(playground)</a>.
Because it is an alias, types such as <code>Set[int]</code> and <code>Map[int, bool]</code> are
identical.
This would not be the case if <code>Set</code> were a <a href="/ref/spec#Type_definitions">defined</a>
(non-alias) type.</p>
<p>Furthermore, the type constraints of a generic alias type don&rsquo;t have to match the
constraints of the aliased type, they only have to
<a href="/ref/spec#Satisfying_a_type_constraint">satisfy</a> them.
For instance, reusing the set example above, one could define
an <code>IntSet</code> as follows:</p>
<pre><code>type integers interface{ ~int | ~int8 | ~int16 | ~int32 | ~int64 }
type IntSet[K integers] = Set[K]
</code></pre>
<p>This map can be instantiated with any key type that satisfies the <code>integers</code>
constraint <a href="/play/p/0f7hOAALaFb?v=gotip">(playground)</a>.
Because <code>integers</code> satisfies <code>comparable</code>, the type parameter <code>K</code> may be used
as type argument for the <code>K</code> parameter of <code>Set</code>, following the usual
instantiation rules.</p>
<p>Finally, because an alias may also denote a type literal, parameterized aliases
make it possible to create generic type literals
<a href="/play/p/wql3NJaUs0o?v=gotip">(playground)</a>:</p>
<pre><code>type Point3D[E any] = struct{ x, y, z E }
</code></pre>
<p>To be clear, none of these examples are &ldquo;special cases&rdquo; or somehow require
additional rules in the spec. They follow directly from the application of
the existing rules put in place for generics. The only thing that changed in the
spec is the ability to declare type parameters in an alias declaration.</p>
<h2 id="an-interlude-about-type-names">An interlude about type names</h2>
<p>Before the introduction of alias types, Go had only one form of type declarations:</p>
<pre><code>type TypeName existingType
</code></pre>
<p>This declaration creates a new and different type from an existing type
and gives that new type a name.
It was natural to call such types <em>named types</em> as they have a <em>type name</em>
in contrast to unnamed <a href="/ref/spec#Types">type literals</a> such as
<code>struct{ x, y int }</code>.</p>
<p>With the introduction of alias types in Go 1.9 it became possible to give
a name (an alias) to type literals, too. For instance, consider:</p>
<pre><code>type Point2D = struct{ x, y int }
</code></pre>
<p>Suddenly, the notion of a <em>named type</em> describing something that is different from
a type literal didn&rsquo;t make that much sense anymore, since an alias name clearly is
a name for a type, and thus the denoted type (which might be a type literal, not a type name!)
arguably could be called a &ldquo;named type&rdquo;.</p>
<p>Because (proper) named types have special properties (one can bind methods to them,
they follow different assignment rules, etc.), it seemed prudent to use a new
term in order to avoid confusions.
Thus, since Go 1.9, the spec calls the types formerly called named types <em>defined types</em>:
only defined types have properties (methods, assignability restrictions, etc) that are
tied to their names.
Defined types are introduced through type definitions, and alias types are
introduced through alias declarations.
In both cases, names are given to types.</p>
<p>The introduction of generics in Go 1.18 made things more complicated.
Type parameters are types, too, they have a name, and they share rules
with defined types.
For instance, like defined types, two differently named type parameters
denote different types.
In other words, type parameters are named types, and furthermore, they
behave similarly to Go&rsquo;s original named types in some ways.</p>
<p>To top things off, Go&rsquo;s predeclared types (<code>int</code>, <code>string</code> and so on)
can only be accessed through their names, and like defined types and
type parameters, are different if their names are different
(ignoring for a moment the <code>byte</code> and <code>rune</code> alias types).
The predeclared types truly are named types.</p>
<p>Therefore, with Go 1.18, the spec came full circle and formally
re-introduced the notion of a <a href="/ref/spec#Types">named type</a> which now
comprises &ldquo;predeclared types, defined types, and type parameters&rdquo;.
To correct for alias types denoting type literals the spec says:
&ldquo;An alias denotes a named type if the type given in the alias declaration
is a named type.&rdquo;</p>
<p>Stepping back and outside the box of Go nomenclature for a moment, the correct
technical term for a named type in Go is probably
<a href="https://en.wikipedia.org/wiki/Nominal_type_system" rel="noreferrer" target="_blank"><em>nominal type</em></a>.
A nominal type&rsquo;s identity is explicitly tied to its name which is exactly what
Go&rsquo;s named types (now using the 1.18 terminology) are all about.
A nominal type&rsquo;s behavior is in contrast to a <em>structural type</em> which has
behavior that only depends on its structure and not its name
(if it has one in the first place).
Putting it all together, Go&rsquo;s predeclared, defined, and type parameter types are
all nominal types, while Go&rsquo;s type literals and aliases denoting type literals
are structural types.
Both nominal and structural types can have names, but having a name
doesn&rsquo;t mean the type is nominal, it just means it is named.</p>
<p>None of this matters for day-to-day use of Go and in practice the details
can safely be ignored.
But precise terminology matters in the spec because it makes it easier
to describe the rules governing the language.
So should the spec change its terminology one more time?
It is probably not worth the churn: it is not just the spec that would need
to be updated, but also a lot of supporting documentation.
A fair number of books written on Go might become inaccurate.
Furthermore, &ldquo;named&rdquo;, while less precise, is probably intuitively clearer
than &ldquo;nominal&rdquo; for most people.
It also matches the original terminology used in the spec, even if it now
requires an exception for alias types denoting type literals.</p>
<h2 id="availability">Availability</h2>
<p>Implementing generic type aliases has taken longer than expected:
the necessary changes required adding a new exported <code>Alias</code> type
to <a href="/pkg/go/types"><code>go/types</code></a> and then adding the ability to record type parameters
with that type.
On the compiler side, the analogous changes also required modifications to
the export data format, the file format that describes a package&rsquo;s
exports, which now needs to be able to describe type parameters for aliases.
The impact of these changes is not confined to the compiler, but affects
clients of <code>go/types</code> and thus many third-party packages.
This was very much a change affecting a large code base; to avoid
breaking things, an incremental roll-out over several releases was necessary.</p>
<p>After all this work, generic alias types will finally be available by default in Go 1.24.</p>
<p>To allow third-party clients to get their code ready, starting with
Go 1.23, support for generic type aliases can be enabled by setting
<code>GOEXPERIMENT=aliastypeparams</code> when invoking the <code>go</code> tool.
However, be aware that support for exported generic aliases is still
missing for that version.</p>
<p>Full support (including export) is implemented at tip, and the default
setting for <code>GOEXPERIMENT</code> will soon be switched so that generic type
aliases are enabled by default.
Thus, another option is to experiement with the latest version of Go
at tip.</p>
<p>As always, please let us know if you encounter any problems by filing an
<a href="/issue/new">issue</a>;
the better we test a new feature, the smoother the general roll-out.</p>
<p>Thanks and happy refactoring!</p>

    </div>

    
    <div class="Article prevnext">
    
    
      
    
      
    
      
    
      
        <p>
        
          
            <b>Next article: </b><a href="/blog/15years">Go Turns 15</a><br>
          
        
        
          
            <b>Previous article: </b><a href="/blog/llmpowered">Building LLM-powered applications in Go</a><br>
          
        
        <b><a href="/blog/all">Blog Index</a></b>
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
    </div>
    

  </div>
</div>

<script src="/js/play.js"></script>


guid: tag:blog.golang.org,2013:blog.golang.org/15years
title: Go Turns 15
author: Austin Clements, for the Go team
url: https://go.dev/blog/15years
feedurl: https://go.dev/blog/feed.atom
content_mime_type: text/html
content:
<div id="blog"><div id="content">
  <div id="content">

    <div class="Article" data-slug="/blog/15years">
    
    <h1 class="small"><a href="/blog/">The Go Blog</a></h1>
    

    <h1>Go Turns 15</h1>
      
      <p class="author">
      Austin Clements, for the Go team<br>
      11 November 2024
      </p>
      
      <div style="float:right; margin: 0 0 1em 1em; width: 245px">
<img src="/doc/gopher/fifteen.gif" height="245" width="245"><br/>
<i>Thanks to Renee French for drawing and animating the gopher doing the “15 puzzle”.</i>
</div>
<p>Happy birthday, Go!</p>
<p>On Sunday, we celebrated the 15th anniversary of <a href="https://opensource.googleblog.com/2009/11/hey-ho-lets-go.html" rel="noreferrer" target="_blank">the Go open source
release</a>!</p>
<p>So much has changed since <a href="/blog/10years">Go&rsquo;s 10 year anniversary</a>,
both in Go and in the world. In other ways, so much has stayed the
same: Go remains committed to stability, safety, and supporting
software engineering and production at scale.</p>
<p>And Go is going strong! Go&rsquo;s user base has more than tripled in the
past five years, making it one of the fastest growing languages. From
its beginnings just fifteen years ago, Go has become a top 10 language
and the language of the modern cloud.</p>
<p>With the releases of <a href="/blog/go1.22">Go 1.22 in February</a> and <a href="/blog/go1.23">Go 1.23
in August</a>, it&rsquo;s been the year of <code>for</code> loops. Go 1.22
made variables introduced by <code>for</code> loops <a href="/blog/loopvar-preview">scoped per
iteration</a>, rather than per loop, addressing a
long-standing language &ldquo;gotcha&rdquo;. Over ten years ago, leading up to the
release of Go 1, the Go team made decisions about several language
details; among them whether <code>for</code> loops should create a new loop
variable on each iteration. Amusingly, the discussion was quite brief
and distinctly unopinionated. Rob Pike closed it out in true Rob Pike
fashion with a single word: “stet” (leave it be). And so it was. While
seemingly insignificant at the time, years of production experience
highlighted the implications of this decision. But in that time, we
also built robust tools for understanding the effects of changes to
Go—notably, ecosystem-wide analysis and testing across the entire
Google codebase—and established processes for working with the
community and getting feedback. Following extensive testing, analysis,
and community discussion, we rolled out the change, accompanied by a
<a href="https://go.googlesource.com/proposal/+/master/design/60078-loopvar.md#transition-support-tooling" rel="noreferrer" target="_blank">hash bisection
tool</a>
to assist developers in pinpointing code affected by the change at
scale.</p>
<p>The change to <code>for</code> loops was part of a five year trajectory of
measured changes. It would not have been possible without <a href="/blog/toolchain">forward
language compatibility</a> introduced in Go 1.21. This,
in turn, built upon the foundation laid by Go modules, which were
introduced in Go 1.14 four and a half years ago.</p>
<p>Go 1.23 further built on this change to introduce iterators and
<a href="/blog/range-functions">user-defined for-range loops</a>. Combined with
generics—introduced in Go 1.18, just two and a half years ago!—this
creates a powerful and ergonomic foundation for custom collections and
many other programming patterns.</p>
<p>These releases have also brought many improvements in production
readiness, including <a href="/blog/routing-enhancements">much-anticipated enhancements to the standard
library&rsquo;s HTTP router</a>, a <a href="/blog/execution-traces-2024">total overhaul
of execution traces</a>, and <a href="/blog/chacha8rand">stronger
randomness</a> for all Go applications. Additionally,
the introduction of our <a href="/blog/randv2">first v2 standard library
package</a> establishes a template for future library
evolution and modernization.</p>
<p>Over the past year we&rsquo;ve also been cautiously rolling out <a href="/blog/gotelemetry">opt-in
telemetry</a> for Go tools. This system will give Go&rsquo;s
developers data to make better decisions, while remaining completely
<a href="https://telemetry.go.dev/" rel="noreferrer" target="_blank">open</a> and anonymous. Go telemetry first
appeared in
<a href="https://github.com/golang/tools/blob/master/gopls/README.md" rel="noreferrer" target="_blank">gopls</a>,
the Go language server, where it has already led to a <a href="https://github.com/golang/go/issues?q=is%3Aissue+label%3Agopls%2Ftelemetry-wins" rel="noreferrer" target="_blank">litany of
improvements</a>.
This effort paves the way to make programming in Go an even better
experience for everyone.</p>
<p>Looking forward, we&rsquo;re evolving Go to better leverage the capabilities
of current and future hardware. Hardware has changed a lot in the past
15 years. In order to ensure Go continues to support high-performance,
large-scale production workloads for the <em>next</em> 15 years, we need to
adapt to large multicores, advanced instruction sets, and the growing
importance of locality in increasingly non-uniform memory hierarchies.
Some of these improvements will be transparent. Go 1.24 will have a
totally new <code>map</code> implementation under the hood that&rsquo;s more efficient
on modern CPUs. And we&rsquo;re prototyping new garbage collection
algorithms designed around the capabilities and constraints of modern
hardware. Some improvements will be in the form of new APIs and tools
so Go developers can better leverage modern hardware. We&rsquo;re looking at
how to support the latest vector and matrix hardware instructions, and
multiple ways that applications can build in CPU and memory locality.
A core principle guiding our efforts is <em>composable optimization</em>: the
impact of an optimization on a codebase should be as localized as
possible, ensuring that the ease of development across the rest of the
codebase is not compromised.</p>
<p>We&rsquo;re continuing to ensure Go&rsquo;s standard library is safe by default
and safe by design. This includes ongoing efforts to incorporate
built-in, native support for FIPS-certified cryptography, so that FIPS
crypto will be just a flag flip away for applications that need it.
Furthermore, we&rsquo;re evolving Go&rsquo;s standard library packages where we
can and, following the example of <code>math/rand/v2</code>, considering where
new APIs can significantly enhance the ease of writing safe and secure
Go code.</p>
<p>We&rsquo;re working on making Go better for AI—and AI better for Go—by
enhancing Go&rsquo;s capabilities in AI infrastructure, applications, and
developer assistance. Go is a great language for building production
systems, and we want it to be a great language for <a href="/blog/llmpowered">building
production <em>AI</em> systems</a>, too.
Go&rsquo;s dependability as a language
for Cloud infrastructure has made it a natural choice for
<a href="https://ollama.com/" rel="noreferrer" target="_blank">LLM</a> <a href="https://weaviate.io/" rel="noreferrer" target="_blank">infrastructure</a>
<a href="https://localai.io/" rel="noreferrer" target="_blank">as</a> <a href="https://zilliz.com/what-is-milvus" rel="noreferrer" target="_blank">well</a>.
For AI applications, we will continue building out first-class support
for Go in popular AI SDKs, including
<a href="https://pkg.go.dev/github.com/tmc/langchaingo" rel="noreferrer" target="_blank">LangChainGo</a> and
<a href="https://developers.googleblog.com/en/introducing-genkit-for-go-build-scalable-ai-powered-apps-in-go/" rel="noreferrer" target="_blank">Genkit</a>.
And from its very beginning, Go aimed to improve the end-to-end
software engineering process, so naturally we&rsquo;re looking at bringing
the latest tools and techniques from AI to bear on reducing developer
toil, leaving more time for the fun stuff—like actually programming!</p>
<h2 id="thank-you">Thank you</h2>
<p>All of this is only possible because of Go&rsquo;s incredible contributors
and thriving community. Fifteen years ago we could only dream of the
success that Go has become and the community that has developed around
Go. Thank you to everyone who has played a part, large and small. We
wish you all the best in the coming year.</p>

    </div>

    
    <div class="Article prevnext">
    
    
      
    
      
    
      
        <p>
        
          
            <b>Next article: </b><a href="/blog/protobuf-opaque">Go Protobuf: The new Opaque API</a><br>
          
        
        
          
            <b>Previous article: </b><a href="/blog/alias-names">What&#39;s in an (Alias) Name?</a><br>
          
        
        <b><a href="/blog/all">Blog Index</a></b>
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
    </div>
    

  </div>
</div>

<script src="/js/play.js"></script>


guid: tag:blog.golang.org,2013:blog.golang.org/protobuf-opaque
title: Go Protobuf: The new Opaque API
author: Michael Stapelberg
url: https://go.dev/blog/protobuf-opaque
feedurl: https://go.dev/blog/feed.atom
content_mime_type: text/html
content:
<div id="blog"><div id="content">
  <div id="content">

    <div class="Article" data-slug="/blog/protobuf-opaque">
    
    <h1 class="small"><a href="/blog/">The Go Blog</a></h1>
    

    <h1>Go Protobuf: The new Opaque API</h1>
      
      <p class="author">
      Michael Stapelberg<br>
      16 December 2024
      </p>
      
      <p>[<a href="https://en.wikipedia.org/wiki/Protocol_Buffers" rel="noreferrer" target="_blank">Protocol Buffers (Protobuf)</a>
is Google&rsquo;s language-neutral data interchange format. See
<a href="https://protobuf.dev/" rel="noreferrer" target="_blank">protobuf.dev</a>.]</p>
<p>Back in March 2020, we released the <code>google.golang.org/protobuf</code> module, <a href="/blog/protobuf-apiv2">a
major overhaul of the Go Protobuf API</a>. This
package introduced first-class <a href="https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect" rel="noreferrer" target="_blank">support for
reflection</a>,
a <a href="https://pkg.go.dev/google.golang.org/protobuf/types/dynamicpb" rel="noreferrer" target="_blank"><code>dynamicpb</code></a>
implementation and the
<a href="https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp" rel="noreferrer" target="_blank"><code>protocmp</code></a>
package for easier testing.</p>
<p>That release introduced a new protobuf module with a new API. Today, we are
releasing an additional API for generated code, meaning the Go code in the
<code>.pb.go</code> files created by the protocol compiler (<code>protoc</code>). This blog post
explains our motivation for creating a new API and shows you how to use it in
your projects.</p>
<p>To be clear: We are not removing anything. We will continue to support the
existing API for generated code, just like we still support the older protobuf
module (by wrapping the <code>google.golang.org/protobuf</code> implementation). Go is
<a href="/blog/compat">committed to backwards compatibility</a> and this
applies to Go Protobuf, too!</p>
<h2 id="background">Background: the (existing) Open Struct API</h2>
<p>We now call the existing API the Open Struct API, because generated struct types
are open to direct access. In the next section, we will see how it differs from
the new Opaque API.</p>
<p>To work with protocol buffers, you first create a <code>.proto</code> definition file like
this one:</p>
<pre><code>edition = &quot;2023&quot;;  // successor to proto2 and proto3

package log;

message LogEntry {
  string backend_server = 1;
  uint32 request_size = 2;
  string ip_address = 3;
}
</code></pre>
<p>Then, you <a href="https://protobuf.dev/getting-started/gotutorial/" rel="noreferrer" target="_blank">run the protocol compiler
(<code>protoc</code>)</a> to generate code
like the following (in a <code>.pb.go</code> file):</p>
<pre><code>package logpb

type LogEntry struct {
  BackendServer *string
  RequestSize   *uint32
  IPAddress     *string
  // …internal fields elided…
}

func (l *LogEntry) GetBackendServer() string { … }
func (l *LogEntry) GetRequestSize() uint32   { … }
func (l *LogEntry) GetIPAddress() string     { … }
</code></pre>
<p>Now you can import the generated <code>logpb</code> package from your Go code and call
functions like
<a href="https://pkg.go.dev/google.golang.org/protobuf/proto#Marshal" rel="noreferrer" target="_blank"><code>proto.Marshal</code></a>
to encode <code>logpb.LogEntry</code> messages into protobuf wire format.</p>
<p>You can find more details in the <a href="https://protobuf.dev/reference/go/go-generated/" rel="noreferrer" target="_blank">Generated Code API
documentation</a>.</p>
<h3 id="presence">(Existing) Open Struct API: Field Presence</h3>
<p>An important aspect of this generated code is how <em>field presence</em> (whether a
field is set or not) is modeled. For instance, the above example models presence
using pointers, so you could set the <code>BackendServer</code> field to:</p>
<ol>
<li><code>proto.String(&quot;zrh01.prod&quot;)</code>: the field is set and contains &ldquo;zrh01.prod&rdquo;</li>
<li><code>proto.String(&quot;&quot;)</code>: the field is set (non-<code>nil</code> pointer) but contains an
empty value</li>
<li><code>nil</code> pointer: the field is not set</li>
</ol>
<p>If you are used to generated code not having pointers, you are probably using
<code>.proto</code> files that start with <code>syntax = &quot;proto3&quot;</code>. The field presence behavior
changed over the years:</p>
<ul>
<li><code>syntax = &quot;proto2&quot;</code> uses <em>explicit presence</em> by default</li>
<li><code>syntax = &quot;proto3&quot;</code> used <em>implicit presence</em> by default (where cases 2 and 3
cannot be distinguished and are both represented by an empty string), but was
later extended to allow <a href="https://protobuf.dev/programming-guides/proto3/#field-labels" rel="noreferrer" target="_blank">opting into explicit presence with the <code>optional</code>
keyword</a></li>
<li><code>edition = &quot;2023&quot;</code>, the <a href="https://protobuf.dev/editions/overview/" rel="noreferrer" target="_blank">successor to both proto2 and
proto3</a>, uses <a href="https://protobuf.dev/programming-guides/field_presence/" rel="noreferrer" target="_blank"><em>explicit
presence</em></a> by default</li>
</ul>
<h2 id="opaqueapi">The new Opaque API</h2>
<p>We created the new <em>Opaque API</em> to uncouple the <a href="https://protobuf.dev/reference/go/go-generated/" rel="noreferrer" target="_blank">Generated Code
API</a> from the underlying
in-memory representation. The (existing) Open Struct API has no such separation:
it allows programs direct access to the protobuf message memory. For example,
one could use the <code>flag</code> package to parse command-line flag values into protobuf
message fields:</p>
<pre><code>var req logpb.LogEntry
flag.StringVar(&amp;req.BackendServer, &quot;backend&quot;, os.Getenv(&quot;HOST&quot;), &quot;…&quot;)
flag.Parse() // fills the BackendServer field from -backend flag
</code></pre>
<p>The problem with such a tight coupling is that we can never change how we lay
out protobuf messages in memory. Lifting this restriction enables many
implementation improvements, which we&rsquo;ll see below.</p>
<p>What changes with the new Opaque API? Here is how the generated code from the
above example would change:</p>
<pre><code>package logpb

type LogEntry struct {
  xxx_hidden_BackendServer *string // no longer exported
  xxx_hidden_RequestSize   uint32  // no longer exported
  xxx_hidden_IPAddress     *string // no longer exported
  // …internal fields elided…
}

func (l *LogEntry) GetBackendServer() string { … }
func (l *LogEntry) HasBackendServer() bool   { … }
func (l *LogEntry) SetBackendServer(string)  { … }
func (l *LogEntry) ClearBackendServer()      { … }
// …
</code></pre>
<p>With the Opaque API, the struct fields are hidden and can no longer be
directly accessed. Instead, the new accessor methods allow for getting, setting,
or clearing a field.</p>
<h3 id="lessmemory">Opaque structs use less memory</h3>
<p>One change we made to the memory layout is to model field presence for
elementary fields more efficiently:</p>
<ul>
<li>The (existing) Open Struct API uses pointers, which adds a 64-bit word to the
space cost of the field.</li>
<li>The Opaque API uses <a href="https://en.wikipedia.org/wiki/Bit_field" rel="noreferrer" target="_blank">bit
fields</a>, which require one bit per
field (ignoring padding overhead).</li>
</ul>
<p>Using fewer variables and pointers also lowers load on the allocator and on the
garbage collector.</p>
<p>The performance improvement depends heavily on the shapes of your protocol
messages: The change only affects elementary fields like integers, bools, enums,
and floats, but not strings, repeated fields, or submessages (because it is
<a href="https://protobuf.dev/reference/go/opaque-faq/#memorylayout" rel="noreferrer" target="_blank">less
profitable</a>
for those types).</p>
<p>Our benchmark results show that messages with few elementary fields exhibit
performance that is as good as before, whereas messages with more elementary
fields are decoded with significantly fewer allocations:</p>
<pre><code>             │ Open Struct API │             Opaque API             │
             │    allocs/op    │  allocs/op   vs base               │
Prod#1          360.3k ± 0%       360.3k ± 0%  +0.00% (p=0.002 n=6)
Search#1       1413.7k ± 0%       762.3k ± 0%  -46.08% (p=0.002 n=6)
Search#2        314.8k ± 0%       132.4k ± 0%  -57.95% (p=0.002 n=6)
</code></pre>
<p>Reducing allocations also makes decoding protobuf messages more efficient:</p>
<pre><code>             │ Open Struct API │             Opaque API            │
             │   user-sec/op   │ user-sec/op  vs base              │
Prod#1         55.55m ± 6%        55.28m ± 4%  ~ (p=0.180 n=6)
Search#1       324.3m ± 22%       292.0m ± 6%  -9.97% (p=0.015 n=6)
Search#2       67.53m ± 10%       45.04m ± 8%  -33.29% (p=0.002 n=6)
</code></pre>
<p>(All measurements done on an AMD Castle Peak Zen 2. Results on ARM and Intel
CPUs are similar.)</p>
<p>Note: proto3 with implicit presence similarly does not use pointers, so you will
not see a performance improvement if you are coming from proto3. If you were
using implicit presence for performance reasons, forgoing the convenience of
being able to distinguish empty fields from unset ones, then the Opaque API now
makes it possible to use explicit presence without a performance penalty.</p>
<h3 id="lazydecoding">Motivation: Lazy Decoding</h3>
<p>Lazy decoding is a performance optimization where the contents of a submessage
are decoded when first accessed instead of during
<a href="https://pkg.go.dev/google.golang.org/protobuf/proto#Unmarshal" rel="noreferrer" target="_blank"><code>proto.Unmarshal</code></a>. Lazy
decoding can improve performance by avoiding unnecessarily decoding fields which
are never accessed.</p>
<p>Lazy decoding can&rsquo;t be supported safely by the (existing) Open Struct API. While
the Open Struct API provides getters, leaving the (un-decoded) struct fields
exposed would be extremely error-prone. To ensure that the decoding logic runs
immediately before the field is first accessed, we must make the field private
and mediate all accesses to it through getter and setter functions.</p>
<p>This approach made it possible to implement lazy decoding with the Opaque
API. Of course, not every workload will benefit from this optimization, but for
those that do benefit, the results can be spectacular: We have seen logs
analysis pipelines that discard messages based on a top-level message condition
(e.g. whether <code>backend_server</code> is one of the machines running a new Linux kernel
version) and can skip decoding deeply nested subtrees of messages.</p>
<p>As an example, here are the results of the micro-benchmark we included,
demonstrating how lazy decoding saves over 50% of the work and over 87% of
allocations!</p>
<pre><code>                  │   nolazy    │                lazy                │
                  │   sec/op    │   sec/op     vs base               │
Unmarshal/lazy-24   6.742µ ± 0%   2.816µ ± 0%  -58.23% (p=0.002 n=6)

                  │    nolazy    │                lazy                 │
                  │     B/op     │     B/op      vs base               │
Unmarshal/lazy-24   3.666Ki ± 0%   1.814Ki ± 0%  -50.51% (p=0.002 n=6)

                  │   nolazy    │               lazy                │
                  │  allocs/op  │ allocs/op   vs base               │
Unmarshal/lazy-24   64.000 ± 0%   8.000 ± 0%  -87.50% (p=0.002 n=6)
</code></pre>
<h3 id="pointercomparison">Motivation: reduce pointer comparison mistakes</h3>
<p>Modeling field presence with pointers invites pointer-related bugs.</p>
<p>Consider an enum, declared within the <code>LogEntry</code> message:</p>
<pre><code>message LogEntry {
  enum DeviceType {
    DESKTOP = 0;
    MOBILE = 1;
    VR = 2;
  };
  DeviceType device_type = 1;
}
</code></pre>
<p>A simple mistake is to compare the <code>device_type</code> enum field like so:</p>
<pre><code>if cv.DeviceType == logpb.LogEntry_DESKTOP.Enum() { // incorrect!
</code></pre>
<p>Did you spot the bug? The condition compares the memory address instead of the
value. Because the <code>Enum()</code> accessor allocates a new variable on each call, the
condition can never be true. The check should have read:</p>
<pre><code>if cv.GetDeviceType() == logpb.LogEntry_DESKTOP {
</code></pre>
<p>The new Opaque API prevents this mistake: Because fields are hidden, all access
must go through the getter.</p>
<h3 id="accidentalsharing">Motivation: reduce accidental sharing mistakes</h3>
<p>Let&rsquo;s consider a slightly more involved pointer-related bug. Assume you are
trying to stabilize an RPC service that fails under high load. The following
part of the request middleware looks correct, but still the entire service goes
down whenever just one customer sends a high volume of requests:</p>
<pre><code>logEntry.IPAddress = req.IPAddress
logEntry.BackendServer = proto.String(hostname)
// The redactIP() function redacts IPAddress to 127.0.0.1,
// unexpectedly not just in logEntry *but also* in req!
go auditlog(redactIP(logEntry))
if quotaExceeded(req) {
    // BUG: All requests end up here, regardless of their source.
    return fmt.Errorf(&quot;server overloaded&quot;)
}
</code></pre>
<p>Did you spot the bug? The first line accidentally copied the pointer (thereby
sharing the pointed-to variable between the <code>logEntry</code> and <code>req</code> messages)
instead of its value. It should have read:</p>
<pre><code>logEntry.IPAddress = proto.String(req.GetIPAddress())
</code></pre>
<p>The new Opaque API prevents this problem as the setter takes a value
(<code>string</code>) instead of a pointer:</p>
<pre><code>logEntry.SetIPAddress(req.GetIPAddress())
</code></pre>
<h3 id="reflection">Motivation: Fix Sharp Edges: reflection</h3>
<p>To write code that works not only with a specific message type
(e.g. <code>logpb.LogEntry</code>), but with any message type, one needs some kind of
reflection. The previous example used a function to redact IP addresses. To work
with any type of message, it could have been defined as <code>func redactIP(proto.Message) proto.Message { … }</code>.</p>
<p>Many years ago, your only option to implement a function like <code>redactIP</code> was to
reach for <a href="/blog/laws-of-reflection">Go&rsquo;s <code>reflect</code> package</a>,
which resulted in very tight coupling: you had only the generator output and had
to reverse-engineer what the input protobuf message definition might have looked
like. The <a href="/blog/protobuf-apiv2"><code>google.golang.org/protobuf</code> module
release</a> (from March 2020) introduced
<a href="https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect" rel="noreferrer" target="_blank">Protobuf
reflection</a>,
which should always be preferred: Go&rsquo;s <code>reflect</code> package traverses the data
structure&rsquo;s representation, which should be an implementation detail. Protobuf
reflection traverses the logical tree of protocol messages without regard to its
representation.</p>
<p>Unfortunately, merely <em>providing</em> protobuf reflection is not sufficient and
still leaves some sharp edges exposed: In some cases, users might accidentally
use Go reflection instead of protobuf reflection.</p>
<p>For example, encoding a protobuf message with the <code>encoding/json</code> package (which
uses Go reflection) was technically possible, but the result is not <a href="https://protobuf.dev/programming-guides/proto3/#json" rel="noreferrer" target="_blank">canonical
Protobuf JSON
encoding</a>. Use the
<a href="https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson" rel="noreferrer" target="_blank"><code>protojson</code></a>
package instead.</p>
<p>The new Opaque API prevents this problem because the message struct fields are
hidden: accidental usage of Go reflection will see an empty message. This is
clear enough to steer developers towards protobuf reflection.</p>
<h3 id="idealmemory">Motivation: Making the ideal memory layout possible</h3>
<p>The benchmark results from the <a href="#lessmemory">More Efficient Memory
Representation</a> section have already shown that protobuf
performance heavily depends on the specific usage: How are the messages defined?
Which fields are set?</p>
<p>To keep Go Protobuf as fast as possible for <em>everyone</em>, we cannot implement
optimizations that help only one program, but hurt the performance of other
programs.</p>
<p>The Go compiler used to be in a similar situation, up until <a href="/blog/go1.20">Go 1.20 introduced
Profile-Guided Optimization (PGO)</a>. By recording the
production behavior (through <a href="/blog/pprof">profiling</a>) and feeding
that profile back to the compiler, we allow the compiler to make better
trade-offs <em>for a specific program or workload</em>.</p>
<p>We think using profiles to optimize for specific workloads is a promising
approach for further Go Protobuf optimizations. The Opaque API makes those
possible: Program code uses accessors and does not need to be updated when the
memory representation changes, so we could, for example, move rarely set fields
into an overflow struct.</p>
<h2 id="migration">Migration</h2>
<p>You can migrate on your own schedule, or even not at all—the (existing) Open
Struct API will not be removed. But, if you’re not on the new Opaque API, you
won’t benefit from its improved performance, or future optimizations that target
it.</p>
<p>We recommend you select the Opaque API for new development. Protobuf Edition
2024 (see <a href="https://protobuf.dev/editions/overview/" rel="noreferrer" target="_blank">Protobuf Editions Overview</a>
if you are not yet familiar) will make the Opaque API the default.</p>
<h3 id="hybridapi">The Hybrid API</h3>
<p>Aside from the Open Struct API and Opaque API, there is also the Hybrid API,
which keeps existing code working by keeping struct fields exported, but also
enabling migration to the Opaque API by adding the new accessor methods.</p>
<p>With the Hybrid API, the protobuf compiler will generate code on two API levels:
the <code>.pb.go</code> is on the Hybrid API, whereas the <code>_protoopaque.pb.go</code> version is
on the Opaque API and can be selected by building with the <code>protoopaque</code> build
tag.</p>
<h3 id="rewriting">Rewriting Code to the Opaque API</h3>
<p>See the <a href="https://protobuf.dev/reference/go/opaque-migration/" rel="noreferrer" target="_blank">migration
guide</a>
for detailed instructions. The high-level steps are:</p>
<ol>
<li>Enable the Hybrid API.</li>
<li>Update existing code using the <code>open2opaque</code> migration tool.</li>
<li>Switch to the Opaque API.</li>
</ol>
<h3 id="publishing">Advice for published generated code: Use Hybrid API</h3>
<p>Small usages of protobuf can live entirely within the same repository, but
usually, <code>.proto</code> files are shared between different projects that are owned by
different teams. An obvious example is when different companies are involved: To
call Google APIs (with protobuf), use the <a href="https://github.com/googleapis/google-cloud-go" rel="noreferrer" target="_blank">Google Cloud Client Libraries for
Go</a> from your project. Switching
the Cloud Client Libraries to the Opaque API is not an option, as that would be
a breaking API change, but switching to the Hybrid API is safe.</p>
<p>Our advice for such packages that publish generated code (<code>.pb.go</code> files) is to
switch to the Hybrid API please! Publish both the <code>.pb.go</code> and the
<code>_protoopaque.pb.go</code> files, please. The <code>protoopaque</code> version allows your
consumers to migrate on their own schedule.</p>
<h3 id="enablelazy">Enabling Lazy Decoding</h3>
<p>Lazy decoding is available (but not enabled) once you migrate to the Opaque API!
🎉</p>
<p>To enable: in your <code>.proto</code> file, annotate your message-typed fields with the
<code>[lazy = true]</code> annotation.</p>
<p>To opt out of lazy decoding (despite <code>.proto</code> annotations), the <a href="https://pkg.go.dev/google.golang.org/protobuf/runtime/protolazy" rel="noreferrer" target="_blank"><code>protolazy</code>
package
documentation</a>
describes the available opt-outs, which affect either an individual Unmarshal
operation or the entire program.</p>
<h2 id="nextsteps">Next Steps</h2>
<p>By using the open2opaque tool in an automated fashion over the last few years,
we have converted the vast majority of Google’s <code>.proto</code> files and Go code to
the Opaque API. We continuously improved the Opaque API implementation as we
moved more and more production workloads to it.</p>
<p>Therefore, we expect you should not encounter problems when trying the Opaque
API. In case you do encounter any issues after all, please <a href="https://github.com/golang/protobuf/issues/" rel="noreferrer" target="_blank">let us know on the
Go Protobuf issue tracker</a>.</p>
<p>Reference documentation for Go Protobuf can be found on <a href="https://protobuf.dev/reference/go/" rel="noreferrer" target="_blank">protobuf.dev → Go
Reference</a>.</p>

    </div>

    
    <div class="Article prevnext">
    
    
      
    
      
        <p>
        
          
            <b>Next article: </b><a href="/blog/survey2024-h2-results">Go Developer Survey 2024 H2 Results</a><br>
          
        
        
          
            <b>Previous article: </b><a href="/blog/15years">Go Turns 15</a><br>
          
        
        <b><a href="/blog/all">Blog Index</a></b>
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
      
    
    </div>
    

  </div>
</div>

<script src="/js/play.js"></script>


