[{"content":"After every race weekend on apexclub.live I used to do the same three things by hand. Enter the starting grid, enter the final classifications, trigger the scoring calculation. Every step was a few clicks and a few minutes. The annoying part wasn\u0026rsquo;t the time. It was the kind of work. It\u0026rsquo;s detectable, it\u0026rsquo;s repetitive, and getting it wrong silently corrupts a season\u0026rsquo;s standings. So I wanted it gone, but only on my terms.\nI wanted something autonomous enough that I didn\u0026rsquo;t have to remember. Deterministic enough that I could trust it. Small enough that I could still explain every line.\nTL;DR: A recurring worker tick runs a cheap Detect query for each registered use case. If there\u0026rsquo;s nothing to do, no LLM runs. If there\u0026rsquo;s a candidate, the engine builds a proposal, persists it, sends me a Telegram message with Approve / Reject buttons, and only on my tap calls a typed executor to apply the change. The LLM proposes. The database and the human approve. The executor writes.\nThe shape of the problem The original setup wasn\u0026rsquo;t bad. Race results came from analytics on Monday. The application had a Telegram bot I already trusted for one-off commands. I had a database I could read and write to. The only thing missing was glue.\nA naive first attempt was to let the existing chat bot propose mutations during a conversation. Press a button, the model picks a tool, the tool runs. I tried it and closed it. The problem wasn\u0026rsquo;t Telegram. It was mixing recurring operations with a conversational tool loop. Most ticks have nothing to do: the race hasn\u0026rsquo;t finished, analytics hasn\u0026rsquo;t synced, results already exist, scores are already published. Paying for an LLM call to discover that \u0026ldquo;nothing is ready\u0026rdquo; is wasteful, and worse, \u0026ldquo;the model decided to call this tool\u0026rdquo; is not an audit record I want to publish against a league\u0026rsquo;s standings.\nI wanted a concrete payload, a visible proposal, a named human decision, and a deterministic function that could be retried safely. So I split the work in five pieces:\na detect step that is just SQL, cheap and easy to reason about; a propose step that builds a structured payload and a human-readable summary; a persist step that stores the proposal before anything is sent anywhere; a human decision delivered through a channel I already trust (Telegram, in my case); a typed executor that applies an approved proposal and is required to be idempotent. Each piece is small. The interesting bit is the contract between them.\nThe contract A use case is anything that can detect a candidate, propose a payload, and apply it. The shape, in Go, is roughly:\n1 2 3 4 5 6 7 type UseCase interface { Kind() string Schedule() string Detect(ctx context.Context) ([]Candidate, error) Propose(ctx context.Context, c Candidate) (*Proposal, error) Execute(ctx context.Context, a *Action) (summary string, err error) } The four methods divide responsibility cleanly:\nDetect is a cheap precondition, usually a single SELECT. Propose turns one candidate into a structured payload and an admin-facing summary. Execute applies an approved action. Idempotency is required. Kind is the dispatch and dedup key. Schedule lives on the use case, not in central wiring. The engine has three pieces that move a use case through that contract:\nA runner that ticks on schedule, calls Detect, and for each candidate checks whether an action already exists for the same kind + subject reference. If not, it asks Propose to build a proposal and hands it to the coordinator. A coordinator that owns persistence and decisions. It stores the proposal, sends the notification, atomically claims an approval, dispatches the executor, and records the outcome. A registry that maps a kind such as insert_race_results to its executor. Registering the same kind twice panics at startup, which is the right moment to discover a wiring mistake. The lifecycle of a persisted action is small enough to draw in text:\n1 2 3 pending -\u0026gt; executing -\u0026gt; executed -\u0026gt; failed pending -\u0026gt; rejected A partial unique index allows only one open action per (kind, subject_ref). That single index is what makes the runner safe to call every 30 seconds across multiple application instances, and what makes a second admin press of \u0026ldquo;Approve\u0026rdquo; a no-op instead of a duplicate write.\nThe approval transport can be anything with two buttons. Telegram was the obvious choice because I was already living there for one-off commands. A web page could work just as well. It just has to call back into the same coordinator.\nThe flow, end to end Here is the whole path of one piece of work, written the way the code actually runs it:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 worker tick (every 30s) -\u0026gt; for each registered use case: Detect() with SQL -\u0026gt; zero candidates: stop -\u0026gt; candidate: is there already an action for (kind, subject)? yes: stop no: Propose(candidate) -\u0026gt; Proposal Coordinator.Submit(proposal) -\u0026gt; persist action with status=pending -\u0026gt; send approval message with action UID -\u0026gt; Telegram button press -\u0026gt; Coordinator.Approve(uid) (atomic: pending -\u0026gt; executing) -\u0026gt; Registry.Dispatch(kind).Execute(action) -\u0026gt; mark action executed or failed The decision to put deterministic work first is the entire insight. The LLM only runs after SQL has already said \u0026ldquo;there is something to do here\u0026rdquo;, and only to convert that \u0026ldquo;something\u0026rdquo; into a payload a human can review.\nWhy this set of qualities A few properties fall out of the shape, not from extra effort.\nWhen nothing needs doing, the worker runs only SELECT queries. No model call, no proposal, no notification. I called that \u0026ldquo;cheap idle ticks\u0026rdquo; above, and I underestimated how much it mattered before I built it. Most ticks are idle. Saving a model call on every idle tick is the difference between a system I forget exists and a system I worry about.\nAuditability is mostly free. Every action is a row: kind, subject reference, JSONB payload, the human-readable summary, the Telegram message identifiers, who decided it, when, and the final outcome. That\u0026rsquo;s a complete answer to \u0026ldquo;why did the standings change on Sunday at 23:14?\u0026rdquo;.\nThe executor contract is Execute(ctx, *Action). Rerunning it with the same action must converge to the same outcome. That requirement is what makes a button-mash, a network retry, or a panic mid-flight all safe. The row in pending / executing is the source of truth, and the executor decides whether to insert, update, or do nothing.\nThere is no global \u0026ldquo;AI: on\u0026rdquo; switch. The system is on whenever the worker is running with a configured approval channel. The absence of an admin to approve is the absence of writes.\nFailure modes are independent. Each use case has its own schedule and its own advisory lock. A broken results matcher doesn\u0026rsquo;t prevent scores from being checked.\nTwo use cases, end to end The same engine runs two unrelated capabilities on apexclub.live. They\u0026rsquo;re not hardcoded into the engine. They\u0026rsquo;re ordinary use cases that happen to be the first ones I needed.\nInsert race results The use case owns kind insert_race_results and schedule */30 * * * *.\nIts Detect query is \u0026ldquo;active-season races in the past with no row in results yet\u0026rdquo;. When that returns nothing, no LLM runs.\nWhen it returns a candidate, the use case:\nmatches the main race to a row in an analytics calendar (the LLM only picks from offered candidates, it can\u0026rsquo;t author the mapping); pulls qualifying pole and classified finishers from the analytics tables; resolves driver names to the application\u0026rsquo;s own grid entries (again, the LLM only picks from offered IDs); assembles a structured result plus the P1–P22 finishing order; refuses incomplete mappings and surfaces them as a manual-entry notice instead of a proposal; persists the payload for approval. On approval, Execute checks for an existing result before inserting and only writes final positions when none are already recorded. A retry after a partial failure converges without overwriting manual data.\nPublish scores The second use case owns kind publish_scores, also scheduled every 30 minutes. Its Detect query is \u0026ldquo;an active-season race with a results row, not yet finalized, no scores row, and at least one real starting position recorded\u0026rdquo;. Same shape, a SELECT that asks whether the next step\u0026rsquo;s preconditions are met.\nThe proposal payload is just a race identifier. On approval, the executor runs the existing scoring code and flips a finalized flag. Re-running the executor upserts and sets the same boolean, which converges safely.\nThere is no direct call between these two use cases. The database state is the hand-off: the results executor creates the data that makes scoring eligible on a later tick. Independent schedules, independent advisory locks, independent failure handling.\nThe traps, in order Building the engine was less work than fixing the things I got wrong the first time. A few worth naming.\nApproval state can stick. The first version moved an action from pending to executing before calling the executor, which prevented two button presses from racing. But if the executor panicked, the action stayed in executing, and because executing counted as open, the runner could never propose it again without a manual database update. The fix was to move the lifecycle into the coordinator: a panic is converted into an error, and Approve is the only transition that marks an action failed. Atomic claiming was only half the story. Every transition needs a recovery path.\nSource identities are fuzzy. The first attempt matched races by season and round number. Real data proved that wrong. For one British Grand Prix, the main calendar said round 11 while analytics said round 9, and analytics round 11 was a different race with no results yet. The LLM\u0026rsquo;s job in this system is to resolve exactly the kind of ambiguity that round numbers and slightly different race names cause. The authority stays narrow: the model picks from offered source rows, never authors them. Deterministic code then checks whether the chosen mapping is safe enough to propose.\nThe approval message is part of the safety boundary. A Telegram message that shows only the top three drivers is not a complete proposal if the scoring system needs the full finishing order. Anything required for approval belongs in deterministic presentation, not in prose the model is free to shorten. The pattern that worked: build the visible fields from the assembled payload in code, and append the model-generated summary as commentary. The fields are guaranteed. The commentary is colour.\nPreconditions encode data completeness. Scoring requires both a result and a starting grid. A result without a grid produces a plausible-looking but silently-wrong score. The Detect query for scoring was the right place to encode that requirement. The engine should refuse a valid-looking operation when its inputs would produce an incorrect result.\nThese weren\u0026rsquo;t architectural mistakes. They were the price of admitting the second iteration. The contract didn\u0026rsquo;t have to change. The implementations got better at honouring it.\nTradeoffs and limits A few honest costs.\nLatency is human-shaped. A use case that detects at 30 seconds and a human who approves five minutes later means a five-minute gap between readiness and execution. For race weekend admin that\u0026rsquo;s fine. For anything time-sensitive, propose-then-approve is the wrong shape.\nApproval delivery is one channel. Today the only way to approve is Telegram. If the notification fails to send, the action is persisted in pending and stays there until a human notices or a redelivery job is added. Persistence beats loss, but it\u0026rsquo;s not the same as a guaranteed delivery.\nOne primary approver. Notifications go to the first ID in the allowlist. Every allowlisted user can press the buttons, but only one gets the message. A real multi-admin workflow needs a delivery and decision policy, not just an allowlist.\nRejected actions don\u0026rsquo;t auto-retry. The repository layer suppresses future proposals for a rejected (kind, subject_ref) to avoid nagging. There is no \u0026ldquo;retry this one\u0026rdquo; operation that doesn\u0026rsquo;t also require a policy or data change. For one-shot work that\u0026rsquo;s the right default. For evolving inputs it can feel stubborn.\nFuzzy matching is bounded. The model can only pick from offered candidates. That\u0026rsquo;s a strong guardrail and also a real ceiling. When the input source gets a new spelling or a new driver, the matcher will refuse rather than guess. Human review remains mandatory.\nWhere I\u0026rsquo;d take it next The next changes I\u0026rsquo;d make are unglamorous:\nA redelivery job for pending actions whose Telegram message failed. A web approval page that calls the same coordinator. It doesn\u0026rsquo;t need to replace Telegram. It just needs to be a second transport. An explicit retry / reopen transition for rejected subjects, gated by policy rather than by data manipulation. More use cases. The shape proved out on results and scoring. The next two I\u0026rsquo;m eyeing are joker-round announcements and end-of-season standings exports. Both are detectable, both are one-shot, both have a payload I want to review before they go out. The thing I would not change is the order. Cheap detect, deterministic propose, human approve, typed execute. Each piece is replaceable. The order is the engine.\nThe engine is small because it refuses to solve every agent problem. It works for recurring, detectable, one-shot changes with a payload that can be reviewed before it\u0026rsquo;s applied. \u0026ldquo;Autonomous\u0026rdquo; here means the system notices and prepares work by itself. Authority to change league results still belongs to a person.\nWhat kind of recurring work would you trust it to propose, but not to approve?\n","date":"2026-07-14T14:00:00+01:00","image":"/posts/the-autonomous-bot-engine/autonomous-bot-engine.png","permalink":"/posts/the-autonomous-bot-engine/","title":"🤖 Building a propose-then-approve bot engine"},{"content":" \u0026ldquo;Messy code is a nuisance. \u0026ldquo;Tidying\u0026rdquo; code, to make it more readable, requires breaking it up into manageable sections. In this practical guide, author Kent Beck, creator of Extreme Programming and pioneer of software patterns, suggests when and where you might apply tidyings to improve your code while keeping the overall structure of the system in mind. (\u0026hellip;)\u0026rdquo; O\u0026rsquo;Reilly - Tidy First\nThe book is clear, objective, and easy to read. Its simple explanations and examples on empirical software design carry sophisticated concepts on evaluating when, how, what, and if to tidy at all.\nTL;DR: The book is an 8/10 and I recommend it to any programmer at any level of experience. It\u0026rsquo;s not a 10/10 because the first section about actual tydings with practical examples felt a bit unnecessary. I understand why it\u0026rsquo;s there, but the discussions presented in sections 2 and 3 were way more valuable for me. It might be the other way around depending on the experience though.\nSections The author split the book into three sections: Tidyngs, Managing, and Theory.\nTydings According to Kent Beck, tidyings are a subset of refactoring. Tydings are the cute, fuzzy little refactorings that nobody could possibly hate on. In this section, the author presents several tidyings that can be done to leave things better than before, e.g. adjusting the order based on reading or cohesion, improving comments, etc.\nFor people with little or no experience with refactorings, this is going to probably be the most valuable part. Personally, the \u0026ldquo;nobody could possibly hate on\u0026rdquo; piece, is the highlight of this section. It helped me to create a mental model to identify the line between a tidy and a \u0026ldquo;full-blown\u0026rdquo; refactor that would require much more work and discussion.\nManaging \u0026ldquo;Tyding is geek self-care\u0026rdquo; - Kent Beck, Tidy First?\nHere is where things started to become more interesting for me. This section addresses when to start and stop tidying, and more importantly how to combine tyding, changing the structure of the code, with changing the behavior of the system.\nIn this section is the reason why I\u0026rsquo;d love to change the title of this book is \u0026ldquo;First, After, Later, Never\u0026rdquo;. It provides a simple and objective guideline to decide when and if to tidy something. I won\u0026rsquo;t spoil it here so you can read this part from the book and reach your conclusions.\nTheory This was my favorite section and every chapter was very interesting to read, digest, and process. Particularly the correlation between software and cash flow versus options. This analogy helped me to think more structurally about the real cost of software, its development, and its evolution.\nAnother highlight of this section is having a clear distinction between behavior and structural changes. Where the behavior changes are the ones related to what the software does and structural changes are the ones that support what the software needs to do. E.g. software for HR offers features like payroll, holiday planning, etc. These features are the behaviors, anything else to support these features is structural, such as the code organization, how data is stored, retrieved, etc. Understanding this distinction, their costs, and values helps to better judge when to assume debt and plan to pay/tidy.\nConclusion This book is the first of a trilogy. The upcoming books are not yet released (as of the date of writing - 22 December 2023), and they will extend the scope of changes and impact. The first one is focused on the individual programmer (you), the second will be focused on the team, and the third will focus on all stakeholders.\n","date":"2023-12-26T14:00:00+01:00","image":"/posts/tidy-first-review/tidy_first.jpeg","permalink":"/posts/tidy-first-review/","title":"🧹 Tidy First? By Kent Beck - My review and takeaways"},{"content":"I participated in the Rinha de backend (pt-BR) or \u0026ldquo;Backend Rooster Fight\u0026rdquo; in English challenge and I was super excited to see the results of my super hacky and performant solution.\nThe results were scheduled to be published at 21:00 BRT (2 AM CEST my local time). So I stayed up til later that night grabbed popcorn, and waited for the results.\nWell, I wish I hadn\u0026rsquo;t waited\u0026hellip;\nTL;DR; My solution didn\u0026rsquo;t run on the official test server because I built the docker image to linux/arm64 instead of linux/amd64 🤦🤦🤦 . I fixed the issue and ran the same tests afterward and I reached ~50% of points of the winner, mainly because in my solution I didn\u0026rsquo;t tolerate any risk of losing writes. Checkout my repository flavio1110/rinha-de-backend with my solution.\nWhat was the Rinha de Backend? Before we get into the details, a brief explanation of what was the challenge.\nFrom July 28th to August 25th, the Backend Fight was held, a tournament in which the API that supported the most load during a stress test would be the winner. Participants had to implement an API with endpoints to create, query and search for \u0026lsquo;people\u0026rsquo; (a kind of CRUD without UPDATE and DELETE). In the tournament, participants still had to deal with CPU and memory restrictions – each participant had to deliver the API in docker-compose format and could only use 1.5 CPU units and 3GB of memory. More details on technical aspects can be found in the instructions) – (translated from its repository in English).\nOn top of the description above, some things important to highlight are:\nYou can write your API with any language or framework, as long as you can build a docker image out of it The solution has to run two instances of the API in parallel. The requests will be balanced as you wish between these instances via Nginx. You have to use one of the following data stores: MySQL, PostgreSQL, or MongoDB. It was a super fun, informal, practical, and a very good opportunity to exercise and learn new things.\nHow the winners were determined? To make the comparison easier and fun, the single parameter used to determine the winner of the challenge was the number of people inserted into the database.\nHow did I do? POV you are looking at me while I check the results. I didn\u0026rsquo;t. I was disqualified because the container with my APIs didn\u0026rsquo;t start. Therefore I got nothing. 🤦🤦🤦\nWrong platform? Yes, that\u0026rsquo;s true. My solution didn\u0026rsquo;t even run because it failed to start the APIs. I built the images for linux/arm64 instead of linux/amd64.\nIt was a mistake on my end, and it made me super disappointed. I ran the tests on the CI pipelines, and it gave me confidence the image was properly built and I didn\u0026rsquo;t double-check the requirements. Shame on me.\nWell, I was disqualified but I was curious to see how it would perform on a server with the same configuration as the one used for the official tests. So, I fixed the platform of the image, spun up an EC2 with the same configuration, and finally ran the tests. The results were a bit disappointing compared to the TOP 10.\nHow did I approach the problem? I had two constraints in mind while designing and building:\nIt has to work – each endpoint has to do what it is supposed to do. Keep things as simple as possible. Each of these points brought a few tradeoffs that ended up limiting the overall performance of the solution.\nYou can check my solution on flavio1110/rinha-de-backend. It\u0026rsquo;s built with Go and PostgreSQL.\nGiven many APIs were performing super well, a few days before the last day of the challenge, the load for the stress test was doubled, and it had a massive impact on the performance of my API. You can check the comparison below:\nBefore\u0026hellip;\nPrint of part of the report generated by Gatling of my solution run on Github BEFORE the load was doubled. After\u0026hellip;\nPrint of part of the report generated by Gatling of my solution run on Github AFTER the load was doubled. What were the bottlenecks? The inserts were the bottleneck. Because I didn\u0026rsquo;t tolerate the risk of losing writes and wanted to keep it simple, I missed the opportunity to introduce a distributed mechanism to validate the data before trying to insert it into the database and perform the inserts in batch.\nValidating the uniqueness of a field Apelido (Nickname) On my designed solution, without a distributed cache, it was not possible to perform without the DB in 100% of the cases.\nI had an in-memory cache, but if the nickname wasn\u0026rsquo;t there because it wasn\u0026rsquo;t there because the entry was inserted via the other instance, that request would reach the DB, occupy a connection, take resources, etc.\nIntroducing a distributed cache like Redis would enable me to add and check the entry in a single place. As a result, we could decrease the memory necessary to run the APIs and move to the cache.\nBatching inserts I inserted each entry per a valid request because I didn\u0026rsquo;t want to risk losing valid writes in case there was any error between the construction of the batch and its execution.\nThis was a huge problem because it used too many DB connections and resources, and slowed down the creation request.\nDid I like it? Despite my results, I loved the challenge! I learned quite some tricks with nginx config and some insteresting stuff like using Redis or even PostgreSQL as a \u0026ldquo;PubSub\u0026rdquo;. The interaction with the community via Twitter and Github was super nice! I also liked the fact of working with something challenging and closer to reality. Loved it!\nWhat could I have done differently? Build the image using the correct platform. A challenge is a challenge. I should have been more flexible with the writing. Tweak the Nginx configuration to load balance based on fewer connections and disable logs. Use a distributed cache and drop the read table. Batch inserts. What\u0026rsquo;s next? Check the official results and look into the repositories of the participants. I guarantee I\u0026rsquo;ll learn something new.\nStay tuned on @rinhadebackend for the next challenges, I can\u0026rsquo;t wait for the next ones.\nOther than that, I\u0026rsquo;ll apply the lessons learned to my solution and hopefully get better results. Watch flavio1110/rinha-de-backend and see how it will evolve.\nThis challenge is officially over, but you can still do it. Challenge accepted?\n","date":"2023-08-28T14:00:00+01:00","image":"/posts/rinha-de-backend/rooster_fight.png","permalink":"/posts/rinha-de-backend/","title":"🐔 What happened with my project on the Rinha de Backend challenge"},{"content":"Picture this, you have a critical and reasonably complicated piece of logic in your application that is handled in the database. Despite any change on it (or around it), you must have a 100% guarantee that piece continues to work just fine. So, what do you do?\nBack in the ancient days\u0026hellip; Ancient cave art of many hand prints. (Credit: Petr Kratochvila/Shutterstock) Before the \u0026ldquo;age of containers\u0026rdquo;, the solution would be a variant of the following:\nHave a database with the schema aligned with the application\u0026rsquo;s version. Ensure your CI tool has access to that database, to run the tests in your CI pipeline. Create a script to arrange the necessary data for the test. Write your testing using the DB. Create a script to revert any DB changes performed by the test, such as inserts, updates, etc. This is a very nice post about the Docker: Life Before and after.\nWhat\u0026rsquo;s the problem with testing with a shared database? Simply put, it\u0026rsquo;s complex, expensive to maintain, and has many other reasons to fail. For example:\nThe DB\u0026rsquo;s schema version is not the same as the application\u0026rsquo;s version you want to test. There is a change in the network policy that blocks access to the database. Someone\u0026rsquo;s test setup corrupted the data that you expected to have, so you arrange script fails, or the test fails because of missing data. Because of the cost and flakiness of such tests, in many cases, the solution was to move logic from DB to the application\u0026rsquo;s code when performance was not a problem, or just manually test it from time to time and hope it doesn\u0026rsquo;t break because of an unforeseen reason (it-never-happened-in-the-last-30-minutes).\nContainers everywhere With the popularization of containers, not only developing, runnings, and deploying became easier, but also integrating and testing your external dependencies became easier, faster, and more dependable.\nWe can now effortlessly run not only the regular dependencies, like RDBMSs and Message Brokers, but we can emulate many cloud provider services using tools like localstack.\nWhat about testing? Testcontainers to rescue! Testcontainers makes it simple to create and clean up container-based dependencies for automated integration/smoke tests. The clean, easy-to-use API enables developers to programmatically define containers that should be run as part of a test and clean up those resources when the test is done.\nIt supports many popular languages/frameworks like Go, Java, .NET, Rust, and others. You can check the full list on its official site](https://www.testcontainers.org/) and because it\u0026rsquo;s open source, you check out and contribute to it in its repositories.\nHow to use Testcontainers on integration tests? For this example, assume that one of the features of your software is to search for users based on a few criteria. Your users are stored in a PostgreSQL database and you query them using SQL. Something like:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func searchPeople(ctx context.Context, db *sql.DB, params searchParams) ([]person, error) { query := `select first_name, last_name, city from people where ($1::text is null or first_name = $1) and ($2::text is null or last_name = $2) and ($3::text is null or city = $3) order by first_name asc` var people []person //Execute query, check errors, populate people variable return people, nil } The search criteria will evolve, by adding new filters or adding new features like pagination. Therefore, we need to make sure that whenever we touch this query its existing behavior is not broken. Let\u0026rsquo;s write a test for it using Testcontainers.\nWriting the test To be able to run these tests against a real database, we need to:\nStart the DB Create the schema Arrange data, Act, and Assert Let\u0026rsquo;s see in detail how to perform each one of these steps.\n1. Start the DB Let\u0026rsquo;s create a function that will start a DB, return its connection string and a function to terminate it when we are done.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 func startTestDB(ctx context.Context) (string, func(t *testing.T), error) { var envVars = map[string]string{ \u0026#34;POSTGRES_USER\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;POSTGRES_PASSWORD\u0026#34;: \u0026#34;super-secret\u0026#34;, \u0026#34;POSTGRES_DB\u0026#34;: \u0026#34;people\u0026#34;, \u0026#34;PORT\u0026#34;: \u0026#34;5432/tcp\u0026#34;, } getConnString := func(host string, port nat.Port) string { return fmt.Sprintf(\u0026#34;postgres://%s:%s@%s:%s/%s?sslmode=disable\u0026#34;, envVars[\u0026#34;POSTGRES_USER\u0026#34;], envVars[\u0026#34;POSTGRES_PASSWORD\u0026#34;], host, port.Port(), envVars[\u0026#34;POSTGRES_DB\u0026#34;]) } req := testcontainers.ContainerRequest{ Image: \u0026#34;postgres:14\u0026#34;, ExposedPorts: []string{envVars[\u0026#34;PORT\u0026#34;]}, Env: envVars, WaitingFor: wait.ForSQL(nat.Port(envVars[\u0026#34;PORT\u0026#34;]), \u0026#34;pgx\u0026#34;, getConnString). WithStartupTimeout(time.Second * 15), } pgC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) if err != nil { return \u0026#34;\u0026#34;, nil, fmt.Errorf(\u0026#34;failed to start db container :%w\u0026#34;, err) } port, err := pgC.MappedPort(ctx, \u0026#34;5432/tcp\u0026#34;) if err != nil { return \u0026#34;\u0026#34;, nil, fmt.Errorf(\u0026#34;failed to get mapped port :%w\u0026#34;, err) } host, err := pgC.Host(ctx) if err != nil { return \u0026#34;\u0026#34;, nil, fmt.Errorf(\u0026#34;failed to get host :%w\u0026#34;, err) } connString := fmt.Sprintf(\u0026#34;postgres://%s:%s@%s:%d/%s?sslmode=disable\u0026#34;, envVars[\u0026#34;POSTGRES_USER\u0026#34;], envVars[\u0026#34;POSTGRES_PASSWORD\u0026#34;], host, port.Int(), envVars[\u0026#34;POSTGRES_DB\u0026#34;]) terminate := func(t *testing.T) { if err := pgC.Terminate(ctx); err != nil { t.Fatalf(\u0026#34;failed to terminate container: %s\u0026#34;, err.Error()) } } return connString, terminate, nil } The code above is self-explanatory if you are familiar with Go, but there are a few aspects that I\u0026rsquo;d like to highlight:\nTestcontainers will map and assign a random port. Therefore, in this case, instead of connecting to 5432, it\u0026rsquo;s necessary to connect to the assigned port. This is similar to the hostname. Therefore, to get your correct connection string it\u0026rsquo;s needed to resolve port and host.\nThe container will automatically shut down when the application is finished. However, it is recommended to defer its termination as soon as it is started. For that, a terminate func is returned by the method above.\nSo far our test looks like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func TestSearchPeople(t *testing.T) { ctx := context.Background() connString, terminate, err := startTestDB(ctx) if err != nil { t.Error(err) } defer terminate(t) db, err := openDB(connString) if err != nil { t.Fatal(\u0026#34;fail to open DB\u0026#34;, err) } // Continue } 2. Create the schema With a DB up and running we can initiate it with the expected schema, a.k.a migrate the DB schema.\nIn the real world, we would use something more sophisticated like go-migrate, but for the sake of simplicity let\u0026rsquo;s write something simpler.\n1 2 3 4 5 6 7 func migrateDB(ctx context.Context, db *sql.DB) error { _, err := db.ExecContext(ctx, \u0026#34;create table if not exists people (first_name text,last_name text,city text)\u0026#34;) if err != nil { return fmt.Errorf(\u0026#34;failed to migrate DB: %w\u0026#34;, err) } return nil } 3. Arrange data, Act, and Assert The last step before the actual test is to arrange the data. We could accomplish it together with the schema migration, however keeping these two parts separate helps to keep each test isolated, avoiding side effects when the data is tweaked.\nFor our tests, we need to insert 4 different people with distinct names and cities. Then we can use this data to act and assert the results. This is what the final test func looks like, testing every variation of our query.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 func TestSearchPeople(t *testing.T) { ctx := context.Background() // Start the DB connString, terminate, err := startTestDB(ctx) if err != nil { t.Error(err) } // Terminate the container when the func TestSearchPeople finishes defer terminate(t) // Open the the database db, err := openDB(connString) if err != nil { t.Fatal(\u0026#34;fail to open DB\u0026#34;, err) } // Migrate schema if err := migrateDB(ctx, db); err != nil { t.Fatal(\u0026#34;fail to migrate DB\u0026#34;, err) } insert := `insert into people values (\u0026#39;Flavio\u0026#39;, \u0026#39;Silva\u0026#39;, \u0026#39;Olbia\u0026#39;), (\u0026#39;Joost\u0026#39;, \u0026#39;Van Huis\u0026#39;, \u0026#39;Amsterdam\u0026#39;), (\u0026#39;Aldben\u0026#39;, \u0026#39;Arimeritin\u0026#39;, \u0026#39;Istambul\u0026#39;), (\u0026#39;Nando\u0026#39;, \u0026#39;Pelect\u0026#39;, \u0026#39;Perth\u0026#39;);` // Arrange data if _, err := db.ExecContext(ctx, insert); err != nil { t.Fatal(\u0026#34;fail to insert initial data\u0026#34;, err) } // Several test cases t.Run(\u0026#34;without filters\u0026#34;, func(t *testing.T) { people, err := searchPeople(ctx, db, searchParams{}) assert.NoError(t, err) assert.Equal(t, 4, len(people)) }) t.Run(\u0026#34;filtering by first name\u0026#34;, func(t *testing.T) { people, err := searchPeople(ctx, db, searchParams{firstName: strPtr(\u0026#34;Joost\u0026#34;)}) assert.NoError(t, err) assert.Equal(t, 1, len(people)) assert.Equal(t, \u0026#34;Joost\u0026#34;, people[0].firstName) }) t.Run(\u0026#34;filtering by last name\u0026#34;, func(t *testing.T) { people, err := searchPeople(ctx, db, searchParams{lastName: strPtr(\u0026#34;Arimeritin\u0026#34;)}) assert.NoError(t, err) assert.Equal(t, 1, len(people)) assert.Equal(t, \u0026#34;Arimeritin\u0026#34;, people[0].lastName) }) t.Run(\u0026#34;filtering by city\u0026#34;, func(t *testing.T) { people, err := searchPeople(ctx, db, searchParams{city: strPtr(\u0026#34;Olbia\u0026#34;)}) assert.NoError(t, err) assert.Equal(t, 1, len(people)) assert.Equal(t, \u0026#34;Olbia\u0026#34;, people[0].city) }) } With this test, we have the guarantee the covered scenarios are working. Profit!\nYou can check the results by running the tests.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 go test ./... -v === RUN TestSearchPeople 2023/05/09 15:48:55 github.com/testcontainers/testcontainers-go - Connected to docker: Server Version: 20.10.22 API Version: 1.41 Operating System: Docker Desktop Total Memory: 3932 MB 2023/05/09 15:48:55 Starting container id: 95dc2928dae3 image: docker.io/testcontainers/ryuk:0.3.4 2023/05/09 15:48:56 Waiting for container id 95dc2928dae3 image: docker.io/testcontainers/ryuk:0.3.4 2023/05/09 15:48:56 Container is ready id: 95dc2928dae3 image: docker.io/testcontainers/ryuk:0.3.4 2023/05/09 15:48:56 Starting container id: adedca079bc6 image: postgres:14 2023/05/09 15:48:57 Waiting for container id adedca079bc6 image: postgres:14 2023/05/09 15:48:58 Container is ready id: adedca079bc6 image: postgres:14 === RUN TestSearchPeople/without_filters === RUN TestSearchPeople/filtering_by_first_name === RUN TestSearchPeople/filtering_by_last_name === RUN TestSearchPeople/filtering_by_city --- PASS: TestSearchPeople (3.48s) --- PASS: TestSearchPeople/without_filters (0.00s) --- PASS: TestSearchPeople/filtering_by_first_name (0.00s) --- PASS: TestSearchPeople/filtering_by_last_name (0.00s) --- PASS: TestSearchPeople/filtering_by_city (0.00s) PASS ok github.com/flavio1110/go-for-csharp-devs/experiments/test-db-interactions\t.724s You can check the source code of this test on my experiments repository.\nFinal thoughts Despite Testcontainers reducing the cost of integration tests and making them more reliable, it doesn\u0026rsquo;t remove the cost entirely. The tests will take more time to run (especially if where you are running the tests doesn\u0026rsquo;t have the image downloaded yet).\nNevertheless, in my opinion, the price is low compared to the benefit that it yields.\nWhat about you? How do you write integration tests, and what are your main challenges?\n","date":"2023-04-17T14:00:00+01:00","image":"/posts/test-db-integrations-with-testcontainers/logo.png","permalink":"/posts/test-db-integrations-with-testcontainers/","title":"🔥 Test DB integrations with Testcontainers"},{"content":" When studying HTML, CSS, and Javascript it\u0026rsquo;s hard to practice with real-world examples, so oftentimes we get stuck trying to come up with a good design and then building it.\nFrontend Mentor to the recue! Frontend Mentor offers front-end coding challenges and interesting projects to practice your HTML, CSS, and JavaScript. It is as close as it gets to work on a professional real-world project.\nAfter creating your account, you can start one of its challeges. There are many free challenges that can get you started and give you plenty of things to play with. However, if you want take one step further, have access to premium challenges, figma and sketch design files, and private solutions, you can subscribe to the pro developer.\nHow does it work? Once you start a challenge, you will download a zip for that challenge. For example the challenge Four card feature section, will give you a zip file containing the instructions, necessary images, basic design details, and the most important the design images for desktop and mobile!\nThese files are all you need to start hacking your front end!\nAfter the challenge is complete you can submit it and get feedback from the community! That\u0026rsquo;s excellent!\nI\u0026rsquo;ve been playing with some challenges myself, (the image of the banner is from my own implementation of the Four Cards feature section).\nI\u0026rsquo;m not a Frontend specialist by no means, but you can find my solutions so far on my GitHub repository.\nWhat about you? Do you know any other tool that offers such an immersive and near-real-world experience?\n","date":"2023-04-11T14:00:00+01:00","image":"/posts/improve-your-fe-skills-with-frontendmentor/four-card-feature-files.webp","permalink":"/posts/improve-your-fe-skills-with-frontendmentor/","title":"🔥 Improve your Frontend skills with Frontend Mentor"},{"content":"got is a CLI written in Go, created on top of the git CLI, to make my life easier by shortening some commands I use daily.\nBecause it\u0026rsquo;s built on top of git, all git commands will also work just fine and I don\u0026rsquo;t need to keep switching between got and git!\nIf I want to clean up my local dead branches I can got rmb instead of git branch | grep -v \u0026quot;main\u0026quot; | xargs git branch -D (you can call me lazy. I accept that 😅).\nxkcd Automation.\nBut\u0026hellip; Why? My first experience with a Distributed version control system (DVCS), was many years ago with mercurial hg, and despite taking some time to get used to the new way of working as compared to a centralized version control system, I got very used to its short commands, aliases, and simplicity. We could do something like hg pull or hg pul (that\u0026rsquo;s not a typo. This is an actual alias for hg pull), we were able to close a branch and commit a message in a single command like hg commit --close-branch -m 'closing this branch'. It was super nice and handy! Then, eventually, I started working with git and I was amazed by its differences, possibilities, and features. However, it felt more verbose for day-to-day tasks. So I ended up creating a bunch of aliases for the commands that I use more often, or commands that are longer and I always have to google it to remember. Sneak peek of my ~/.zshrc\n1 2 3 4 5 6 7 #... alias push=\u0026#39;git push origin head\u0026#39; alias stat=\u0026#39;git status -s\u0026#39; alias gbr=\u0026#34;git branch | grep -v \u0026#34;main\u0026#34; | xargs git branch -D\u0026#34; alias pick=\u0026#34;git cherry-pick\u0026#34; # and a few more... #... Speaking on zsh, if you use it with the go plugin, you will have a conflict with the alias got for go test.\nThe aliases work so well and are easy to maintain\u0026hellip; But I wanted something fancier and all have the opportunity to go through the process of writing a CLI in Go.\nHow Got is built using cobra, which makes the work so much easier. Cobra has its own CLI called Cobra Generator that helps bootstrap your CLI project and add commands. You can check the full documentation here, but here goes the basic usage:\nInstall With go installed, open the terminal and execute the following command:\n1 go install github.com/spf13/cobra-cli@latest Booststraping your CLI Navigate to a folder that you want to have your project, init a go module, then execute the cobra init. e.g\n1 2 go mod init my-cli cobra-cli init Voilá! You have your custom CLI, ad you can run it with go run .! You will see a result like:\n1 2 3 4 5 6 A longer description that spans multiple lines and likely contains examples and usage of using your application. For example: Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application. You can now add a command with executing cobra-cli add ping, then execute the command again. You will see a result like:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 A longer description that spans multiple lines and likely contains examples and usage of using your application. For example: Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application. Usage: my-cli [command] Available Commands: completion Generate the autocompletion script for the specified shell help Help about any command ping A brief description of your command Flags: -h, --help help for my-cli -t, --toggle Help message for toggle Use \u0026#34;my-cli [command] --help\u0026#34; for more information about a command. The foundation for your CLI is in place, you only need to worry about the actual logic of the commands because cobra will take care of all the pumbling for you.\nYou can check several examples in the cobra documentation, and use it as a base to create your shine CLI.\nOk, what about the logic in got? In got I have two ways of executing some logic. 1) using go-git to perform some actions like iterate in all local branches and delete all except main, and 2) executing a git cli command from my Go application directly.\ngo-git go-git is a highly extensible git implementation library written in pure Go. It can be used to manipulate git repositories at low level (plumbing) or high level (porcelain), through an idiomatic Go API. - (Description from its repo).\nIt is such a well-documented, flexible, and powerful lib. I highly recommend looking into it if you even thought to do something with git, or if you intend to create your own lib. 10/10!\nThe example below stages all files (including untracked) and commit them:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 path, err := os.Getwd() // get the current path exitIfError(err) r, err := git.PlainOpen(path) // open the repository related to the path exitIfError(err) w, err := r.Worktree() // get the current worktree exitIfError(err) _, err = w.Add(\u0026#34;.\u0026#34;) // Stage all files exitIfError(err) _, err = w.Commit(\u0026#34;commit yay!\u0026#34;, \u0026amp;git.CommitOptions{}) // commit exitIfError(err) I have to say that for basic operations like this one, we could be better served by executing the git cli directly like in the example below. However, go-git opens several possibilities like in the support of several type of storage, such as in-memory, file system, or anything you can think of as long as you implement the Storer interface. At this moment I\u0026rsquo;m not using such features yet, but I\u0026rsquo;m planning to use them for some commands like got squash that will squash all commits of the current branch.\n1 2 3 4 5 6 7 8 9 10 cmd := exec.Command(\u0026#34;git\u0026#34;, \u0026#34;add\u0026#34;, \u0026#34;-A\u0026#34;) cmd.Stdout = os.Stdout cmd.Stderr = os.Stdout _ = cmd.Run() cmd = exec.Command(\u0026#34;git\u0026#34;, \u0026#34;commit\u0026#34;, \u0026#34;-m\u0026#34;, \u0026#34;commit yay!\u0026#34;) cmd.Stdout = os.Stdout cmd.Stderr = os.Stdout _ = cmd.Run() Fallback to git It is important because I wanted to \u0026ldquo;proxy\u0026rdquo; all unknown got commands to git. In this way, I can use got for my custom commands and the standard git commands without worrying about what is available where. The other benefit is I don\u0026rsquo;t need to implement things that are good enough or I don\u0026rsquo;t use so often.\nImplementing it was fairly simple. The snippet below is part of the root.go\n1 2 3 4 5 6 7 8 9 10 11 12 func Execute() { err := rootCmd.Execute() if err != nil { fallbackToGit() } } func fallbackToGit() { cmd := exec.Command(\u0026#34;git\u0026#34;, os.Args[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stdout _ = cmd.Run() // Skipping error here, because git already sends it to stdout, and I don\u0026#39;t have anything else to do with it. Conclusion and what\u0026rsquo;s next? This small project started as an experiment for playing with writing a custom CLI, but I ended up creating something that is very useful for me. Win-win!\nI still have a few commands I want to introduce in got, but my next step is to create a reasonable test suit, so I can have confidence that things work as they suppose to. This is especially important given it is responsible for my interaction with git. That\u0026rsquo;s a big deal!\nAnyways, I created it to attend to my laziness, but if got looks interesting to you, feel free to use, fork, and contribute. 💪\nI hope this post can help you to see how easy is to write a CLI using Go, and maybe can inspire you to identify things that you do daily and can be somehow optimized.\nTill next time o/\n","date":"2023-03-30T14:00:00+01:00","image":"/posts/embracing-my-lazyness-with-a-custom-git-cli/got.png","permalink":"/posts/embracing-my-lazyness-with-a-custom-git-cli/","title":"🐉 got: Embracing my lazyness with my custom git CLI"},{"content":"We live in an unprecedented era in which we can learn pretty much anything we want for free (or paying very little depending on how much patience you have with ads).\nYouTube is undoubtedly one of the best sources of free content. The only challenge is really to find good channels where you learn stuff and have a good time while doing it.\nThis is the first issue of a series of posts in which I\u0026rsquo;m going to recommend three channels with excellent videos.\nTechWorld with Nana YouTube Channel | Twitter\nThis is my favorite channel for DevOps-related topics. Nana\u0026rsquo;s didactics and real-world examples help a lot to get to know new topics or deep dive into a specific subject. This 4-hour long k8s full course is just one of many others. Check it out.\nMatt Pocock YouTube Channel | Twitter\nAdvanced TypeScript wizardry, plus on-the-day updates from the latest TypeScript releases (and other open-source loveliness). Check it out\nFireship YouTube Channel | Twitter\nThe Gateway Drug for developers. This is how the channel describes itself. 😅\nThe super funny weekly code report and the 100 seconds pills of knowledge about many different things are very informative and entertaining. Check it out.\nWhat about you? What other YouTube channels do you watch to get up to speed?\n","date":"2023-03-28T14:00:00+01:00","image":"/posts/youtube-suggestions-1/youtube.webp","permalink":"/posts/youtube-suggestions-1/","title":"📺 YouTube: Suggestions #1"},{"content":"If you are using Go ad PostgreSQL, and need to performa a bulk import a CSV, it\u0026rsquo;s most likely you will find the COPY protocol is the feature that suits you better. In that direction, you will find examples using pgx CopyFrom that relies on the native protocol, and it\u0026rsquo;s fairly easy to use. However, depending how it\u0026rsquo;s used you can have an exponencial increase of memory consumption of your application making it unreliable and more expensive to run.\nTL;DR;: Don\u0026rsquo;t load the file in memory and use pgx.CopyFromRows. Instead, use the io.Reader of the file and implement a custom pgx.CopyFromSource.\nCheckout the repository with examples and details presented here.\nContext Most of the examples out there, are either using CopyFromRows or CopyFromSlice. However, the big problem is these two options require you to have the entire content in memory to use. This is not a big deal when dealing with small files, there won\u0026rsquo;t be concurrent usage, or you have infinite memory 😅.\nHow big is the problem? Comparing the memory consumption for the two distinct approaches importing a file with ~16MB (1M rows).\nApproach/metric TotalAlloc Sys Stream file 61 MiB 12 Mib Read entire file 84 MiB 58 Mib +37.70% +346.15% Yes, you read it right! using CopyFromRows obtained +346.15% of memory from the OS! 58 MiB instead of 12 MiB.\nYou can read more about the meaning of each metric on https://golang.org/pkg/runtime/#MemStats and find the source code and details of the comparisson on this repository..\nWhat\u0026rsquo;s the most efficient way for using the COPY protocol with pgx? Instead of reading the entire in memory, the idea is to stream each line of the file directly to PostgreSQL. In this way, we only need to keep the current line in-memory as opposed to the entire file.\nHow can we do it? The CopyFrom method receives an implementation of the interface CopyFromSource.\nLet\u0026rsquo;s implement this interface using a CSV file with the followng three columns: first_name, last_name, and city.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 func newPeopleCopyFromSource(csvStream io.Reader) *peopleCopyFromSource { csvReader := csv.NewReader(csvStream) csvReader.ReuseRecord = true // reuse slice to return the record line by line csvReader.FieldsPerRecord = 3 return \u0026amp;peopleCopyFromSource{ reader: csvReader, isBOF: true, // first line is header record: make([]interface{}, len(peopleColumns)), } } type peopleCopyFromSource struct { reader *csv.Reader err error currentCsvRow []string record []interface{} isEOF bool isBOF bool } func (pfs *peopleCopyFromSource) Values() ([]any, error) { if pfs.isEOF { return nil, nil } if pfs.err != nil { return nil, pfs.err } // the order of the elements of the record array, must match with // the order of the columns in passed into the copy method pfs.record[0] = pfs.currentCsvRow[0] pfs.record[1] = pfs.currentCsvRow[1] pfs.record[2] = pfs.currentCsvRow[2] return pfs.record, nil } func (pfs *peopleCopyFromSource) Next() bool { pfs.currentCsvRow, pfs.err = pfs.reader.Read() if pfs.err != nil { // when get to the end of the file return false and clean the error. // If it\u0026#39;s io.EOF we can\u0026#39;t return an error if errors.Is(pfs.err, io.EOF) { pfs.isEOF = true pfs.err = nil } return false } if pfs.isBOF { pfs.isBOF = false return pfs.Next() } return true } func (pfs *peopleCopyFromSource) Err() error { return pfs.err } You can now use this implementation in the CopyFrom method. e.g.\n1 _, err := pgxConn.CopyFrom(ctx, pgx.Identifier{\u0026#34;people\u0026#34;}, peopleColumns, newPeopleCopyFromSource(csvStream)) Conclusion Using the CopyFrom with CopyFromRows or CopyFrom will significantly increase the memory comsultion of your application. The high memory usage can bring several problems like OOM errors, increase of costs, unavailability, etc.\nBy using a custom implementation of CopyFromSource will make your application much more efficient, reliable, and cheaper to ru.\nYou can find the entire source code of the examples above on this repository. There you will also find more deatils about the comparisson and the not-so-great implementation.\n","date":"2023-03-26T14:00:00+01:00","image":"/posts/csv-import-go-postgresql/csv-rainbow.webp","permalink":"/posts/csv-import-go-postgresql/","title":"📂 Go: Importig a CSV to PostgreSQL"},{"content":"Go doesn\u0026rsquo;t have a primitive decimal type for arbitrary-precision fixed-point decimal numbers. Yes, you read it right. Therefore, if you need to deal with fixed-point precision there are two main options:\nUse an external package like decimal, which introduces the decimal type. However, the current version (1.3.1), can \u0026ldquo;only\u0026rdquo; represent numbers with a maximum of 2^31 digits after the decimal point. Use int64 to store and deal with these numbers. For e.g. given you need 6 precision digits, therefore 79.23, 23.00, and 54.123456, become respectively 79230000, 23000000, and 54123456. There is an open proposal to add decimal float types (IEEE 754-2008) in the std lib. However, for now, it\u0026rsquo;s just a proposal being discussed, without guarantee it will be ever added.\n","date":"2023-03-21T14:00:00+01:00","image":"/posts/go-decimal-type/gopher-side-eye.webp","permalink":"/posts/go-decimal-type/","title":"🔢 Go: Where is the decimal type?"},{"content":"Welcome, here I\u0026rsquo;ll be sharing my discoveries, studies, and experiences in the software industry. As a software engineer, I\u0026rsquo;ve had the opportunity to work on a variety of projects and technologies, and I\u0026rsquo;ve learned a lot along the way.\nMy goal in creating this blog is to document my journey and share what I\u0026rsquo;ve learned with others. Whether you\u0026rsquo;re a seasoned developer or just starting out, I hope that the insights and knowledge I share will be useful and valuable to you.\nIn this blog, you can expect to find posts about various topics related to software engineering, such as programming languages, frameworks, tools, best practices, and more. I\u0026rsquo;ll also share my thoughts on the latest trends and developments in the industry.\nAbove all, I hope that this blog will inspire you to continue learning and growing as a software engineer. Thanks for stopping by, and I look forward to sharing my experiences with you.\n","date":"2023-03-19T18:47:00+01:00","image":"/posts/welcome/code-banner.webp","permalink":"/posts/welcome/","title":"👋 Welcome"},{"content":"Below a living list of compiled links for whoever is learning Go.\nOfficial docs Effective Go - https://go.dev/doc/effective_go\nA document that gives tips for writing clear, idiomatic Go code. A must-read for any new Go programmer. It augments the tour and the language specification, both of which should be read first. Go Code Review Comments https://github.com/golang/go/wiki/CodeReviewComment\nThis page collects common comments made during reviews of Go code, so that a single detailed explanation can be referred to by shorthands. This is a laundry list of common mistakes, not a comprehensive style guide. You can view this as a supplement to Effective Go. Go Docs - https://go.dev/doc/ Root for many useful documentations FAQ - https://go.dev/doc/faq Answers to common questions about Go. Blogs Dave Cheney - https://dave.cheney.net/ Specially the practical go section, it has TONS of good advice and real-world examples of how to deal with daily challenges. I highly recommend it. Courses The way to go - https://www.educative.io/courses/the-way-to-go\nIt\u0026rsquo;s a course from educative.io, it goes from the basics concepts of the language to more advanced ones. It also brings very interesting insights into the differences between the approaches of Java/C# to Go.. How to code in go - https://www.digitalocean.com/community/tutorial_series/how-to-code-in-go\nA great collection of tutorials that cover basic Go concepts, ideal for beginers Videos Go in 100 seconds - https://www.youtube.com/watch?v=446E-r0rXHI\u0026t=38s Short introduction about Go Simplicity is complicated - https://www.youtube.com/watch?v=rFejpH_tAHM Rob Pike talks about how Go is often described as a simple language. It is not, it just seems that way. Rob explains how Go\u0026rsquo;s simplicity hides a great deal of complexity, and that both the simplicity and complexity are part of the design. Concurrency is not parallelism - https://www.youtube.com/watch?v=qmg1CF3gZQ0\u0026t=1582s Rob Pike talks about concurrency and how Go implements it Understanding channels - https://www.youtube.com/watch?v=KBZlN0izeiY\u0026t=1011s Channels provide a simple mechanism for goroutines to communicate, and a powerful construct to build sophisticated concurrency patterns. We will delve into the inner workings of channels and channel operations, including how they\u0026rsquo;re supported by the runtime scheduler and memory Concurrency in Go - https://www.youtube.com/watch?v=\\_uQgGS_VIXM\u0026list=PLsc-VaxfZl4do3Etp_xQ0aQBoC-x5BIgJ Playlist with a few short videos about different components of concurrency in Go Just for func - https://www.youtube.com/watch?v=H_4eRD8aegk\u0026list=PL64wiCrrxh4Jisi7OcCJIUpguV_f5jGnZ A very complete playlist of tutorials given by Francesc Campoy, a past Developer Advocate for the Go team at Google, that cover simple to advanced topics in Go Golang crash course - https://www.youtube.com/watch?v=SqrbIlUwR0U A 90 minutes video that covers most of Go features with cristal clear live coding examples, excelent for beginers to get a fast gist of Go Podcasts Go Time - Spotify Diverse discussions from around the Go and its community ","date":"2023-03-19T14:00:00+01:00","image":"/posts/go-resources/gophers.webp","permalink":"/posts/go-resources/","title":"🧠 Go resources for beginners"}]